Issues

How to Create Custom Editors for the Umbraco Grid

Ever since Umbraco released Umbraco 7.2, the grid has been a great alternative to the old richtext editor - or some of the other ways we used to build content pages. The grid comes with a few editors by default, which lets you create you create amazing pages.

But it's first when you start adding custom grid editors to your Umbraco installation that you fully start to see the power of the grid. Like with property editors, Umbraco provides a great and flexible way to implement your own grid editors - which is what this article will be about.

As you may need an editor as both a property editor and a grid editor, the article will also go through the steps for achieving this with minimal effort.

Creating my own grid editor? Doesn't that take a lot of time?

Possibly, yes. Creating and implementing your own grid editor is an effort you do to give your content editors something extra - so there is a good chance this might take some extra time. Therefore it's also important to have a look at the alternatives.

For instance, LeBlender is a package that lets you build new grid editors made up of properties and data types like you would do for a normal document type - all from the convenience of the backoffice.

Describing it very shortly, LeBlender is package for getting something out the door really quickly, as you can do a lot over a short time, whereas for a custom grid editor implemented on your own, you can work on the details of the editor, hopefully making it more user friendly.

Packages like Nested Content (which now comes with Umbraco by default), Stacked Content etc. achieves somewhat the same, but are more alternatives to the Umbraco Grid.

So, where do I start?

Grid editors are very similar to property editors. Both are defined in a package.manifest file, where you describe the grid editor with a JSON object. Here, like the root object of each package.manifest file may have a propertyEditors property, there is also a corresponding gridEditors property.

The JSON object for a grid editor is a little different than the one for a property editor. In it's simplest form you should specify the name, alias, view and icon properties:

  • The name property indicates the visual name of the editor, which editors will see when they are to insert a new grid control.
  • The alias property is the identifier of your grid editor, so Umbraco can recognize your grid editor. Therefore this should be unique to your grid editor, and it should not be changed over time.
  • view is the URL to the HTML view of your editor.
  • Like with name, icon is shown to the editor when inserting new grid controls.

As an example, the JSON object for our LinkPicker grid control looks something like below in it's simplest form:

{
    "name": "Related links",
    "alias": "skybrud.linkPicker.related",
    "view": "/App_Plugins/Skybrud.LinkPicker/Views/LinkPickerGridEditor.html",
    "icon": "icon-link"
}

Where the configuration of a property editor is managed through the pre-values of the underlying data type, grid editors are configured directly in the package.manifest files instead.

To configure the link picker as shown above, the same JSON object could instead look like:

{
    "name": "Related links",
    "alias": "skybrud.linkPicker.related",
    "view": "/App_Plugins/Skybrud.LinkPicker/Views/LinkPickerGridEditor.html",
    "icon": "icon-link",
    "config": {
        "title": {
            "show": true,
            "placeholder": "Related links"
        },
        "limit": 0,
        "types": {
            "url": true,
            "content": true,
            "media": true
        },
        "showTable": true,
        "columns": {
            "type": true,
            "id": true,
            "name": true,
            "url": true,
            "target": true
        }
    }
}

If you're creating your own grid editor, you'll be able to access the configuration through control.config in your Angular view and $scope.control.config in your Angular controller respectively.

For more information about configuring grid editors, you can have a look at the official Umbraco documentation on Grid Editors.

Implementing a custom grid editor

For the sake of this article, let's assume we're implementing a grid editor so that backoffice user's can insert a small image box, which contains an image (selected from the media archive), as well as a title and small description about the image.

The configuration

Like described in the section above, we first need to add the configuration for the grid editor. We could save this in a JSON file located at /App_Plugins/SkriftImagePicker/package.manifest, and then with the contents like:

{
    "gridEditors": [
        {
            "name": "Small Image box",
            "alias": "SmallImageBox",
            "view": "/App_Plugins/SkriftImagePicker/Views/GridEditor.html",
            "icon": "icon-pictures-alt-2",
            "config": {
                "title": {
                    "mode": "required",
                    "placeholder": "Enter an image title"
                },
                "description": {
                    "mode": "optional",
                    "placeholder": "Optional: Enter an image description"
                }
            }
        }
    ]
}

We use the configuration to tell the grid editor the desired placeholder text of the title field, and that the user must enter a value (as the field is required). In a similar way, we tell the grid editor the placeholder text of the description field, but this time that the field is optional.

The JSON for config is just an example, as you can create your own structure for another grid editor. It's also worth noticing, that you can add multiple grid editors using the same view, but with another alias - for instance if we needed another image box, where the description is mandatory. The package.manifest file could then look like this instead:

{
  "gridEditors": [
    {
      "name": "Small image box",
      "alias": "SmallImageBox",
      "view": "/App_Plugins/SkriftImagePicker/Views/GridEditor.html",
      "icon": "icon-pictures-alt-2",
      "config": {
        "title": {
          "mode": "required",
          "placeholder": "Enter an image title"
        },
        "description": {
          "mode": "optional",
          "placeholder": "Optional: Enter a small image description",
          "rows": 2
        }
      }
    },
    {
      "name": "Large image box",
      "alias": "LargeImageBox",
      "view": "/App_Plugins/SkriftImagePicker/Views/GridEditor.html",
      "icon": "icon-pictures-alt-2",
      "config": {
        "title": {
          "mode": "required",
          "placeholder": "Enter an image title"
        },
        "description": {
          "mode": "required",
          "placeholder": "Enter an image description",
          "rows": 5
        }
      }
    }
  ]
}

When adding two grid editors like that, using the same view, but with different configuration, we can re-use the logic of the view (and underlying controller) to provider the backoffice user with multiple options - in this example both a small and a large image box, which then could be rendered differently when displayed on the website.

The controller

To handle the logic of the grid editor, I've implemented a new Angular controller with the name Skrift.ImagePickerGridEditor.Controller. The controller is responsible for initializing the grid editor and makes sure that the right thing happens when you click to add an image etc.

To describe the controller a bit further, we can register the controller with Angular in the umbraco module (just assume this should always be umbraco). For the controller shown below, the first argument is the alias of our controller, and the second parameter is the function called each time an instance of our controller is initialized.

The parameters in the function is the resources that we need for the logic of the controller - here the $scope of the controller, as well as Umbraco's mediaHelper and entityResource (I'll come back to these later).

angular.module("umbraco").controller("Skrift.ImagePickerGridEditor.Controller", function ($scope, mediaHelper, entityResource) {

});

Right now the controller it self is quite empty. We can then add our logic inside this function - eg. a function for initializing the configuration:

function initConfig() {

    // Make a "shortcut" for the grid editor configuration
    var cfg = $scope.control.editor.config;

    if (!cfg) cfg = {};

    if (!cfg.title) cfg.title = {};
    if (!cfg.title.mode) cfg.title.mode = 'optional';
    if (!cfg.title.placeholder) cfg.title.placeholder = '';

    if (!cfg.description) cfg.description = {};
    if (!cfg.description.mode) cfg.description.mode = 'optional';
    if (!cfg.description.placeholder) cfg.description.placeholder = '';
    if (!cfg.description.rows) cfg.description.rows = 3;

    cfg.title.required = cfg.title.mode == 'required';
    cfg.description.required = cfg.description.mode == 'required';

    $scope.cfg = cfg;

}

Since we're working on a controller for a grid editor, we can access the configuration through $scope.control.editor.config (as a shorthand, I'm also storing the configuration in $scope.cfg).

The initConfig function here is responsible for filling out the missing configuration, as you don't have to fill our all the configuration properties in the package.manifest file. This is something that is specific to the grid editor we're building, so it might not be necessary for other grid editors.

In a similar way, we also need to initialize the value. The value of the grid control can be accessed through $scope.control.value. To keep things simple, our example grid editor just stores the ID of the selected image (in $scope.control.value.imageId), instead of meta data of the image. When rendering the grid control on the website, we can then take the ID and lookup the image in the media cache so we have a fresh reference to the meta data (name URL etc.).

However as we just store the image ID in the JSON value, our controller has to fetch the meta data of the image so we can show it to the user in the backoffice. For this we can use Umbraco's entityResource, where we request data about an entity with the stored ID and of the type media, and we then get the meta data back:

function initValue() {

    if (!$scope.control.value.imageId) return;

    // Use the entityResource to look up data about the media (as we only store the ID in our control value)
    entityResource.getById($scope.control.value.imageId, 'media').then(function (data) {
        setImage(data);
    });

}

The getById function will return an Angular promise, which we then catch by passing a callback function to then. The callback function then calls setImage, which is responsible for properly updating our value ($scope.control.value.imageId) and the model for the view ($scope.image and $scope.imageUrl in particular):

function setImage(image) {

    // Make sure we have an object as value
    if (!$scope.control.value) $scope.control.value = {};

    // Reset the image properties if no image id specified
    if (!image) {
        $scope.control.value.imageId = 0;
        $scope.image = null;
        $scope.imageUrl = null;
        return;
    }

    // Set the image ID in the control value
    $scope.control.value.imageId = image.id;

    // Update the UI
    $scope.image = image;
    $scope.imageUrl = (image.image ? image.image : mediaHelper.resolveFileFromEntity(image)) + '?width=' + 500 + '&mode=crop';

}

So far, we have only declared functions that are available inside the controller. None of these are available in the Angular view - for this we need to declare functions on $scope - eg. the function that is called when the user clicks add an image:

$scope.selectImage = function () {
    $scope.mediaPickerOverlay = {
        view: 'mediapicker',
        title: 'Select image',
        multiPicker: false,
        onlyImages: true,
        disableFolderSelect: true,
        show: true,
        submit: function (model) {

            // Get the first image (there really only should be one)
            var data = model.selectedImages[0];
                
            setImage(data);

            $scope.mediaPickerOverlay.show = false;
            $scope.mediaPickerOverlay = null;

        }
    };
};

An the corresponding function for removing (resetting) the selected image:

$scope.removeImage = function() {
    setImage(null);
};

Both $scope.selectImage and $scope.removeImage use the internal setImage function for updating the model - just like we did before from initValue.

Also notice that when $scope.selectImage is triggered, we initialize a new object for $scope.mediaPickerOverlay. This is the configuration for Umbraco's build-in media picker. The submit property in this object is the callback function triggered when the user selects an image from the media archive.

You can see the Umbraco documentation for more information about the media picker overlay. Also, above is just the pieces that makes up the controller - all together the file looks something like:

angular.module("umbraco").controller("Skrift.ImagePickerGridEditor.Controller", function ($scope, mediaHelper, entityResource) {

    function initConfig() {

        // Make a "shortcut" for the grid editor configuration
        var cfg = $scope.control.editor.config;

        if (!cfg) cfg = {};

        if (!cfg.title) cfg.title = {};
        if (!cfg.title.mode) cfg.title.mode = 'optional';
        if (!cfg.title.placeholder) cfg.title.placeholder = '';

        if (!cfg.description) cfg.description = {};
        if (!cfg.description.mode) cfg.description.mode = 'optional';
        if (!cfg.description.placeholder) cfg.description.placeholder = '';
        if (!cfg.description.rows) cfg.description.rows = 3;

        cfg.title.required = cfg.title.mode == 'required';
        cfg.description.required = cfg.description.mode == 'required';

        $scope.cfg = cfg;

    }

    function initValue() {

        if (!$scope.control.value.imageId) return;

        // Use the entityResource to look up data about the media (as we only store the ID in our control value)
        entityResource.getById($scope.control.value.imageId, 'media').then(function (data) {
            setImage(data);
        });

    }

    function setImage(image) {

        // Make sure we have an object as value
        if (!$scope.control.value) $scope.control.value = {};

        // Reset the image properties if no image id specified
        if (!image) {
            $scope.control.value.imageId = 0;
            $scope.image = null;
            $scope.imageUrl = null;
            return;
        }

        // Set the image ID in the control value
        $scope.control.value.imageId = image.id;

        // Update the UI
        $scope.image = image;
        $scope.imageUrl = (image.image ? image.image : mediaHelper.resolveFileFromEntity(image)) + '?width=' + 500 + '&mode=crop';

    }
    
    $scope.selectImage = function () {
        $scope.mediaPickerOverlay = {
            view: 'mediapicker',
            title: 'Select image',
            multiPicker: false,
            onlyImages: true,
            disableFolderSelect: true,
            show: true,
            submit: function (model) {

                // Get the first image (there really only should be one)
                var data = model.selectedImages[0];
                
                setImage(data);

                $scope.mediaPickerOverlay.show = false;
                $scope.mediaPickerOverlay = null;

            }
        };
    };

    $scope.removeImage = function() {
        setImage(null);
    };

    initConfig();

    initValue();

});

The view

The main idea about model-view based frameworks like Angular is that we put the logic inside the controller, so the view is much simpler. Therefore the view looks something like:

<div class="SkriftImagePicker" ng-controller="Skrift.ImagePickerGridEditor.Controller">
    
    <div>
        <div class="option">
            <span>Image</span>
            <div class="image" ng-if="image">
                <img ng-src="{{imageUrl}}" alt="" /><br />
                <a href="#" class="remove" ng-click="removeImage()" prevent-default><i class="icon icon-delete"></i></a>
            </div>
            <div class="image" ng-if="!image">
                <a href="#" class="usky-editor-placeholder" ng-click="selectImage()" prevent-default>
                    <div style="padding-top: 40px; text-align: center;">
                        <i class="icon icon-picture" style="font-size: 22px;"></i><br />
                        Select image
                    </div>
                </a>
            </div>
        </div>

        <label class="option">
            <span>Title</span><br />
            <input type="text" ng-model="control.value.title" placeholder="{{cfg.title.placeholder}}" ng-required="cfg.title.required"/>
        </label>
    
        <label class="option">
            <span>Description</span><br />
            <textarea type="text" ng-model="control.value.description" placeholder="{{cfg.description.placeholder}}" rows="{{cfg.description.rows}}" ng-required="cfg.description.required"></textarea>
        </label>
    </div>
    
    <umb-overlay ng-if="mediaPickerOverlay.show" model="mediaPickerOverlay" position="right" view="mediaPickerOverlay.view"></umb-overlay>

</div>

The ng-controller attribute indicates the alias of our Angular controller. At the bottom, the umb-overlay directive is used to show the the media picker overlay - but only when we need to.

Creating a directive

So far we have implemented a grid editor, where the underlying Angular controller and view is made specifically for this grid editor. So what if we to use this logic somewhere else? Well - we can't easily reuse the controller and the logic anywhere else. At least not without rewriting it.

But we can create an Angular directive instead. Directives - eg. like the umb-overlay we used before - are components that be re-used throughout your Angular application, or in this case, the Umbraco backoffice. This means that we can create our own directive, which we then can use in both a grid editor and a property editor - or for instance as part of a custom backoffice section.

A directive is declared somewhat the same way as a controller, but there are some differences. Rather than a controller function, we now have a directive function instead. You still declare an alias as the first argument, and pass a function as the second argument:

angular.module('umbraco').directive('skriftImagepicker', function (entityResource, mediaHelper) {
    return {
        scope: {
            value: '=',
            config: '='
        },
        transclude: true,
        restrict: 'E',
        replace: true,
        templateUrl: '/App_Plugins/SkriftImagePicker2/Views/ImagePickerDirective.html',
        link: function (scope) {
            
            // all our controller logic

        }
    };
});

However this time the function return an object with details about the directive - let's walk through them one by one:

  1. scope declares which attributes should be accessible in the scope of the directive's controller - for instance here the attributes value and config will be accessible in the controller. = indicates that the value is passed by reference (eg. an object), whereas @ indicates the raw value of the attribute (eg. a string value).
  2. transclude
  3. restrict let's you declare how the directive can be used. E stands for element, meaning we can use <skrift-imagepicker />, but not <div skrift-imagepicker></div> (which would have been A for attribute). We could also set it to EA to allow it to be used as either an element or an attribute.
  4. replace declares whether the HTML we insert for the directive should be replaced with the contents of templateUrl. If false, the HTML will be inserted in the element of the directive instead.
  5. templateUrl is the URL to the HTML view of the directive. template with the actual HTML could be used as an alternative.
  6. link is the function used as controller for the directive.

The entire JavaScript file for the directive along with the view can be found here and here. The GridEditor.html file from earlier can be replaced with something as little as <skrift-imagepicker value="control.value" config="control.editor.config" />, and the underlying controller for the grid editor is therefore no longer necessary.

It's also worth mentioning that the alias of the directive should use camel casing, but when you insert the directive in your views, you should use kebab-case instead. Eg. skriftImagepicker becomes skrift-imagepicker.

Reusing the directive for a property editor

Now, since we already have implemented the directive, we can somewhat easily reuse our logic for creating a property as well. We can do this by adding the property editor definition to the propertyEditors property in our package.manifest file:

{
  "propertyEditors": [
    {
      "alias": "Skift.ImagePicker",
      "name": "Skrift Image Picker",
      "editor": {
        "view": "/App_Plugins/SkriftImagePicker2/Views/PropertyEditor.html",
        "hideLabel": false,
        "valueType": "JSON"
      }
    }
  ]
}

For simplicity, I have omitted the gridEditors, javascript and css properties. You can see the full file here. The view can then be as simple as:

<skrift-imagepicker value="model.value" config="model.config" />

Notice that since this is the view for a property editor, we must now use model.value and model.config instead of control.value and control.editor.config. Also, as we haven't defined any prevalues for the property editor yet, model.config will be empty.

Wrapping things up

That should be it. We now have a directive, a grid editor and a property editor. To help understanding this article, I've created an Umbraco demo solution, which is available in this GitHub repository. Here you can find the individual files that makes of our custom package.

In the solution, the files located in /App_Plugins/SkriftImagePicker/ represents the package from when is was just a grid editor, while the files located in /App_Plugins/SkriftImagePicker2/ is everything put together.

The repository also contains a Visual Studio solution file, so you can open it Visual Studio, hit build, and then play around the grid and property editor in Umbraco. The login for the Umbraco backoffice is skrift@skrift.io for the username and skrift12345678 for the password.

If you wish to play a bit more with the code, the Umbraco documentation can also be a great help:

Anders Bjerner

Anders Bjerner is an Umbraco MVP and System Developer at Limbo (formerly Skybrud.dk), with offices in Vejle and Copenhagen, Denmark. He has a background in Computer Science, and has been working with Umbraco since 2011. His work at Limbo typically consists of implementing Umbraco solutions for various government and private clients as well as developing custom packages (where a number of them can be found on the Umbraco Marketplace and NuGet). When not working and playing around with Umbraco, he can be found on his bike exploring the hills around his hometown (some of the steepest/highest in an otherwise flat Denmark).

comments powered by Disqus