Angular.js binding to jQuery UI (Datepicker example)

By Christian Grobmeier

Recently I found out it is not straight forward to use jQuery UI together with Angular.js. Reason: if you are using the usual intialization like $('.datepicker').datepicker(); you will not access the scope of your controller and thus not change the values of your model. You would need to wrap the jQuery UI Datepicker into an Angular.js directive to make this work.

PLEASE NOTE: There is a project called “Angular UI” at Github [1]. It helps you with the same problem. I for my case didn’t want to include another project and decided to write it myself. You might think different and want to look at it – it is recommended you do so before you implement your own stuff.

The Directive

This is the directive which wraps the datepicker.

var directives = angular.module('directives', []);

directives.directive('datepicker', function() {
   return function(scope, element, attrs) {
       element.datepicker({
           inline: true,
           dateFormat: 'dd.mm.yy',
           onSelect: function(dateText) {
               var modelPath = $(this).attr('ng-model');
               putObject(modelPath, scope, dateText);
               scope.$apply();
           }
       });
   }
});

What happens? We create a new directive with the name “datepicker”. You can use the directive like this:

<input type="text" datepicker ng-model="myobj.myvalue" />

In our factory function we get three parameters. Important are the scope, which is the current scope of the enclosing controller, and the element, which is the annotated HTML element. As “element” is a jQuery object, we can easily call the datepicker method which initializes the jQuery UI datepicker.

The interesting part happens in the onSelect method. It is a datepicker callback which is executed when the user selects a date. The argument is entering the method as text. Our goal is to apply the changed value to the controller scope.

First we need to get the location of the value. With $(this) we get hold of the jQuery object of our input field. With the standard jQuery method “attrs” we return the path to our model as string: it is the expression in the ng-model attribute. Use that model path as argument to the putObject method which I will show later. This method will take an object as argument and puts the necessary value to it. The location is given as string.

Finally we need to apply the changes to our scope to let Angular.js do the work with updating etc.

The putObject with path function

In the section above I used a tiny function which is really helping me on a daily basis. I call it putObject and usually it runs as a jQuery plugin. Here is a plain and more minimalistic version of it:

function putObject(path, object, value) {
    var modelPath = path.split(".");

    function fill(object, elements, depth, value) {
        var hasNext = ((depth + 1) < elements.length);
        if(depth < elements.length && hasNext) {
            if(!object.hasOwnProperty(modelPath[depth])) {
                object[modelPath[depth]] = {};
            }
            fill(object[modelPath[depth]], elements, ++depth, value);
        } else {
            object[modelPath[depth]] = value;
        }
    }
    fill(object, modelPath, 0, value);
}

Let’s imagine you have an empty object and want to fill a specific value in it. For example, your object is {} and you want to create something like: { “test”: { “value”: “OK” } }. Then you can use the path test.value to address this position. Like:

var obj = {};
putObject("test.value", obj, "OK");
console.log(obj);

On the console you should now see the expected object.

The function is walking down the whole object graph and creates new objects if necessary or puts the value into the desired property. The idea is basically taken from OGNL [2] which does the same but with more power for Java. I will leave out the detailled explain of this and close this short blog post with: it has something to do with recursion ;-)

Even more elegance with $parse

I am very glad Sergiu Neamt wrote an even better solution as a comment to this blog post (see below). He proposed something like can be shown at JSFiddle: using $parse [3].

$parse does basically the same as I have done with putObject, just much more elegant and more in the Angular.js way. Sergius code looks like this:

myApp.directive('myDatepicker', function ($parse) {
    return function (scope, element, attrs, controller) {
        var ngModel = $parse(attrs.ngModel);
        $(function(){
            element.datepicker({
               ...
               onSelect:function (dateText, inst) {
                    scope.$apply(function(scope){
                        // Change binded variable
                        ngModel.assign(scope, dateText);
                    });
               }
            });
        });
    }
});

function MyCtrl($scope) {
    $scope.userInfo = {
        person: {
            mDate: '1967-10-07'
        }
    };   
}    

The $parse function converts an expression (like the path in the ng-model) into a function. Our returned object will have an “assign” property, created by Angular.js, which acts as a kind of setter. In case our expression is assignable, you can use this property to set a value.

In my opinion, Sergius way is to prefer.

References


Follow me on Twitter :-)

  • Sergiu Neamt

    the directive is not working when you use the datepicker in ng-repeat statement. It’s working only for the first row.

  • Sergiu Neamt

    Hello,

    Your directive was the first I used in a application, I tried the datepicker from angular-ui but there is a nasty behaviour in IE and I get back your directive. But I wasn’t very happy with your putObject() function, because isn’t feet in angular pattern. After spend some hours googling after a better solution I found changed you directive as bellow:

    myApp.directive(‘myDatepicker’, function ($parse) {
    return function (scope, element, attrs, controller) {
    var ngModel = $parse(attrs.ngModel);

    element.datepicker({
    showOn:”both”,
    buttonImage: “images/calendar.gif”,
    buttonImageOnly:true,
    changeYear:true,
    changeMonth:true,
    dateFormat:’yy-mm-dd’,
    maxDate: new Date(),
    yearRange: ’1920:2012′,
    onSelect:function (dateText, inst) {
    scope.$apply(function(scope){
    // Change binded variable
    ngModel.assign(scope, dateText);
    });
    }
    });
    }
    });

    You should see the $parse obkect and how can be used to assign from dom value to model.

    Regards
    Sergiu Neamt

  • Sergiu Neamt

    here is the jsfiddle: http://jsfiddle.net/nnsese/xB6c2/26/

    I had to add jquery $(function()) because sometimes the jqueryui datepicker wasn’t available.

  • http://www.responsivedesignstudio.com Neha Khanna

    Hi,

    Have you tried it with more complex components like jqGrid? How does it mange to update the model with jqGrid component?

  • http://www.grobmeier.de Christian Grobmeier

    Have not tried jqGrid yet. When you pass model data from jqGrid to Angular, you most likely need to call $apply.

  • http://vstarkov.com Vladimir Starkov

    Thank you! this article is amazingly helpfull for me (as angular-newbee)

  • Edgar Amaro

    hey what css did you use for the date picker?

  • ganaraj p r

    Actually you dont need a $parse to do this. You need to get hold of the ngModelController and then set the value through that. That is pretty easy to achieve.

    Have a look at the fiddle here :

    http://jsfiddle.net/xB6c2/121/

  • Steffen Sommer

    If you want to track the changes of the selected datepicker value, it can be done by:

    $scope.$watch(‘userInfo’, function(userInfo) {

    });

    I needed that, because ng-change didnt trigger, because jQuery-ui-datepick is a select :)