Angular Directives Part III, wrapping external libs

Angular – directives part III wrapping external components

So you found this awesome library. Coolest graph ever or an input control that makes you cry with joy?

If you want it to interact with angular you need to wrap it.

Wrapping

wrap it you say, what does it mean?

It means that every time it throws an event and that event need to interact with your app you need to perform a $scope.apply basically…

What happens if I don’t ?

Well it won’t work so good…

Example

You have found your awesome component and it performs an action and throws either a known event or a custom event.

At this point you have to decide wether to handle it or not. Only YOU can decide if you want to handle it. I will show you two scenarios WHEN and WHEN NOT to handle it.

Scenario 1

customElement.on('customEvent',function(){
    // when this event is thrown it turns the element red 

})

We decide we don’t want to know that the element turned red so we DONT handle it

Scenario 2

customElement.on('valueChanged',function(newValue){
    // this is probably a value we want to set in a scope, so we set it 
    scope.$apply(function(){
        scope.prop = newValue;
    })
})

Clearly there is a new value here that we care about and by using this line

scope.$apply(function(){
        scope.prop = newValue;
    })

We ensure it happens in the angular loop.
It should be said that this is all happening in the context of a directive so the full code looks like:

module('app')
    .directive('someDirective', function(){
        return {
            link : function(scope,element,attributes){
                customElement.on('valueChanged',function(newValue){

                    scope.$apply(function(){
                        scope.prop = newValue;
                    });
                });
                }
            }

});

External lib

Chart.js http://www.chartjs.org

A really nice looking lib that lets you draw graphs

Install

bower install Chart.js --save  

Ensure you have done bower init before so you have a bower config file so it actually save Chart.js in the dependencies property.

Howto

So to make it work you need a canvas element like so

<canvas id="myChart" width="400" height="400"></canvas>

Next you need to get hold of the context of the canvas element like so

var ctx = document.getElementById("myChart").getContext("2d");

Last step is to create your chart with a call like this

var myNewChart = new Chart(ctx).PolarArea(data); // PolarArea is just an example chart type

OR

var myNewChart = new Chart(ctx).PolarArea(data,options);

But WAIT, what is data and options?

Data – is a json structure looking like this

var data = {
    labels: ["January", "February", "March", "April", "May", "June", "July"],
    datasets: [
        {
            label: "My First dataset",
            fillColor: "rgba(220,220,220,0.2)",
            strokeColor: "rgba(220,220,220,1)",
            pointColor: "rgba(220,220,220,1)",
            pointStrokeColor: "#fff",
            pointHighlightFill: "#fff",
            pointHighlightStroke: "rgba(220,220,220,1)",
            data: [65, 59, 80, 81, 56, 55, 40]
        },
        {
            label: "My Second dataset",
            fillColor: "rgba(151,187,205,0.2)",
            strokeColor: "rgba(151,187,205,1)",
            pointColor: "rgba(151,187,205,1)",
            pointStrokeColor: "#fff",
            pointHighlightFill: "#fff",
            pointHighlightStroke: "rgba(151,187,205,1)",
            data: [28, 48, 40, 19, 86, 27, 90]
        }
    ]
};

Options – is a config object. To see exactly what you can change I suggest you have a look at the documentation http://www.chartjs.org/docs/#bar-chart-introduction

Studying the component we see that it has a few methods of interest

update()

It rerenders the chart with updated values.

getBarsAtEvent(event)

A method that returns the bar elements that are in the clicked position

There are also addData and removeData but for now we settle with wrapping and using the two first methods

So lets put some graph code in our directive

html file

<div ng-app="app">
        <canvas id="myChart" width="400" height="400"></canvas>

        <chart-wrapper></chart-wrapper>
</div>

With the added

<canvas id="myChart" width="400" height="400"></canvas>

And our directive

<chart-wrapper></chart-wrapper>

And app.js

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

app.directive('chartWrapper',function(){
    return {
        restrict : 'E',
        replace : true,
        template : '<h1>hello</h1>',
        link : function(){

            var data = {
                labels: ["January", "February", "March", "April", "May", "June", "July"],
                datasets: [
                    {
                        label: "My First dataset",
                        fillColor: "rgba(220,220,220,0.2)",
                        strokeColor: "rgba(220,220,220,1)",
                        pointColor: "rgba(220,220,220,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(220,220,220,1)",
                        data: [65, 59, 80, 81, 56, 55, 40]
                    },
                    {
                        label: "My Second dataset",
                        fillColor: "rgba(151,187,205,0.2)",
                        strokeColor: "rgba(151,187,205,1)",
                        pointColor: "rgba(151,187,205,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(151,187,205,1)",
                        data: [28, 48, 40, 19, 86, 27, 90]
                    }
                ]
            };

            var ctx = document.getElementById("myChart").getContext("2d");
            var myBarChart = new Chart(ctx).Bar(data);
            myBarChart.update();

        }
};

});

At this point we got a fully functional barchart. But it kind of sucks right? We are not able to change anything, cause the data is static, or interact with it.

So let’s first move the data somewhere else – Into a controller.

app.js – controller part

app.controller('appController', function($scope){
    $scope.data = {
                labels: ["January", "February", "March", "April", "May", "June", "July"],
                datasets: [
                    {
                        label: "My First dataset",
                        fillColor: "rgba(220,220,220,0.2)",
                        strokeColor: "rgba(220,220,220,1)",
                        pointColor: "rgba(220,220,220,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(220,220,220,1)",
                        data: [65, 59, 80, 81, 56, 55, 40]
                    },
                    {
                        label: "My Second dataset",
                        fillColor: "rgba(151,187,205,0.2)",
                        strokeColor: "rgba(151,187,205,1)",
                        pointColor: "rgba(151,187,205,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(151,187,205,1)",
                        data: [28, 48, 40, 19, 86, 27, 90]
                    }
                ]
    };
});

app.js – directive

app.directive('chartWrapper',function(){
    return {
        restrict : 'E',
        replace : true,
        scope : {
            data : '='
        },
        link : function(scope, element , attr ){
            var ctx = document.getElementById("myChart").getContext("2d");
            var myBarChart = new Chart(ctx).Bar(scope.data);
            myBarChart.update();

        }
    };
});

That’s a lot smaller right?

So, interaction. Someone clicks a bar and I want to know about it..

First off we imagine what we want to do..

  • We want to click a datapoint and to know what data point
  • We want to call a callback and get notified somehow what we clicked

Let’s start

We read the documentation and find that we need to

  • add a click event to canvas element
  • call getPointsAtEvent(event) to get what was clicked

So the directive now looks like

app.directive('chartWrapper',function(){
    return {
        restrict : 'E',
        replace : true,
        scope : {
            data : '=',
            clicked : '&lineClicked'
        },
        link : function(scope, element , attr ){
            var canvas = document.getElementById("myChart"); 
            var ctx = canvas.getContext("2d");
            var myLineChart = new Chart(ctx).Line(scope.data);
            myLineChart.update();

            canvas.onclick = function(evt){

                var activePoints = myLineChart.getPointsAtEvent(evt); 
                if (activePoints.length > 0){
                    scope.clicked({ value : activePoints[0].value });
                }   


            };

        }
    };
}); 

With the following onclick added

canvas.onclick = function(evt){
    var activePoints = myLineChart.getPointsAtEvent(evt); 
    if (activePoints.length > 0){
        scope.clicked({ value : activePoints[0].value });
    }   
};

And also

scope : {
        data : '=',
        clicked : '&lineClicked'
    }

Also we add the following in index.html

 <chart-wrapper data="data" line-clicked="clicked(value)" ></chart-wrapper>

We also add a scope function to the controller

$scope.clicked = function(value){
    console.log('you clicked ' + value);
    $scope.values.push(value);
};

$scope.values = [];

And finally we also added a repeater to index.html to showcase the values array

<ul>
    <li ng-repeat="value in values">
    {{value}}
    </li>
</ul>

So that’s it right, you see it type your value in the console. WRONG our poor repeater is not updated.. Why oh why…

Well simple answer you called the callback without being in angulars world…
Remember this code?

canvas.onclick = function(evt){
    var activePoints = myLineChart.getPointsAtEvent(evt); 
    if (activePoints.length > 0){
        scope.clicked({ value : activePoints[0].value });
    }   
};

You didn’t call scope.$apply..

So let’s fix that

canvas.onclick = function(evt){
    scope.$apply(function(){
        var activePoints = myLineChart.getPointsAtEvent(evt); 
        if (activePoints.length > 0){
            scope.clicked({ value : activePoints[0].value });
        }   
    });

};

Aaaaand, it works ! Awesome..

Full code

index.html

<html>
    <body>
        <div ng-app="app" ng-controller="appController">
            Clicked values :
            <ul>
                <li ng-repeat="value in values">
                    {{value}}
                </li>
            </ul>

            <canvas id="myChart" width="400" height="400"></canvas>

            <chart-wrapper data="data" line-clicked="clicked(value)" ></chart-wrapper>
        </div>

        <script src="angular.js"></script>
        <script src="chartjs.js"></script>
        <script src="app.js"></script>
    </body>
</html>

app.js

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

app.controller('appController', function($scope){
    $scope.data = {
                labels: ["January", "February", "March", "April", "May", "June", "July"],
                datasets: [
                    {
                        label: "My First dataset",
                        fillColor: "rgba(220,220,220,0.2)",
                        strokeColor: "rgba(220,220,220,1)",
                        pointColor: "rgba(220,220,220,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(220,220,220,1)",
                        data: [65, 59, 80, 81, 56, 55, 40]
                    },
                    {
                        label: "My Second dataset",
                        fillColor: "rgba(151,187,205,0.2)",
                        strokeColor: "rgba(151,187,205,1)",
                        pointColor: "rgba(151,187,205,1)",
                        pointStrokeColor: "#fff",
                        pointHighlightFill: "#fff",
                        pointHighlightStroke: "rgba(151,187,205,1)",
                        data: [28, 48, 40, 19, 86, 27, 90]
                    }
                ]
            };

    $scope.clicked = function(value){
        console.log('you clicked ' + value);
        $scope.values.push(value);
    };

    $scope.values = [];
});

app.directive('chartWrapper',function(){
    return {
        restrict : 'E',
        replace : true,
        scope : {
            data : '=',
            clicked : '&lineClicked'
        },
        link : function(scope, element , attr ){
            var canvas = document.getElementById("myChart"); 
            var ctx = canvas.getContext("2d");
            var myLineChart = new Chart(ctx).Line(scope.data);
            myLineChart.update();

            canvas.onclick = function(evt){
                scope.$apply(function(){
                    var activePoints = myLineChart.getPointsAtEvent(evt); 
                    if (activePoints.length > 0){
                        scope.clicked({ value : activePoints[0].value });
                    }   
                });

            };

        }
    };
});

jquery ui

Jquery and jquery ui has been around the block for a while. A lot of code has been written so in some cases it just makes more sense to wrap something from jqeryui than to build your own thing in angular. After all, as a programmer you are usually on a time crunch.

So we will wrap the Datepicker, https://jqueryui.com/datepicker/#dropdown-month-year
Because it has cool functions like

  • animations
  • localization
  • from,date functionality

And much more

Get started

We decided on implementing date selection and animation. But first off date selection. From a jquery perspective we need to run init code to make sure the datepicker is initialized correctly. We also need to wire up the changed event and tell someone that our date changed. That someone could be a controller.

app.directive('datePicker', function(){
    return {
        replace : true,
        scope : {
            changed : '&dateChanged',
            animation : '@'
        },
        template : '<input type="text" />',
        link : function(scope,element,attrs){


            function init(){
                element.datepicker();
            }

            init();
        }
    }
}); 

We created a directive and call jquery ui so that our picker is initialized. all this happens inside a link function.

link : function(scope,element,attrs){
    function init(){
        element.datepicker();
    }

    init();
}

Next off is handling the changed event.

element.on('change',function(){
    // do something here
});

As you may have noticed. In writing the directive we added two scope properties

scope : {
            changed : '&dateChanged',
            animation : '@'
        }

A changed callback – to be called when a date changed

An animation property – the idea is to set an animation on the datepicker, more on that one later..

As you may have noticed we didn’t call the changed callback upon changing a value. Let’s do that now

element.on('change',function(){
            scope.$apply(function(){
                scope.changed({ date: element.val() }); 
            });
});

And of course this means we have index.html file looking like this.

<div ng-app="app" ng-controller="appController">

        <date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker>

        Selected date:  {{selectedDate}}
</div>

Lets add the dateChanged callback to the appController

$scope.dateChanged = function(date){
    $scope.selectedDate = date;
}

So at this point we take care of the changed event and communicates with the controller that a date has been selected.

It’s time to add animation and as you saw before that was added to the directives isolated scope like so:

scope : {
        changed : '&dateChanged',
        animation : '@'
    }

And we added a value to it like so:

<date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker>

It’s just a matter of calling the correct datePicker constructor and we are done. So in the directive we change the init method to look like:

    function init(){
        if(scope.animation){
            element.datepicker({
                showAnim : scope.animation
            }); 
        }else{
            element.datepicker();
        }
    }

So if animation is set we call the datepicker constructor and pass it an options object.

Complete code

index.html

<p><date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker></p>
<p><date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker></p>
<p><date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker></p>
<p><date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker></p>
<p><date-picker animation="bounce" date-changed="dateChanged(date)" control-id="test"></date-picker></p>

app.js

app.controller('appController', function($scope){


    $scope.dateChanged = function(date){
        $scope.selectedDate = date;
    }


});

app.directive('datePicker', function(){
    return {
        replace : true,
        scope : {
            changed : '&dateChanged',
            animation : '@'
        },
        template : '<input type="text" />',
        link : function(scope,element,attrs){
            element.on('change',function(){
                scope.$apply(function(){
                    scope.changed({ date: element.val() }); 
                });
            });

            function init(){
                if(scope.animation){
                    element.datepicker({
                        showAnim : scope.animation
                    }); 
                }else{
                    element.datepicker();
                }
            }

            init();
        }
    }
});

That’s all folks. Remember I am writing this for my own memory and for you guys so if you have any comments or suggestion of topics feel free to leave a comment

Cheers Chris

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s