Getting started with AngularJS Directive

As part of our exploration we need to set up the Bootstrap CSS and AngularJS libraries. We are also referencing a js file named app.js where our code will recide. Let’s start with the following code.

<!DOCTYPE html>
<html ng-app="demo">
    <head>
       <title>Dissecting an AngularJs directive</title> 
        <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
        <script src="app.js"></script>
    </head>
    
    <body ng-controller="demoController">
        <div class="container" style="margin-top:40px;">
            <div class="row">
                <div class="span12">
                    
                    <div class="btn-group" data-toggle="buttons-checkbox">
                        <button type="button" class="btn btn-primary" ng-model="options.facebook" btn-checkbox>Facebook</button>
                        <button type="button" class="btn btn-primary" ng-model="options.twitter" btn-checkbox>Twitter</button>
                        <button type="button" class="btn btn-primary" ng-model="options.google" btn-checkbox>Google+</button>
                    </div>
                    
                    <div></div>

                </div>
            </div>
        </div>
    </body>
</html>

In Bootstrap, we can create a button group by wrapping several buttons with btn class inside a wrapper having class btn-group. To make any button inside the group look like a selected one we need to add the class active to that button which will be handled by our directive. In the above markup we have invoked our yet to be defined directive btnCheckbox1 using the attribute btn-checkbox2 on each of the buttons.

We have explicitly named our module demo using the ng-app directive on the html tag and thus we have to bootstrap the module inside the js file. Now all of our buttons are looking similar as we have not added active class to any.

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

function demoController($scope){
    $scope.message = "from angularjs";
    $scope.options = {
        facebook : true,
        twitter : false,
        google : true
    };
}

Inside the javascript we are specifying that facebook and google should have an active class and should look like they are selected. Let’s create a directive to handle this specification.

The missing Link

We will start with the bare minimum needed for satisfying our directive. The most often used part of a directive is its link function. Inside the link function, by default, we have access to the scope, element, and the attributes as parameters. We can register the directive on the module we have defined in the previous step.

app.directive("btnCheckbox", function(){
    return function(scope, element, attr){
        console.log(scope, element, attr);

        element.bind("click", function(){
            console.log("Need to change the model value but dont know how to yet");
        })
    }
});

If we refresh the browser now, we can see that we have access to the scope, element, and attributes. We have also used the link function to bind a click method on the element so that whenever we have clicked an element it will output a message to the console.

Next we need to have access the ngModelController3 so that we can read and set the model data associated with the element. In this case our link function should be injected with the ngModelController and for that to happen our directive should return a Directive Definition Object rather than a simple link function.

Directive Definition Object

app.directive("btnCheckbox", function(){
    return {
        require:"ngModel",
        link: function(scope, element, attr, ngModel){
                console.log(scope, element, attr);
                console.log(ngModel);
                element.bind("click", function(){
                    console.log("Need to change the model value but dont know how to yet");
                })
            }
    }
});

The changes introduced are, we are now returning Directive Definition Object rather than a simple link function and as we have added a require property on that definition object link function will receive the ngModelController object as an extra parameter.

Being a Paparazzi or how to love watching a model

We can access the current model value through $modelValue property of ngModelController. Inside the directive it is not enough to update the value once, instead we have to watch the model value and need to take an action whenever the model value is changed. $watch method on scope can handle monitoring an expression and then reacting to it whenever it changes.

app.directive("btnCheckbox", function(){
    return {
        require:"ngModel",
        link: function(scope, element, attr, ngModel){
                scope.$watch(function(){
                    return ngModel.$modelValue;
                }, function(modelValue){
                    console.log(modelValue);
                    if (modelValue) {
                        element.addClass("active");
                    }else{
                        element.removeClass("active");
                    };
                });
                element.bind("click", function(){
                    console.log("Need to change the model value but dont know how to yet");
                })
            }
    }
});

Now our directive code is picking up the initial model changes and is ready to handle further changes to the model.

Applying the changes back

So far, we have managed to propagate any changes from the model to the view or DOM elements. The other part of the story is to propagate the changes from the view to the model. If $watch could be considered as a key factor in former, the corresponding player for the later is $apply4.

$apply5 on the scope is used to propagate model changes from the view/DOM. Another useful method to change the model value is $setViewValue on the NgModelController.

app.directive("btnCheckbox", function(){
    return {
        require:"ngModel",
        link: function(scope, element, attr, ngModel){
                scope.$watch(function(){
                    return ngModel.$modelValue;
                }, function(modelValue){
                    console.log(modelValue);
                    if (modelValue) {
                        element.addClass("active");
                    }else{
                        element.removeClass("active");
                    };
                });
                element.bind("click", function(){
                    scope.$apply(function(){
                        ngModel.$setViewValue(element.hasClass("active") ? false : true);
                    });
                })
            }
    }
});

As a final grand test lets add three checkboxes to change the model values.

<!DOCTYPE html>
<html ng-app="demo">
    <head>
       <title>Dissecting an AngularJs directive</title> 
        <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
        <script src="app.js"></script>
    </head>    
    <body ng-controller="demoController">
        <div class="container" style="margin-top:40px;">
            <div class="row">
                <div class="span12">                    
                    <div class="btn-group" data-toggle="buttons-checkbox">
                        <button type="button" class="btn btn-primary" ng-model="options.facebook" btn-checkbox>Facebook</button>
                        <button type="button" class="btn btn-primary" ng-model="options.twitter" btn-checkbox>Twitter</button>
                        <button type="button" class="btn btn-primary" ng-model="options.google" btn-checkbox>Google+</button>
                    </div>                    
                    <div></div>
                    <div>                        
                        <input type="checkbox" ng-model="options.facebook" />
                        <input type="checkbox" ng-model="options.twitter" />
                        <input type="checkbox" ng-model="options.google" />
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

Check the final demo here

Footnotes & References

1 Directives have camel cased names such as ngBind

2 The directive can be invoked by translating the camel case name into snake case with these special characters :, -, or _

3 http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController

4 http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

5 $apply either takes an angularjs expression string or a function

My first experience with AngularJS a few months back was hate at first sight. Further deep explorations and the directives feature convinced me about the power of AngularJS as a framework. - Suhair
comments powered by Disqus