Angular.js binding to jQuery UI (Datepicker example)

von

Recently I found out it is not straight forward to use jQuery UI together with Angular.js. Reason: if you are using the usual initialization 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 &amp;&amp; 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 detailed 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

Tags: #AngularJS #JavaScript #jQuery #jQuery UI

Newsletter

ABMELDEN