AngularJS Sitecore Content Tree

This post requires a basic knowledge of AngularJS principles

In a few recent projects I’ve had a need to allow the user to select an item out of Sitecore.  I’ve also been experimenting with how well Sitecore will play with AngularJS, so this was the obvious conclusion.  The ending results were pretty great and i want to share them with you.

Completed

Core Pieces

  1. Server side service to deliver whatever you need out of content items.  For my needs i simply needed the name, ID, and icon.
  2. Create a recursive AngularJS directive to render each layer of the tree

Server Side

First we need a serializable class to store our item data.

public class ContentTreeNode
    {
        public string Icon = "";
        public string DisplayName;
	    public string DatabaseName;
        public ID Id;
        public bool Open;
        public List<ContentTreeNode> Nodes; 

        public ContentTreeNode()
        {
        }

        public ContentTreeNode(Item item, bool open = true)
        {
	        if (item != null)
	        {
		        DatabaseName = item.Database.Name;
		        Open = open;
		        SetIcon(item);
		        DisplayName = item.DisplayName;
		        Id = item.ID;
		        if (Open)
			        Nodes = item.Children.Select(c => new ContentTreeNode(c, false)).ToList();
	        }
        }

	    public void SetIcon(Item item)
	    {
		    if (item != null)
		    {

			    Icon = null;
			    Icon = item[FieldIDs.Icon];
			    if ((Icon.IsNullOrEmpty() || Icon.StartsWith("-") || Icon.StartsWith("~")) && item.Template != null)
			    {
				    Icon = item.Template.Icon;
			    }
		    }
		    if (!string.IsNullOrWhiteSpace(Icon))
		    {
			    string[] parts = Icon.Split('/');
			    Icon = string.Join("/", parts.Skip(parts.Length - 3));
		    }
	    }
    }

At which point we can create a simple rest service to return the JSON of the particular content node and it’s immediate children.

new ContentTreeNode(Factory.GetDatabase(data.database).GetItem(new ID(data.id)));

Client Side

First we need an angularJS factory to handle communicating with the server, in this example there’s only one request and that’s to get the item data.

(function () {
    'use strict';

    angular
        .module('app')
        .factory('CMfactory', CMfactory);

    CMfactory.$inject = ['$http'];

    function CMfactory($http) {
        var service = {

            contentTree: function(id, database, server) {
                var data = { "id": id, "database": database};
                return $http.post("scs/cmcontenttree.json", data);
            }
        };
        
        return service;
    }
})();

Your post destination would be the path to your rest service to get the item data.

Now we need two levels of AngularJS controllers, one to wrap everything, and one to handle the logic of the individual content tree layer.

(function () {
    'use strict';

    angular
        .module('app')
        .controller('cmmastercontroller', cmmastercontroller);

    cmmastercontroller.$inject = ['CMfactory', '$scope'];

    function cmmastercontroller(CMfactory, $scope) {
        /* jshint validthis:true */
    	var vm = this;
        vm.events = {
        	'click': function (val) {
        		vm.events.selected = val;

        	}
        };
    }
})();

Notice how the wrapping controller has an “events” object, this is what is passed into the content tree levels and allows us to control events from each level of the recursive directive from one place.

Next is the controller to handle the tree level.

(function () {
    'use strict';

    angular
        .module('app')
        .controller('cmcontenttreecontroller', cmcontenttreecontroller);

    cmcontenttreecontroller.$inject = ['CMfactory', '$scope'];

    function cmcontenttreecontroller(CMfactory, $scope) {
        /* jshint validthis:true */
        var vm = this;
        vm.Open = false;
        vm.selected = "";
        vm.selectNode = function (id)
        {
            vm.selected = id;
        }
        $scope.init = function (nodeId, selectedId, events) {
        	CMfactory.contentTree(nodeId, "master").then(function (response) {
        		vm.data = response.data;
                if (selectedId === nodeId) {
                    events.selectedItem = vm.data.Id;
                    events.click(vm.data);
                }
            });
        }
    }
})();

Now that we have our factory and controllers we can introduce the AngularJS directive. Into which we pass the root item id to render the tree for “parent”, the events object “events”, and the selected item id “selected”.

(function() {
    'use strict';

    angular
        .module('app')
        .directive('cmcontenttree', cmcontenttree);

    cmcontenttree.$inject = ['CMfactory', '$compile'];

    
    function cmcontenttree(CMfactory, $compile, attributes) {

        var directive = {
            templateUrl: "scs/cmcontenttree.html",
            restrict: 'E',
            scope: {
                parent: '=',
                events: '=',
                selected: '='
            },
            compile: function(tElement, tAttr) {
                var contents = tElement.contents().remove();
                var compiledContents;
                return function(scope, iElement, iAttr) {
                    if (!compiledContents) {
                        compiledContents = $compile(contents);
                    }
                    compiledContents(scope, function(clone, scope) {
                        iElement.append(clone);
                    });
                };
            }
        }
        return directive;
    }

})();

In your templateUrl you would put the path to whatever renders your directives HTML, ours is below.

Markup

First you need markup to place the content tree on the page.

<div ng-controller="cmmastercontroller as vm">
	<cmcontenttree parent="'{GUID}'" events="vm.events"></cmcontenttree>
</div>

Next we have the markup that drives our directive from above.

<div ng-controller="cmcontenttreecontroller as vm" ng-init="init(parent, selected, events)">
    <div class="scContentTreeNode">
        <span ng-if="vm.data.Nodes.length>0">
            <img ng-if="!vm.Open" src="/sitecore/shell/themes/standard/images/treemenu_collapsed.png" class="scContentTreeNodeGlyph" alt="" border="0" ng-click="vm.Open = true">
            <img ng-if="vm.Open" src="/sitecore/shell/themes/standard/images/treemenu_expanded.png" class="scContentTreeNodeGlyph" alt="" border="0" ng-click="vm.Open = false">
        </span>
        <span ng-if="vm.data.Nodes.length ===0">
            <img src="/sitecore/shell/themes/standard/images/noexpand15x15.gif" class="scContentTreeNodeGlyph" alt="" border="0">
        </span>
        <div class="scContentTreeNodeGutter">

        </div>
        <a hidefocus="true" ng-class="events.selectedItem == vm.data.Id ? 'scContentTreeNodeActive':'scContentTreeNodeNormal'" style="position: relative;" ng-click="events.click(vm.data);events.selectedItem = vm.data.Id">
            <span ng-if="vm.data.Icon" class="scContentTreeSelectable">
                <img ng-src="/temp/IconCache/{{vm.data.Icon}}" width="16" height="16" class="scContentTreeNodeIcon" alt="" border="0">
                {{vm.data.DisplayName}}
            </span>
            <span ng-hide="vm.data.Icon">
                <img src="/sitecore/shell/themes/standard/images/sc-spinner16.gif" />
            </span>
        </a>
        <div ng-if="vm.Open" class="scContentTreeIndent" ng-repeat="item in vm.data.Nodes">
            <cmcontenttree parent="item.Id" selected="selected" events="events"></cmcontenttree>
        </div>
    </div>
</div>

Styles

This example mostly relies on the Sitecore Speak content tree styles

<link rel="stylesheet" href="/sitecore/shell/client/Speak/Assets/css/speak-default-theme.css" />

however to clean it up a bit i’ve added my own style touches, feel free to add them to yours or customize it however you like.

.scContentTreeNodeGutter {
    display: inline;
}
.scContentTreeNode {
    padding: 0px 5px 0px 16px;
    white-space: nowrap;
    clear: both;
}
.scContentTreeNodeGlyph {
    margin: 5px 0px;
}
a.scContentTreeNodeActive {
    background-color: #D0EBF6;
}
  a.scContentTreeNodeNormal:hover{
    background-color: #E3E3E3;
  }
  .scContentTreeNode > a {
    display: inline-block;
    padding: 3px 5px;
    margin: 1px 0;
    height: 21px !important;
}
  .scContentTreeSelectable {
      cursor: pointer;
  }

What do we have here?

Once you have it all set up and working, inside your outer controller you’ll have an events object that will contain the currently selected sitecore item. Use it however you wish in your client side code.

Ideas

There are many practical applications to having a content tree in your control, i’ve used it to browse content from a different server for content diffs and to use it to select a datasource item for a custom rendering operation. The possibilities are limitless, so have fun with it.