Yesterday I was consulting about Angular and one of the things that popped up was to wrap a third party library inside an Angular directive.
If you are new to Angular or to Angular directives there are tons of posts about the subject in the internet and I suggest to start with Dan Wahlin’s directives post series.
In this post I‘m going to show you how to start creating a library wrapper using a directive and a previous directives knowledge is a must.
So, let’s get started.
The Tale of Endless JavaScript Libraries
There are gazillion JavaScript libraries out there. Every day somebody writes yet another JavaScript library and this isn’t going to stop. More than that, many libraries weren’t created with Angular in mind and are not Angular oriented. That doesn’t mean that the libraries aren’t good or aren’t usable when you use Angular. That only means that if you want to use those libraries in an Angular oriented application, you will have to invest some time to make the integration. One of the options to make the integration is by using directives. This option is very suitable to wrap libraries that deal with creating UI elements. There are other options such as filters (for example the angular-moment filters), services (for example this simple underscore.js wrapper) or even by creating a mixen between angular components and some library. The power to choose what option fits to your wrapper is in your hands.
The Customer Use Case
One of my customers needed the help to integrate a timeline widget inside their Angular application. So first of all I helped them to find a relevant library to fit their needs – this was the CHAP Links Library timeline.
Note – I’m not going to explain the process of choosing a library (that fits into a post by itself and you can find some details about this subject in my “Pro Single Page Application Development” book).
Once the customer picked the library the next stage was to explore the library documentation and API and to think about how to integrate it into the application. Since the library is generating a timeline UI element that was easy – wrap it with a directive.
Creating the Directive
The directive code:
(function () {
'use strict';
angular.
module("timelineExample").
directive("timeline", timelineDirective);
timelineDirective.$inject = [];
function timelineDirective() {
var timeline;
function link(scope, element) {
initTimeline();
scope.$watchCollection('timelineDataSource', function() {
timeline.draw(scope.timelineDataSource);
});
function initTimeline() {
// create a new timeline with the div element of the template
timeline = new links.Timeline(element[0], scope.timelineOptions);
// attach an event listener using the links events handler (part of the timeline API)
links.events.addListener(timeline, 'rangechanged', handleRangeChanged);
}
function handleRangeChanged(properties) {
if (scope.timelineRangeChanged) {
scope.timelineRangeChanged()(properties); }
}
}
return {
restrict: 'E',
scope: {
timelineDataSource: '=',
timelineOptions: '=',
timelineRangeChanged: '&'
},
link: link,
template: '<div></div>'
};
}
}());
An example for a controller that sets up the directive followed by the HTML that use both the controller and the directive:
(function () {
'use strict';
angular.
module("timelineExample").
controller("mainController", mainController);
mainController.$inject = [];
function mainController() {
var vm = this;
function init() {
vm.dataSource = [
{
'start': new Date(2010,7,23),
'content': 'Conversation'
},
{
'start': new Date(2010,7,23,23,0,0),
'content': 'Mail from boss'
},
{
'start': new Date(2010,7,24,16,0,0),
'content': 'Report'
},
{
'start': new Date(2010,7,26),
'end': new Date(2010,8,2),
'content': 'Traject A'
},
{
'start': new Date(2010,7,28),
'content': 'Memo'
},
{
'start': new Date(2010,7,29),
'content': 'Phone call'
},
{
'start': new Date(2010,7,31),
'end': new Date(2010,8,3),
'content': 'Traject B'
},
{
'start': new Date(2010,8,4,12,0,0),
'content': 'Report'
}
];
vm.options = {
'width': '100%',
'height': '300px',
'editable': true,
'style': 'box'
};
vm.rangeChanged = onRangeChanged;
}
function onRangeChanged(properties) {
console.log('rangechanged ' + properties.start + ' - ' + properties.end);
}
init();
}
}());
<div class="main" ng-controller="mainController as vm">
<timeline timeline-data-source="vm.dataSource" timeline-options="vm.options" timeline-range-changed="vm.rangeChanged"></timeline>
</div>
Tearing Down The Directive Code
The initTimeline Function
The responsibility of the function is to initialize the timeline object. The timeline object constructor receives as parameters both an element to work on and an options object to configure the timeline. As you can see in the function implementation, I’m passing the element from the link function declaration as the timeline element. The element in the first index should be the DIV that is created from the directive’s template. I’m also passing the scope.timelineOptions which is bound to the directive timeline-options attribute. If the directive user won’t pass that object don’t worry, the timeline constructor function will set a default configuration to the timeline automatically. The second line sets one of the timeline API event handlers and enables me to show you how to wire API event handlers.
Take a close look at the handleRangeChanged callback and particularly to the double calling to the function:
scope.timelineRangeChanged()(properties);
This line ensures that when someone binds the timeline-range-changed attribute from the outside, we first fetch the outside function (the first call – scope.timelineRangeChanged()) and then perform the relevant action with the relevant properties data.
The scope.$watchCollection Watcher
The timeline data might change during runtime and we will probably want to react to such changes. Therefore, I wired a watcher to the data source collection to see if something changes there. The current implementation is to draw the whole timeline again if there is a change in the data source. The $watchCollection callback accepts two parameters, the new collection and the old collection, that can be used to check for the difference between the collections and later use the timeline addItem/addItems/deleteItem API functions (I didn’t use those parameters). It is up to you to decide how to use the API and therefore I urge you to look at the library documentation before you rush forward and implement your wrapper.
The Directive Definition Object
There are only two interesting definitions that I used in the wrapper:
- template – used to create a DIV that the timeline object will use to create the timeline UI.
- scope – used to create an isolated scope for the directive in order to isolate it from the other application scopes. Also, the created scope defines 3 scope variables that enables the binding of the data source, the options object and a function to react to the timeline changed event.
Summary
In this post I showed how I created a simple wrapper on top of a UI library. Probably there are other ways to implement this but this was the draft that I created to my customer. One last thing, again I urge you to open the API of the library you want to wrap before rushing into implementation. That will help you to understand how the developer who wrote the library expect you to use it. You can download a working example from here.