How to integrate your UI Widgets with AngularJS

When building apps we tend to create reusable code, which helps us in the long run, makes our code easier to understand and all in all cleaner. We like to build modular things.
This modularity that we’re used to shows itself not just in the units that we write but the in UI and elements that we build. Sometimes it makes sense to think ahead, and create our widgets for re-usability.

That is how the Mobiscroll UI Library is built. We created our AngularJS integration plugins for the components and we are going to show you how you can do it as well.

A basic understanding of AngularJS is needed. For more on how to get started with Angular head on over to Angular Tutorials.

Angular JS

We are going to go through the steps you need to take:

A short intro

Integrating your widgets

A short intro

What are directives and how to use them?

We can think of angular directives as custom html attributes or elements that define behavior for the element. For example if  we want an image on a page that changes depending on a certain condition we can just write:

<img ng-src="http://www.gravatar.com/avatar/{{user}}"/>

Whenever the user expression value changes, the image source will change too. The directive that is responsible for this behavior is the ngSrc. There is a set of built in directives that comes with Angular, like the ngRepeat, ngModel, ngView and so on. For more info and examples on the built in directives see the Angular documentation.

Why do we need them?

Custom directives let you define how the element should behave. Also the element can be the directive itself! Instead of creating the above img with ngSrc, you could create a directive called myuser and write it as follows:

<myuser />

Of course you will have to implement the actual behavior for the directive, but as you will see it’s not difficult at all.

Integrating your widgets

So you have your tool belt packed with your custom UI elements and need to plug them into your latest shiny app that you’re building. The question is: how to make them work the Angular way?

Lets take the Mobiscroll Calendar as an example:

Since the calendars logic is already implemented inside the mobiscroll control, we will focus on the following:

Two way - Photo by Phil Gilbert

Defining a directive

Defining a directive is relatively easy. Inside a module you just give it a name, set dependencies if any and return an object with some basic options (this is called the directive definition object). We will also use a service called $parse (more on that later).

Let’s call our directive calendar in the mobiscroll module:

1
2
3
4
angular.module('mobiscroll', [] /* no dependencies */)
   .directive('calendar', ['$parse', function ($parse) {
       return { /* the Directive Definition Object */ };
   }]);

For our calendar the directive definition object will contain the following:

1
2
3
4
5
6
7
8
9
{
   restrict: 'A',
   link: function (scope, iElement, iAttrs) {
      /* here comes the link function */ 
   },
   controller: ['$scope','$element', '$attrs', function($scope, $element, $attrs) {
      /* here comes the controller function */
   }]
}

The restrict: 'A' option will tell angular that our directive is an attribute.

Initializing the mobiscroll calendar

The link function will be called once for each element. This is where most of the custom logic comes regardless if we integrate a control into angular or create a new directive from scratch. For our calendar directive the initialization will take place in here. It is straight forward, since the link function gets the HTML element (iElement) as parameter. We only have to call the mobiscroll init.

Binding to a scope variable

Binding to a scope variable means two things:

  • when the calendar changes, the scope variable should be updated, and
  • when the scope variable changes the calendar should be updated

So one thing we also need to set up in the linking phase is when the element changes its value (you select a date on the calendar) we update the scope variable.

We do this by attaching a handler to the elements change event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
link: function (scope, iElement, iAttrs) {
     // initialize mobiscroll on the element
     iElement.mobiscroll().calendar();
     // add onChange handler
     iElement.on('change', function () {
          scope.$apply(function () {
               var getter = $parse(iAttrs['calendar']);
               var setter = getter.assign;
               // check for multiselect option
               var inst = iElement.mobiscroll('getInst');
               if (inst.settings.multiSelect || inst.settings.selectType == 'week') {
                    setter(scope, iElement.mobiscroll('getValues'));
               }
               else {
                    setter(scope, iElement.mobiscroll('getDate'));
               }
          });
     });
 },
...

Inside the link function the iAttrs parameter holds all the attributes the element has. The calendar attribute (iAttrs["calendar"]) will hold the scope variable the component should be bound to. The parse service will actually parse the expression and return a function. If the expression is assignable it will have an assign function. Here its name is setter, and calling it will set the variable.  The mobiscroll calendar has different methods to get the value based on the multiselect option. As you can see in the handler, we have to check that option first and get the value accordingly.

Another notable thing is that all the logic is wrapped around the $apply function. This is required for angular, to get notified about the changes made inside the function.

The next step is to notify the calendar if the variable changes. For this, we use the controller function and the scopes $watch function. This way we can register a listener to be executed when our variable changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$scope.$watch(
 function () {
    var getter = $parse($attrs['calendar']);
    return getter($scope);
 },
 function (newValue) {
    if (newValue) {
       var inst = $element.mobiscroll('getInst');
       if (inst.settings.multiSelect || inst.settings.selectType == 'week') {
          $element.mobiscroll('setValues', newValue);
          if (newValue.length) {
             $element.mobiscroll('setDate', newValue[0]);
          }
       }
       else {
          $element.mobiscroll('setDate', newValue);
       }
       $element.val(inst.val);
    }
 },
 true);

The first function returns the value that should be watched. The second function gets called when the value changes. Here we also have to distinguish between the multiselect and single select modes of the mobiscroll calendar. The last parameter is a boolean, which tells angular to compare objects for equality rather than for reference.

At this point we should have a working calendar directive already.

Passing properties

The mobiscroll calendar has many properties that can enhance user experience: display, theme, minDate, maxDate and so on…

Passing configuration options is easy. In the linking function we have access to all attributes of the element, so passing the initialization option as an attribute is a convenient way.

1
2
3
4
5
6
7
8
9
10
...
link: function (scope, iElement, iAttrs) {
   // prepare the initialization object for mobiscroll
   var initOptS = iAttrs['mobiscrollOptions'] || '{}';
   var initOpt = scope.$eval(initOptS);
   // initialize mobiscroll on the element
   iElement.mobiscroll().calendar(initOpt);
 
   iElement.on('change', function () {
...

Here we used the $eval function to create the mobiscroll initialization object. The $eval uses the parse service that we mentioned earlier and evaluates the expression on the current scope. This way we can pass the mobiscroll options in the mobiscroll-options attribute like this:

<input calendar="birthday" mobiscroll-options="{ theme: 'ios', display: 'inline' }" />

Handling events

Mobiscroll event handlers are functions that you pass. So where are these functions defined?
When we pass a setting like onSelect, Angular treats it just as any other properties. It evaluates it against the current scope. This means when we define a function in the current scope we can pass it as an event handler to mobiscroll.

However there is a small detail you should be aware of. When the mobiscroll event handlers are called, they are outside of the Angular world. If we change a scope variable inside a handler like this, Angular will not be notified about it and the change will not propagate. To overcome this the simplest solution is to put the event handler inside an $apply() function.

1
2
3
4
5
6
7
8
9
10
$scope.calendarSelected = function() {
   // your handlers logic here, for example:
   $scope.someVariable = "Date Selected!";
}
$scope.calendarSelectedHandler = function(){
   // wrapping the handler in the apply
   $scope.$apply(function(){
      $scope.calendarSelected();
   });
}

You can also read about handling events in the mobiscroll documentation.

Usage Examples

After writing our Angular directive, here come a couple of examples on how we could use it.

Using the calendar in inline mode and binding it to the “mybirthday” variable.

<input calendar="mybirthday" mobiscroll-options="{ display: 'inline' }"/>

To select multiple dates we should just pass the multiselect option as true:

<input calendar="myholidays" mobiscroll-options=" { multiSelect: true } "/>

Adding event handlers from the scope:

<input calendar="myholidays" mobiscroll-options="{ multiSelect: true, onDayChange: dayChanged}" />

Inside your controller:

1
2
3
4
5
6
7
8
9
10
...
$scope.dayChanged = function(){
   $scope.wasDayChanged = true;
};
$scope.dayChangedHandler = function(){
   $scope.$apply(function(){
      $scope.dayChanged();
   });
};
...

For more info on our AngularJS integration plugins please see our documentation.
This is the main idea behind the Mobiscroll AngularJS Integration. Let us know how and what do you plan to integrate next?


Did we leave something out, do you have anything to add, comment? Please let us know in the comment section below!

Start building better products
Sign up and get posts like this in your inbox. We respect your time and will send you only the good stuff!

Tags: , , , , , , ,