Tag Archives: umbraco 7

Umbraco: Displaying Content Tree with AngularJS Directive

It seems that more and more CMS-based websites implement frontend editing. Sitecore gives you this possibility by default, but in Umbraco, the feature has to be custom made for every solution.

When creating new content from front-end, one of the challenges encountered most often is placing the new content in the content tree.

Here is a AngularJS directive based solution to the problem. It is easy to port from solution to solution and  customize in the front-end. When the relevant files are added to any given solution, all that a front-end developer needs to do, to display the content tree is include <content-tree> directive in her html.

The directive can be customized and show only certain document types or only certain levels. The content nodes that do not belong to the selection can either be removed from the selection all together or disabled.

The following demo displays the nodeId of the selected node. In a solution that uses content tree  to place newly created content, the nodeId would be saved in a hidden input field and submited to the controller together with the new content.

Solution requires AngularJS and it was made for Umbraco 7 with MVC.

Solutions consists of 4 files:

  • \frontend\js\QApp\controllers\contentTreeController.js

This controller makes an ajax call to surface controller and passes on the filters from directive. It then receives the Node list and exposes it on the scope.

gameQApp.controller('TreeController', ['$scope', '$http', function ($scope, $http) {
    var treeDirective      = angular.element(document.querySelector('content-tree')),
        docTypes           = treeDirective.attr('documenttypes'),
        excludedNodes      = treeDirective.attr('excludednodes'),
        levelRange         = treeDirective.attr('levelrange');

    $http({
      url: 'umbraco/surface/ContentTreeSurface/GetNodes',
      data: { 'docTypes' : docTypes, 'excludedNodes' : excludedNodes, 'levelRange' : levelRange},
      method: 'POST'
    }).success(function(data){
        $scope.nodes = data.slice().reverse();
    });

}]);
  • \Controllers\SurfaceControllers\ContentTreeSurfaceController.cs

If like me, you are coming from a front-end development background, this code might look a bit daunting. What it basically does is crawling down the node tree in Umbraco and creating a flat list of IContent objects while applying the various filters.

Once the flat list (called _result) is ready, the code runs through it and creates structured list (starting from foreach (IContent item in _result) line). The loop, creates a Node object and checks if it’s parent is already on the list. If the parent is on the list, the new Node is added to its children list.

The structured list is returned to the angular controller.

using GameQ.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using umbraco;
using umbraco.cms.businesslogic.web;
using Umbraco.Core.Models;
using Umbraco.Web.Mvc;

namespace GameQ.Controllers.SurfaceControllers
{
    public class ContentTreeSurfaceController : SurfaceController
    {
        private List _result = new List();

        public JsonResult GetNodes(string docTypes = "all", string excludedNodes = "hide", string levelRange = "all")
        {

            // content and type services
            var cs = Services.ContentService;

            //get all nodes
            List level1 = cs.GetRootContent().ToList();
            foreach (IContent item in level1)
            {
                ProcessAllItems(item, docTypes, excludedNodes, levelRange);
            }

            //get filtered nodes
            _result = _result.OrderBy(x => x.Level).ToList();

            List nodes = new List();

            //build a structured list
            foreach (IContent item in _result)
            {
                //create Node object
                Node nItem = new Node();
                nItem.nodeId = item.Id;
                nItem.name = item.Name;
                nItem.children = new List();
                nItem.enabled = true;
                nItem.level = item.Level;

                //check if the node should be disabled
                if (item.WriterId == 999999999)
                {
                    nItem.enabled = false;
                }

                //put the node in the list
                if ((nodes.FirstOrDefault(x => x.nodeId == item.Id)) == null)
                {
                    //find if parent exists
                    Node parent = null;
                    parent = FindParent(nodes, item.ParentId, parent);

                    if (parent != null)
                    {
                        parent.children.Add(nItem);
                    }
                    else
                    {
                        nodes.Add(nItem);
                    }
                }
            }

            return Json(nodes, JsonRequestBehavior.AllowGet);
        }

        // building a flat list of all content
        private void ProcessAllItems(IContent n, string docTypes, string excludedNodes, string levelRange)
        {
            ProcessItem(n, docTypes, excludedNodes, levelRange);
            foreach (var item in n.Children().ToList())
            {
                ProcessAllItems(item, docTypes, excludedNodes, levelRange);
            }
        }

        //add node to flat list while applying the relevant filters
        private void ProcessItem(IContent node, string docTypes, string excludedNodes, string levelRange)
        {
            string[] docTypesList   = docTypes.Split('-');
            string[] levels = levelRange.Split('-');

            bool toAdd = false;

            if (docTypes == "all")
            {
                toAdd = true;
            }
            else
            {
                foreach (var type in docTypesList)
                {
                    if (node.ContentType.Alias == type)
                    {
                        toAdd = true;
                    }
                }
            }

            if (levelRange == "all")
            {
                if (toAdd == true)
                {
                    toAdd = true;
                }
            }
            else
            {
                if (levelRange.Contains("-"))
                {
                    if (node.Level >= Int32.Parse(levels[0]) && node.Level <= Int32.Parse(levels[1]) && toAdd == true)                     {                         toAdd = true;                     }                     else                     {                         toAdd = false;                     }                 }                 else                 {                     if (node.Level >= Int32.Parse(levelRange) && toAdd == true)
                    {
                        toAdd = true;
                    }
                    else
                    {
                        toAdd = false;
                    }
                }
            }

            if (toAdd)
            {
                _result.Add(node);
            }
            else if (toAdd == false && excludedNodes == "show")
            {
                node.WriterId = 999999999;
                _result.Add(node);

            }
        }

        //retriving parent to build a structured list
        public Node FindParent(List nodes, int ParentId, Node parent)
        {

            foreach (var node in nodes)
            {
                if (node.nodeId == ParentId)
                {
                    parent = node;
                    break;
                }
                else
                {
                    if (node.children.Any())
                    {
                       parent = FindParent(node.children, ParentId, parent);
                       if (parent != null)
                       {
                           break;
                       }

                    }
                }
            }

            return parent;
        }

    }
}
  • \Models\ContentTreeModel.cs

This is a very simple model that describes Node object. This limits the amount of data sent back to frontend and allows for children property (used for the structured list)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Umbraco.Core.Models;

namespace GameQ.Models
{
    public class Node
    {
        public int nodeId { get; set; }
        public string name { get; set; }
        public List children { get; set; }
        public bool enabled { get; set; }
        public int level { get; set; }
    }
}
  • \frontend\js\QApp\directives\contentTree.js

The directive file, contains two elements: the factory – RecursionHelper and the directive – contentTree. RecursionHelper allows the directive to read the nested list of nodes and display the template in a nested fashion. RecursionHelper was written by Mark Lagendijk and you can see it here.

The directive part describes the…directive (sic!), its template, and two functions. OnClick exposes the nodeId of the clicked node on the rootscope and showChildren is just a UI detail that allows for unfolding of the content tree.

gameQApp.factory('RecursionHelper', ['$compile', function($compile){
    return {
        /**
         * Manually compiles the element, fixing the recursion loop.
         * @param element
         * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
         * @returns An object containing the linking functions.
         */
        compile: function(element, link){
            // Normalize the link parameter
            if(angular.isFunction(link)){
                link = { post: link };
            }

            // Break the recursion loop by removing the contents
            var contents = element.contents().remove();
            var compiledContents;
            return {
                pre: (link && link.pre) ? link.pre : null,
                /**
                 * Compiles and re-adds the contents
                 */
                post: function(scope, element){
                    // Compile the contents
                    if(!compiledContents){
                        compiledContents = $compile(contents);
                    }
                    // Re-add the compiled contents to the element
                    compiledContents(scope, function(clone){
                        element.append(clone);
                    });

                    // Call the post-linking function, if any
                    if(link && link.post){
                        link.post.apply(null, arguments);
                    }
                }
            };
        }
    };
}]);

gameQApp.directive('contentTree', function(RecursionHelper) {
  return {
    restrict: 'E',
    template:
        '<ul>' + 
               '<li ng-repeat="node in nodes" data-nodeId="{{node.nodeId}}" id="{{node.nodeId}}">' + 
                   '<span ng-click="showChildren(node.nodeId)"><span class="glyphicon glyphicon-triangle-right" aria-hidden="true" ng-show="node.children.length"></span><span class="glyphicon glyphicon-minus" aria-hidden="true" ng-show="!(node.children.length)"></span> {{node.name}}</span>' +
                   ' <a ng-click="onClick(node.nodeId)" ng-if="node.enabled">Choose node</a>' +
                   '<content-tree nodes="node.children"></content-tree>' +                    
               '</li>' +
          '</ul>',
    scope: {
      nodes: '=',
      choosenode: '&',
      documenttypes: "="
    },

    compile: function(element, $scope, attr) {
      return RecursionHelper.compile(element, function(scope, iElement, iAttrs, controller, transcludeFn){
      });
    },
    controller: function($scope, $attrs) {
     var scope = angular.element(document.querySelector('[ng-app]')).scope();
      $scope.onClick = function(nodeId) {
        scope.chosenNodeId = nodeId;
      }

      $scope.showChildren = function(nodeId) {
        var id            = nodeId,
            listElement   = $.find('#' + id),
            subList       = $(listElement[0]).find('>content-tree').find('>ul'),
            icon          = $(listElement[0]).find('>span').find('.glyphicon');

            subList.toggleClass('showList');
            icon.toggleClass('glyphicon-triangle-right');
            icon.toggleClass('glyphicon-triangle-bottom');
      }
    }
  };
});

Once all these files are in place, all that is left is adding the directive element to the html:

<div ng-controller="TreeController">
	<content-tree nodes="nodes" choosenode="choosenode()" excludednodes="show" documenttypes="Home-Games-Game">
	</content-tree>
	<p>Chosen node: <span ng-bind="chosenNodeId"></span></p>
</div>

The three configuration parameters are documenttypes, levelrange and exludednodes.

  • if no configuration params are sent, the tree will contain all the nodes. See http://marialind.dk/#/search
  • documenttypes – lists the documenttypes that should be included (ex: “Frontpage-Article-List”). See http://marialind.dk/#/bydoctype
  • levelrange – either a starting level for the tree or a level range (ex: “2″ or “2-3″). “2-2″ will list only level 2. See http://marialind.dk/#/bylevel
  • excludednodes – if set to “show”, it will cause the excluded nodes to show on the list but without the “choose node” link. See http://marialind.dk/#/bydoctypedisable

The directive can be added on any number of pages on a website but, as of now, it cannot be added several times on the same page. At least not without modification of the directive and the controller.

The other weak point of the directive is the way I mark disabled nodes in .cs controller. There must be a better way then changing the WriterId to 999999999 if the node should be added to the list but disabled.

Improving UX with Umbraco 7

Umbraco 7 logoOn November 21st Niels Hartvig announced the release of long awaited Umbraco 7 (codename Belle). The new version runs on the same engine as Umbraco 6 but sports a completely new backoffice. Here are my initial thoughts on the topic + a little introductory tutorial.

In this post I’ll:

  1. be really excited about the new look of Umbraco 7 backoffice (I mean, have you seen the skin on that?!)
  2. explain a bit more seriously why I think Umbraco 7 will be a revelation for editors and will take this CMS to a whole new level
  3. show you a quick tutorial of how to build Umbraco 7 custom property editor and leverage the new secret powers to improve editors’ user experience

Umbraco 7, codename Belle indeed

So, have you had a look at the sparkling beauty that is Umbraco 7′s interface yet? No more feeling like a bum when working on your Umbraco installation in a crowded hipster coffee shop, right? Ok, that really isn’t the point. The point is that the new backoffice, while looking nicely minimal, also follows many of the basic interface design patterns that the old version gleefully ignored.

Some examples include improved workflows with “save and publish button at the bottom of the page; animation and self-healing pattern on deleting items from content tree; or additional information appearing on hover. When adding media items to RTE, the editor will be able to use drag-and-drop functionality or upload new files in the same tab sliding from the right which makes the process faster and more intuitive.

The new interface is also much faster than the old one, which makes a great difference when you work long hours curating content.

Improving editors’ experience with Umbraco 7

However, the best feature of Umbraco’s new backoffice is that it is built with HTML5 and AngularJS. This means that it is relatively easy for developers to create custom property editors. Why is this relevant? Don’t we already have all the properties we might want? Well, kind of. But with Umbraco 7 we can combine properties to create new ones or slightly adjust them to fit better with editors’ needs. An example that springs to mind is a property editor that allows editors to arrange content by dragging-and-dropping content boxes.

But mainly I see Umbraco 7′s advantage over older versions and other CMSs in that it allows developers to adjust interface to improve user experience. Here is what I mean with this:

Imagine you are building a website for an airport. It is a large and complex site with many content items and functionalities. On the frontpage of the website there is a small box that displays the status of the airport (“All is well”, “There are long queues, make sure to come in advance”, “We are snowed in and all flights are cancelled” etc.).  The editor would also like to be able to choose whether the message is “not important”, “important”, or “critical”. The status is used to decide two things: whether the message should be emailed to employees and how it is displayed on the website (i.e the css styling). The property editors are a textbox and a radio button list with labels denoting the importance of the status.

In a complex, large installation it is easy to imagine that editors forget exactly what each status means in practice. Of course I am choosing a “very important” for a status about longer queues but does it mean that the text is displayed in red? And will the employees be emailed? Any developer worth his money will instantly describe all the options in the property editor’s description field. Fair enough, but think of the volume of tiny text that you need to use to describe in details just these 3 options. And what if there are 5 or 10 of them? Scanning through this text is simply not optimal when the editor wants to perform a simple task of choosing the importance of his content.

Enters, Umbraco 7. With HTML5 and AngularJS, it is a question of about 30 minutes to build a new property editor that will deal with this clatter in the best UX style. Instead of having one long text, the developer can ensure that hovering over each option will cause a hint box to appear, describing the consequences of this particular choice. No clatter, only the information the editor needs, delivered in relevant small chunks.

This way of delivering content is in keeping with hover-reveal contextual content UX pattern. It reduced the noise and delivers only relevant information. Keep in mind though that for mobile devices this interaction would have to happen on click and not hover!

Building your first custom property editor with Umbraco 7

Disclaimer: This mini tutorial is based on and extends this tutorial so you might want to have a look at it as well.

Required files

To begin with, in your App_Plugin folder you will need to create a folder for your property (RadioListEditor in my case). The new folder will include the following files:

  • package.manifest
  • radioList.controller.js
  • radioList.html
  • toolTipsStyle.css

package.manifest is a json file that describes the property editor and binds all required files together.

{
    propertyEditors: [
        {
            /*unique alias*/
            alias: "My.RadioListEditor",
            /*name*/
            name: "Radio List Editor",
            /*html file (view) to render the editor*/
            editor: {
                view: "~/App_Plugins/RadioListEditor/radioList.html"
            }
        }
    ]
    ,
    //array of files we want to inject into the application on app_start.
    //In this case a controller (necessary) and stylesheet (optional)
    javascript: [
        '~/App_Plugins/RadioListEditor/radioList.controller.js'
    ]
	,
	css: [
		 '~/App_Plugins/RadioListEditor/tooltipsStyle.css'
	]
}

radioList.controller.js is, as the name suggests, your AngularJS controller that does all the heavy lifting of passing data there and back. In our case the controller doesn’t actually need to do much other than pass data to the model so it will be limited to a bare minimum:

angular.module("umbraco")
    .controller("My.RadioListController",
        function (){}
    );

It is not important that the function is empty, but it still have to be there.

The last required file is radioList.html which is an html snippet used to render the property editor in backoffice (a.k.a your view)

<div>
    <ul ng-controller="My.RadioListController">
        <li class="radio-option">
            <input type="radio" value="1" ng-model="model.value">
            <label>Not important</label>
            <div class="radio-tooltip">The color of the status message will be black</div>
        </li>
        <li class="radio-option">
            <input type="radio" value="2" ng-model="model.value"/>
            <label>Important</label>
            <div class="radio-tooltip">The color of the status message will be green</div>
        </li>
        <li class="radio-option">
            <input type="radio" value="3" ng-model="model.value"/>
            <label>Critical</label>
            <div class="radio-tooltip">The color of the status message will be red</div>
        </li>
    </ul>
</div>

As you can see there is not much magic here either, except for ng-model=”model.value”. This is an AngularJS directive (easily recognized by the ng- prefix) that takes care of passing the value your editor chooses to the model that handles content in Umbraco. There is probably a whole other article to write about how you use model.value and what it actually does. For now, just know that you need it in the input fields so that the editor’s input is saved and accessible when you want to display it.

And lastly, an optional file that takes care of displaying div tooltips on hover – the css file

.radio-tooltip {
  display: none; }

.radio-option:hover .radio-tooltip {
  display: block;
  width: 150px;
  margin-top: -22px;
  padding: 10px;
  border: 1px solid #fff;
  background: #F2E994;
  position: absolute;
  left: 410px;
  z-index: 9999; }

Note that if you want this functionality to be available for mobile-device users, you might want to cause the behavior with JavaScript or do some media-query/wurfl magic instead. If you choose to use JavaScript you will need to include your script in the same way you included the .css file in package.manifest

Once all your files are created all you need to do is restart your application to make sure that the new property editor is registered (you might need to restart the application every time you change something in the controller code).

Next, go to the developer section of your backoffice and click on the 3 dots next to the Data Types option. Create a new Data Type and give it a name of your choosing. From the dropdown list choose the Radio List Editor (or whatever name you gave your property editor in package.manifest) and hit save. Now the property editor is available to your document types as per usual. Add it to your document type, create content node of that type and see the magic.

tooltip on hover - screenshot

This is a very simple example of how Umbraco 7 gives much greater control over backoffice interface to developers, and how this control can improve editors’ experience. There is much to be gained from this control and I am looking forward to exploring further.