Create an Umbraco Multiple Date Property Editor Using the jQuery UI Datepicker

Posted by Phil on February 15, 2014

In a new site I'm building in my spare time lately, a need arose to have the year's events scheduled out in an easily-edited back office page. The requirement was a property type that allowed the user to add multiple dates - each accompanied by a comment field - for a document node so that they didn't have to do anything crazy like one node per event or other similar hacks.

There are actually quite a few tutorials out there already on how to actually create your own custom property editor in Umbraco 7, but most of these examine more basic types where the field values map 1 <=> 1. My particular problem required many dates for one field. It was not a straight-forward solution and I'm as-yet unconvinced that I've tackled the problem in the most efficient or "correct" manner. But it works nicely and I can't find any other examples out there, so here goes:

Scaffolding

The property editor we're going to make, the multidate picker, requires three key parts as is the case with any Umbraco custom editor:

  • Package Manifest
  • HTML
  • Controller

In any custom property editor, the Package Manifest is like the contents page for Umbraco to see and reference the constituent parts. This includes html pages, CSS files, JavaScript files and any other dependencies required to make the plugin work. For the purposes of this demo, I'm only using the HTML page (the view) and the JavaScript file (the controller).

Getting Started: Folders and Manifests

In order to make the property editor plug in to Umbraco's back office, we need to keep everything grouped together. This is done under the solution's App_Plugins folder. We'll create a new folder below that called "Multidate" and all our files will be housed within that.

To ensure Umbraco knows what it should specifically look for and include, we add the Package Manifest, which is just a simple XML file that details the included files and locations. Add a new XML file called "package.manifest" and save it to our folder. The contents of the file look like this:

{   
    //you can define multiple editors   
    propertyEditors: [      
        {
            /*this must be a unique alias*/ 
            alias: "MySite.Multidate",
            /*the name*/
            name: "Multidate Picker",
            /*the html file we will load for the editor*/
            editor: {
                view: "~/App_Plugins/Multidate/multidate.html"
            }
        }
    ]
    ,
    //array of files we want to inject into the application on app_start
    javascript: [
        '~/App_Plugins/Multidate/multidate.controller.js'
    ]
}

It looks a lot like a JSON object - and it is. The file is picked up and contains (in this example) two parts: propertyEditors, which define the presentation aspects that will surface for the user in the back office (e.g. what name will be displayed in the data type lists) and javascript, which lists all the included script files as an array.

The propertyEditors section also contains the value for the actual editor itself. This is just a regular HTML page that uses AngularJS to tie in the functionality. That functionality is contained in the multidate.controller.js file. We'll look at the view first.

Rendering the UI: The View

As mentioned in the previous section, our property editor UI is defined by an HTML file. The file contains the form control markup aided by AngularJS model binding that Umbraco interacts with based on whatever logic is defined in our controller script. What's the controller script? You can see it defined in the source below on the ng-controller attribute, which has the value, "MySite.MultidateController".

Umbraco will execute the code it finds in our previously-defined scripts where an Angular module has a controller definition that matches the name specified in the ng-controller attribute. The actual controls we have are pretty straight-forward: text inputs for the date and associated comment fields, a link to add further input field pairs and one more link to remove existing value pairs. Altogether the view code looks like this:

<div class="umb-editor umb-multiple-datepicker" ng-controller="MySite.MultidateController" ng-app="umbraco">
    <div class="control-group" ng-repeat="item in model.value track by $index">
        <input type="text" jqdatepicker name="item_{{$index}}" ng-model="item.date.value" class="dp" id="dp-{{model.alias}}-{{$index}}" />
        <input type="text" name="comment_{{$index}}" ng-model="item.comment.value" placeholder="comments..." />
        <a prevent-default href="" title="Remove this date"
            ng-show="model.value.length > 0"
            ng-click="remove($index)">
            <i class="icon icon-remove"></i>
        </a>
    </div>
    <div class="control-group">
        <p><a prevent-default href="" title="Add another date"
            ng-click="add()">Add another date 
            <i class="icon icon-add"></i>
        </a></p>
    </div>
</div>

Business Logic: Defining the Controller

The controller is where the bulk of the grunt work is done. This is where the behaviours for the view are defined and the data persistence is managed; again via AngularJS and its hooks into the Umbraco engine.

It's at this point where I need to call attention to my complete lack of familiarity with Angular and its interaction with Umbraco. Ordinarily you would define a directive in Angular which are loosely akin to event bindings; they attach behaviours to DOM elements. The tricky part I found was the order in which these events were executed. We needed the jQuery UI library to load before the view is rendered and then call the $.datepicker() function on the target input elements. I'm not convinced the below is the best way to achieve this, but it definitely does work (leave your improvements in the comments, please. It would be much appreciated).

So in order to bring up the jQuery UI datepicker control, we load the script as an asset and then attach a promise to fire once it has been loaded. That defines the behaviour we want for storing the selected date against the model for the given control. The datepicker function is then executed against any fields with the ".dp" class assigned. We want behaviours defined for adding and removing value pairs, so functions are assigned to the $scope object to handle these tasks.

Finally I've added a directive to watch for any newly-created value pairs (as triggered by the "add" function) and set the datepicker against those at the same time.

function MultipleDatePickerController($scope, assetsService) {

    //tell the assetsService to load the jQuery UI library from the plugin folder
    assetsService
        .load([
            "/App_Plugins/Multidate/jquery-ui.min.js"
        ])
        .then(function () {
            //this function will execute when all dependencies have loaded   
            $.datepicker.setDefaults({
                // When a date is selected from the picker
                onSelect: function (newValue) {
                    if (window.angular && angular.element)
                        // Update the angular model
                        angular.element(this).controller("ngModel").$setViewValue(newValue);
                }
            });
            $('.dp').datepicker({ dateFormat: 'dd/mm/yy' });
        });

    //load the separate css for the datepicker to avoid it blocking our js loading
    assetsService.loadCss("/css/jquery-ui.custom.min.css");

    if (!$scope.model.value) {
        $scope.model.value = [];
    }

    //add any fields that don't have values
    if ($scope.model.value.length > 0) {
        for (var i = 0; i < $scope.model.value.length; i++) {
            if ((i + 1) > $scope.model.value.length) {
                $scope.model.value.push({ value: "" });
            }
        }
    }

    $scope.add = function () {
        if ($scope.model.value.length <= 52) {
            console.info($scope.model.value.length);
            $scope.model.value.push({ value: "" });
        }
    };

    $scope.remove = function (index) {
        var remainder = [];
        for (var x = 0; x < $scope.model.value.length; x++) {
            if (x !== index) {
                remainder.push($scope.model.value[x]);
            }
        }
        $scope.model.value = remainder;
    };

}

var dp = angular.module("umbraco").controller("AcuIT.MultidateController", MultipleDatePickerController);

dp.directive('jqdatepicker', function () {

    return function (scope, element, attrs) {

        scope.$watch("jqdatepicker", function () {
            try {
                $.datepicker.setDefaults({
                    // When a date is selected from the picker
                    onSelect: function (newValue) {
                        if (window.angular && angular.element)
                            // Update the angular model
                            angular.element(this).controller("ngModel").$setViewValue(newValue);
                    }
                });

                $('.dp').datepicker({ dateFormat: 'dd/mm/yy' });
            }
            catch(e)
            {
                //console.log(e);
            }
        });
    };

});

Putting it to work

Having put all the pieces in place, the last remaining step is to actually ensure the data type is available in the Umbraco back office (in the Developer section) and apply it to a document type.

All going well, the result should look something like this:

Hope this helps. Leave a comment below!

Comments