SPEAK UI Filterable TreeView

While testing some Sitecore 7.5 features and reviewing the Sitecore 8 Videos it is obvious that SPEAK UI is quickly becoming the new trend for developing Sitecore Modules. Recently I stumbled upon the following SDN Thread – SPEAK UI – Filter TreeView. It turns out that the existing TreeView doesn’t support features like filtering by Template or Item ID. On the bright side – every SPEAK Control can be extended pretty easy.

NOTE: You must have Sitecore Rocks Installed To Develop SPEAK Modules (and you need to have it in general, because it is awesome !). If you don`t have Sitecore Rocks Installed, please visit this place first to find some tutorials and the extension itself.

NOTE: For the purposes of this blog post I am going to use the following technologies – Visual Studio 2013, Sitecore 7.5, Sitecore Rocks 1.2.6.0

Here are the 3 easy steps to create a Filterable TreeView.

Step 1 – Setup the Visual Studio Project

Create a new ASP.NET Web Application Project (I am going to name it TreeViewWithFilters). It should use the empty project template with folders and core references for MVC. If you are not sure on how to do this please see the screenshot bellow.

Project Setup

After the project is created delete everything from it and create the following folder structure – /sitecore/shell/client/Business Component Library/Layouts/Renderings/ListsAndGrids/TreeViews. Your project should look like the screenshot bellow.

Tree View With Filters

Set the System.Web.Mvc Reference`s Copy Local Property to False (for some strange reason it is conflicting with the Sitecore Version despite the fact it is using the same version that Sitecore uses).

From your local Sitecore Instance add references to Sitecore.Kernel, Sitecore.Mvc, Sitecore.Speak.Client, Sitecore.Speak.Mvc (I usually keep the files in a folder named “Components” inside the solution root) with Copy Local set to False.

With this the project is all set and we can start hacking !

Step 2 – Create the Custom Component

Under the TreeViews folder create a new SPEAK Component with JavaScript. Place it under /core/sitecore/client/Business Component Library/Layouts/Renderings/Lists and Grids in the Content Treer. Your Solution and your Sitecore Content Tree should now look like this:

Initial Creation

After this is set – we need to create the Rendering Parameters. As we are going to extend the existing Tree View – copy and paste the TreeView Parameters (with Rocks you can use Clipboard -> Copy (CTRL + C), Clipboard -> Paste (CTRL + V)) and rename them to TreeViewWithFilters Parameters. After this is set we are going to add a new template section called Filters. The section will have one Single-Line Text Field – ExcludeTemplatesForDisplay (Make sure the field is marked as Shared). The template should look like the screenshot bellow.

Rendering Parameters

After this is set let’s move to the code !

Step 3 – The Codezzz !

Again as we are going to extend the existing TreeView we will reuse all the code there and just do a few small modifications. The original TreeView code files are:

  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.cshtml
  • [SITECORE_WEB_ROOT\sitecore\shell\client\Business Component Library\Layouts\Renderings\ListsAndGrids\TreeViews\TreeView.js

TreeViewWithFilter.cshtml Changes

Our TreeViewWithFilter.cshtml should look like this:


@using Sitecore
@using Sitecore.Data.Items
@using Sitecore.Globalization
@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Resources
@using Sitecore.Web.UI
@using Sitecore.Web.UI.Controls.Common.JqueryUiUserControls
@model RenderingModel
@{
    var userControl = this.Html.Sitecore().Controls().GetJqueryUiUserControl(this.Model.Rendering);
    userControl.Class = "sc-treeview";
    // TreeViewWithFilters Addition
    userControl.Requires.Script("business", "treeviewwithfilters.js");
    var rootItemIcon = string.Empty;
    string rootItemId = userControl.GetString("RootItem");
    string contentLanguage = userControl.GetString("ContentLanguage");
    var selectedItems = userControl.GetString("SelectedItemId");
    var preLoadPath = userControl.GetString("PreLoadPath");

    var checkedItems = userControl.GetString("CheckedItems");
    var clickFolderMode = userControl.GetInt("ClickFolderMode");
    var checkboxEnabled = userControl.GetBool("IsCheckboxEnabled");
    var showScIcons = userControl.GetBool("ShowSitecoreIcons");
    var selectMode = userControl.GetInt("SelectMode");

    Item rootItem = null;
    if (!string.IsNullOrEmpty(rootItemId))
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetItem(rootItemId, Language.Parse(contentLanguage));
    }

    if (rootItem == null)
    {
        rootItem = ClientHost.Databases.ContentDatabase.GetRootItem();
    }

    rootItemIcon = Images.GetThemedImageSource(!string.IsNullOrEmpty(rootItem.Appearance.Icon) ? rootItem.Appearance.Icon : "Applications/16x16/documents.png", ImageDimension.id16x16);
    userControl.SetAttribute("data-sc-rootitem", rootItem.DisplayName + "," + rootItem.Database.Name + "," + rootItem.ID + "," + rootItemIcon);
    userControl.SetAttribute("data-sc-rootitempath", rootItem.Paths.Path);
    userControl.SetAttribute("data-sc-loadpath", preLoadPath);
    userControl.SetAttribute("data-sc-contentlanguage", contentLanguage);

    // TreeViewWithFilters Addition
    userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

    userControl.AddOptionAttribute("selectedItems");
    //userControl.AddOptionAttribute("preLoadPath");
    userControl.AddOptionAttribute("checkedItems");
    userControl.AddOptionAttribute("clickFolderMode");
    userControl.AddOptionAttribute("selectMode");
    userControl.AddBoolOptionAttribute("IsCheckboxEnabled", "checkbox");
    userControl.AddBoolOptionAttribute("ShowSitecoreIcons");
    userControl.AddBoolOptionAttribute("ShowHiddenItems", "showhiddenitems");

    var htmlAttributes = userControl.HtmlAttributes;
}
<div @htmlAttributes>
    <ul></ul>
</div>

There are 2 minor changes to the original code:

1. Changing the required script to our Javascript file:


    userControl.Requires.Script("business", "treeviewwithfilters.js");

2. Passing the ExcludeTemplatesForDisplay parameter:


 // TreeViewWithFilters Addition
 userControl.SetAttribute("data-sc-excludetemplatesfordisplay", userControl.GetString("ExcludeTemplatesForDisplay"));

TreeViewWithFilter.js Changes

Our TreeViewWithFilter.js should look like this:


/// <reference path="../../../../../../assets/lib/dist/sitecore.js" />
/// <reference path="../../../../../../assets/vendors/underscore/underscore.js" />

require.config({
    paths: {
        dynatree: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/jquery.dynatree-1.2.4",
        dynatreecss: "/sitecore/shell/client/Speak/Assets/lib/ui/1.1/deps/DynaTree/skin-vista/ui.dynatree",
    },
    shim: {
        "dynatree": { deps: ["jqueryui"/*, 'css!dynatreecss'*/] }
    }
});

define(['sitecore', 'dynatree'], function (_sc) {
    var control = {
        componentName: "TreeView",
        selector: ".sc-treeview",
        control: "dynatree",
        namespace: "ui-",

        attributes:
        [
          { name: "selectedItemId", added: true },
          { name: "selectedItemPath", added: true },
          { name: "checkedItemIds", added: true },
          { name: "pathToLoad", added: true },
          { name: "isBusy", added: true },
          { name: "selectedNode", added: true },
          { name: "showSitecoreIcons", added: true, defaultValue: true },
          { name: "isCheckboxEnabled", pluginProperty: "checkbox", defaultValue: true },     // Show checkboxes.
          { name: "isKeyboardSupported", pluginProperty: "keyboard", defaultValue: true },     // Support keyboard navigation.
          { name: "isPersist", pluginProperty: "persist", defaultValue: false },      // Persist expand-status to a cookie
          { name: "isAutoFocus", pluginProperty: "autoFocus", defaultValue: true },    // Set focus to first child, when expanding or lazy-loading.
          { name: "isAutoCollapse", pluginProperty: "autoCollapse", defaultValue: false }, // Automatically collapse all siblings, when a node is expanded.
          { name: "clickFolderMode", defaultValue: 1 }, // 1:activate, 2:expand, 3:activate and expand
          { name: "selectMode", defaultValue: 3 },      // 1:single, 2:multi, 3:multi-hier
          { name: "isNoLink", pluginProperty: "noLink", defaultValue: false },       // Use <span> instead of <a> tags for all nodes
          { name: "debugLevel", defaultValue: 0 },       // 0:quiet, 1:normal, 2:debug
          { name: "showHiddenItems", defaultValue: false },
          { name: "contentLanguage", defaultValue: "en" },

          // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }
        ],

        events:
        [
          { name: "onPostInit" },        // Callback(isReloading, isError) when tree was (re)loaded.
          { name: "onActivate", on: "onActivate" },        // Callback(dtnode) when a node is activated.
          { name: "onDeactivate" },      // Callback(dtnode) when a node is deactivated.
          { name: "onSelect", on: "onSelect" },          // Callback(flag, dtnode) when a node is (de)selected.
          { name: "onExpand" },          // Callback(flag, dtnode) when a node is expanded/collapsed.
          { name: "onLazyRead", on: "nodeExpanding" },   // Callback(dtnode) when a lazy node is expanded for the first time.
          { name: "onCustomRender" },    // Callback(dtnode) before a node is rendered. Return a HTML string to override.
          { name: "onCreate" },          // Callback(dtnode, nodeSpan) after a node was rendered for the first time.
          { name: "onRender" },          // Callback(dtnode, nodeSpan) after a node was rendered.
          { name: "postProcess" }        // Callback(data, dataType) before an Ajax result is passed to dynatree.
        ],

        functions:
        [
          { name: "disable" },
          { name: "enable" },
          { name: "getTree" },
          { name: "getRoot" },
          { name: "getActiveNode" },
          { name: "getSelectedNodes" }
        ],

        view:
        {
            initialized: function () {
                var clickFolderModeAttribute = this.$el.attr("data-sc-option-clickfoldermode"),
                  selectModeAttribute = this.$el.attr("data-sc-option-selectmode");

                this.model.set("clickFolderMode", clickFolderModeAttribute && !isNaN(clickFolderModeAttribute) ? parseInt(clickFolderModeAttribute, 10) : 1);
                this.model.set("selectMode", selectModeAttribute && !isNaN(selectModeAttribute) ? parseInt(selectModeAttribute, 10) : 3);
                this.model.set("contentLanguage", this.$el.attr("data-sc-contentlanguage"));
                this.model.set("showHiddenItems", this.$el.attr("data-sc-option-showhiddenitems") !== undefined && this.$el.attr("data-sc-option-showhiddenitems").toLowerCase() === "true");

                // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

                var items = this.$el.attr("data-sc-rootitem");
                if (!items) {
                    return;
                }

                var pathToLoad = this.$el.attr("data-sc-loadpath");

                if (pathToLoad) {
                    this.model.set("pathToLoad", pathToLoad);
                }

                var parts = items.split("|");
                var selectedItemUri = null;

                for (var n = 0; n < parts.length; n++) {
                    var uri = parts[n].split(',');

                    var databaseUri = new _sc.Definitions.Data.DatabaseUri(uri[1]);
                    var itemUri = new _sc.Definitions.Data.ItemUri(databaseUri, uri[2]);
                    var itemIcon = uri[3];

                    var rootNode = {
                        title: uri[0],
                        key: itemUri.getItemId(),
                        isLazy: true,
                        isFolder: true,
                        icon: this.model.get("showSitecoreIcons") ? itemIcon : "",
                        url: "#",
                        path: "/" + uri[0],
                        itemUri: itemUri,
                        selected: selectedItemUri == null
                    };

                    if (this.widget) {
                        this.widget.apply(this.$el, ["getRoot"]).addChild(rootNode);
                    }

                    if (this.model.get("pathToLoad") && this.model.get("pathToLoad") !== "") {
                        this.loadKeyPath();

                    }
                }

                this.pendingRequests = 0;
            },

            onActivate: function (node) {
                if (node && node.data && node.data.itemUri && node.data.path) {

                    this.model.set("selectedNode", node.data);
                    this.model.set("selectedItemId", node.data.itemUri.itemId);
                    this.model.set("selectedItemPath", node.data.path);
                }
            },

            onSelect: function (flag, node) {
                var list = [];

                _.each(node.tree.getSelectedNodes(), function (dnode, index) {
                    list.push(dnode.data.itemUri.itemId);
                });
                this.model.set("checkedItemIds", list.join("|"));
            },

            nodeExpanding: function (node) {

                var itemUri = node.data.itemUri, children;

                this.pendingRequests++;
                this.model.set("isBusy", true);

                var self = this;

                // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

                    self.appendLoadedChildren(node, filteredItems, res);

                    self.pendingRequests--;
                    if (self.pendingRequests <= 0) {
                        self.pendingRequests = 0;
                        self.model.set("isBusy", false);
                    }

                    if (self.model.get("pathToLoad") && self.model.get("pathToLoad").length > 0) {
                        self.loadKeyPath();
                    }
                }, {
                    fields: ["__Hidden"],
                    language: self.model.get("contentLanguage")
                });

            },

            appendLanguageParameter: function (item) {
                if (item.$icon.indexOf(".ashx") > 0) {
                    item.$icon += "&la=" + this.model.get("contentLanguage");
                    item.$mediaurl += "&la=" + this.model.get("contentLanguage");
                }
            },

            appendLoadedChildren: function (parentNode, childrenNodes, destArray) {
                //add children

                var self = this;
                _.each(childrenNodes, function (item) {
                    var newNode = {};
                    newNode.rawItem = item;
                    newNode.title = item.$displayName;
                    newNode.key = item.itemId;
                    if (self.model.get("showSitecoreIcons")) {
                        self.appendLanguageParameter(item);
                        newNode.icon = item.$icon;
                    }
                    newNode.itemUri = item.itemUri;
                    newNode.path = item.$path;
                    newNode.select = self.model.get("selectMode") === 3 ? parentNode.isSelected() : false;
                    newNode.isFolder = item.$hasChildren;
                    newNode.isLazy = item.$hasChildren;
                    destArray.push(newNode);

                }, this);

                parentNode.setLazyNodeStatus(DTNodeStatus_Ok);
                parentNode.addChild(destArray);
                //expand needed node
            },
            loadKeyPath: function () {
                var separator = "/",
                    pathParts,
                    currentNodeId,
                    path = this.model.get("pathToLoad"),
                    tree = this.widget.apply(this.$el, ["getTree"]),
                    node;

                pathParts = path.split(separator);
                if (pathParts.length === 0) {
                    return false;
                }
                else {
                    currentNodeId = pathParts.shift();
                    if (!currentNodeId) return false;
                    node = tree.getNodeByKey(currentNodeId);
                    if (!node) {
                        this.model.set("pathToLoad", "");

                        return false;
                    }

                    this.model.set("pathToLoad", pathParts.join(separator));
                    node.expand();

                    if (pathParts.length === 0) {
                        this.model.set("selectedItemId", currentNodeId);
                        node.activate(true);
                        node.select(true);
                    }

                }
            }
        }
    };

    _sc.Factories.createJQueryUIComponent(_sc.Definitions.Models, _sc.Definitions.Views, control);
});

The method is modified to do the following.

1. Register our new attribute with the model.

   // TreeViewWithFilters Addition
          { name: "excludeTemplatesForDisplay", defaultValue: "" }

2. Load the value we passed in the rendering

         // TreeViewWithFilters Addition
                this.model.set("excludeTemplatesForDisplay", this.$el.attr("data-sc-excludetemplatesfordisplay"));

3. Override the nodeExpanding method in order for it to filter based on our custom filter.

        // TreeViewWithFilters Addition
                var excludedTemplatesList = new Array();
                if (this.model.get("excludeTemplatesForDisplay") !== undefined && this.model.get("excludeTemplatesForDisplay") != "") {
                    excludedTemplatesList = this.model.get("excludeTemplatesForDisplay").split("|");
                }

                var database = new _sc.Definitions.Data.Database(itemUri.getDatabaseUri());
                database.getChildren(itemUri.getItemId(), function (items) {
                    var res = [],
                      filteredItems = _.filter(items, function (item) {

                          // TreeViewWithFilters Modification

                          if (self.model.get("showHiddenItems")) {
                              return true && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                          }

                          return item.__Hidden !== "1" && $.inArray(item.$templateId, excludedTemplatesList) == -1;
                      });

And this is it !

The component can be found in the standard Speak Rendering selector as seen on the screenshot bellowTree View With Filters

After the component is included in the layout the custom field can be edited from the component properties.

Tree View With Filters Properties

Currently the code supports only TemplateIDs, but it can be modified to support template keys etc.

The code is also pushed to Bitbucket along with some extra filters – ExcludeItemsForDisplay and IncludeTemplatesForDisplay. Adding new filters is just a matter of modifying the Parameter templates, handling them in the rendering and modifying the nodeExpanding method in order for it to support your custom type of filter.

Happy Speaking !

2 thoughts on “SPEAK UI Filterable TreeView”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s