Thanks First

At the very beginning of this post, I want to give credit to following two online courses. What I learnt from these courses, converted in this post.

· AngularJS Custom directive course by Dan Wahlin.[Note: Click on this link to get 15% discount]

· AngularJS Directives Fundamentals by Joseph Eames on Plural Sight

 What is a Directive?

In the angularJS directives perform the following tasks,

  • It attaches specified behavior to DOM elements
  • It creates new element on the DOM
  • It transform the DOM elements.

In simple words, using directives we can modify the behavior of a particular DOM element or add new custom element on the DOM. For example ng-show is a directive. It modifies behavior of an element that whether that element will be visible or not.

Let us consider following code listing as an example,


<div ng-show="true">
    Hi I will be visible
</div>
<div ng-show="false">
    Hi I will not be visible
</div>

We are using built in directive ng-show to amend the behavior of the div. First div will be visible whereas second div won’t.

First custom directive

A very simple custom directive can be created as shown in below listing,


myApp.directive('myFirstDirective', function () {
    return {
        template: "<b>Hello from custom directive</b>",
        restrict: "E"
    }
})

Above is the minimal directive we can create. Custom directive can be created by using the directive method. It takes directive name and the function as the input parameters. There are some important points, you must keep in mind while creating a directive,

  • Directive name is provided in the camel case
  • On the view directive can be used by separating the camel case name of the directive either using dash, colon, or underscore
  • Combination of dash, underscore or colon can also be used

We have given the directive name as myFirstDirective. On the view it can be used either as my-first-directive or my:first:directive or my_first_directive or even as my_first-directive or my-first:directive

On the view custom directive myFirstDirective can be used as shown in below listing. As you see we are using the custom directive with various combinations of dash, underscore, and colon. For angular all three options are exactly the same.

<body>
    <my-first-directive></my-first-directive>
    <br />
    <my:first:directive></my:first:directive>
    <br />
    <my_first_directive></my_first_directive>
    <br />
    <my:first_directive></my:first_directive>
    <br />
    <my-first-directive />
</body>

In the chrome browser developer tools on selecting inspect element option, you can see that the directive template has been replaced with the html.

image

Custom directive can be used on the view in four different ways.

  1. As an attribute – set restrict value to A
  2. As a custom element – set restrict value to E
  3. As a comment – set restrict value to M
  4. As a class- set restrict value to C

Out of these four methods, as an attribute and as an element are widely used. In older version of some browsers comment and class ways of using directive can be used.

Custom Directive with data binding

Let us proceed to create a custom directive which will use the angularjs bindings. We have a controller as listed below,


myApp.controller('studentscontroller', ['$scope', function ($scope) {
    $scope.student = {
        name: "dj",
        age: 32,
        subject: [
            "math",
            "geography"
        ]
    }
}]);

Usually to display data from the controller, we use binding expression on the view. Other way to display data using the custom directive which will use the data binding. We can create custom directive with the binding expression as shown in listing below:

myApp.directive('studentDetail', function () {
    return {
        template: "<b>hey {{student.name}} is {{student.age}} old </b>",
        restrict: "E"
    }
});

As you notice inside the custom directive studentDetail, we are using the binding expression. Now we can use studentDetail custom directive on the view as shown in below listing,

<div ng-controller="studentscontroller">
    <student-detail></student-detail>
</div>

On the view you will get data renderd using the custom directive as shown in image below,

image

And on the element inspection in the chrome browser developer tool, you will find element in the DOM as shown in image below,

image

In the DOM if we want to replace the custom element that can be done by setting the replace option in the custom directive. Let us modify the studentDetail directive by setting the replace attribute to true.

myApp.directive('studentDetail', function () {
    return {
        template: "<b>hey {{student.name}} is {{student.age}} old </b>",
        restrict: "E",
        replace: true
    }
});

Now in the DOM, we will notice that custom element has been removed as shown in the image below,

image

Using templateUrl

When we create complex custom directive, template as a string could be tough to handle. We can have template in external HTML file and can be loaded using the templateUrl attribute. We can move the template in the external HTML file which must have the same name as of the directive. So let us add studentDetail.html file in the application and move template string the HTML file.

studentDetail.html

<div>
    <b>hey {{student.name}} is {{student.age}} old </b>
</div>

We need to modify the directive as shown in the listing below:

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetaildj.html",
        restrict: "E",
        replace: true
    }
});

Let us go bit creative and use bootstrap to make the template more immersive. I am using the bootstrap panel. Using the ng-show directive, we are hiding/showing the subject div.

<div class="panel panel-primary">
    <div class="panel-heading">
        {{student.name}} {{student.age}}
    </div>
    <div ng-show='!!student.subject'>
        Subjects:
        <ul ng-repeat="s in student.subject">
            <li>{{s}}</li>
        </ul>
    </div>
</div>

Events in the directive

Let us see that how can we work with events in directive. Let us say that we have an event in the controller as shown in below listing,

myApp.controller('studentscontroller', ['$scope', function ($scope) {
    $scope.student = {
        name: "dj",
        age: 32,
        subject: [
            "math",
            "geography"
        ]
    }

    $scope.setGrade = function (student) {
        student.grade = "A+"
    }

}]);

We have added setGrade function in the controller. We can use this event directly in the directive template as listed below,

<div class="panel panel-primary">
    <div class="panel-heading">
        {{student.name}} {{student.age}}
    </div>
    <div ng-show='!!student.subject'>
        Subjects:
        <ul ng-repeat="s in student.subject">
            <li>{{s}}</li>
        </ul>
    </div>
    <div ng-show='!!student.grade'>
        {{student.grade}}
    </div>
    <div ng-show='!student.grade'>
        <button class="btn btn-success" ng-click="setGrade(student)">set grade</button>
    </div>
</div>

In the directive template we added a button and using the ng-click directive handling the event on the button. In browser application will look like as shown in image next,

image

The problem with above approach is that the click event reside in the parent controller than being part of the directive. Hence it violates the rule of encapsulation. In idea scenario we will like to have click event function as the part of the directive.

AngularJS directive supports a controller inside that. We can add a controller inside the directive as shown in listing below,

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        controller: function ($scope) {
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
        }

    }
});

In the browser application will render exactly the same as it was running when the setGrade function was part of the controller.

Scopes in directives

AngularJS directives are of three types as mentioned below,

  1. Shared scope
  2. Inherited scope
  3. Isolated scope
Shared scope

Default scope is the shared scope. So far we have been working with the shared scope. In the shared scope, the directive share the scope of the controller, in which it is enclosed it.

Let us again consider the example used previously, studentDetail directive is enclosed inside the studentsController. By default studentDetail directive have the same scope as of the studentsController.

To understand it better try logging value of the $scope in the console for both the controller and the directive as shown in the listing below,

myApp.controller('studentscontroller', ['$scope', function ($scope) {
    $scope.student = {
        name: "dj",
        age: 32,
        subject: [
            "math",
            "geography"
        ]
    }
    console.log($scope);
}
]);

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        controller: function ($scope) {
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
            console.log($scope);
        }

    }
});

In the Google chrome developer tool you will find that id of both the $scope are the same.

image

In the shared scope the directive shared the scope of the controller it is enclosed in. Shared scope can be depicted by the next image. As you see that $scope object is being shared in between the controller and the directive. If in the directive we change data on the $scope object, it would be reflected to the controller.

image

In shared scope data attached to the $scope can be changed in the directive and it would be visible in the controller.

Inherited scope

Other option is directive can inherit the scope of the controller it is enclosed in. In this case scope of the controller will be visible to the directive whereas directive scope won’t be visible to the controller. The controller scope becomes the parent of the directive scope. Inherited scope can be set by setting the value of scope property to true as shown in next code listing.

myApp.controller('studentscontroller', ['$scope', function ($scope) {
    $scope.student = {
        name: "dj",
        age: 32,
        subject: [
            "math",
            "geography"
        ]
    }
    console.log($scope);
}
]);

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        scope: true,
        controller: function ($scope) {
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
            console.log($scope);
        }

    }
});

In the Google chrome developer tool we will find that id of directive $scope and the controller $scope are different.

image

However if we examine parent of the directive child scope, we will find that if of the parent is set to the id of the controller scope.

image

Isolated scope

Isolated scope is the most used scope in the angularJS custom directive. It allows us to work different data from the different instance of the same custom directive. Let us consider the studentDetail directive, and use it multiple times as shown in listing below,

<div class="container" ng-controller="studentscontroller">
    <student-detail>  </student-detail>
    <student-detail>  </student-detail>
    <student-detail>  </student-detail>
</div>

In the browser application will be rendered as shown in the next image. No point to guess that all four entry of directives are using the same data from the controller.

image

When you click on one of the set grade, it will set grade for all the four students as shown in the next image,

image

For sure we may not want this kind of behavior in the application. This problem can be solved with the isolated scope. To create directive with the isolated scope, set scope property to object while creating the directive. It is shown in the listing below,

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        scope: {},
        controller: function ($scope) {
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
            console.log($scope);
        }

    }
});

We have created isolated scope for the directive. Now scope is not shared in between the directive and the controller. At this point, on running the application we will find that data is not displayed in the application because scope is not shared.

When you click on one of the set grade, it will set grade for all the four students as shown in the next image,

By diagram let us try to understand shared and isolated scope.

image

 

By diagram let us try to understand shared and isolated scope.

image

image

Now the question bin front of us is that, in isolated scope how to share the data from the controller to the directive? In Isolated scope data can be shared using the local scope property. Directive with the isolated scope can talk to the outside world using the local scope properties. There are three options in local scope properties.

image

@local scope property

The @local scope property can access the string from the outside the directive. Using the @local scope property string value can be passed to the directive.

Let us assume we have a simple directive as shown in the listing below,

myApp.directive('aStudentDirective', function () {
    return {
        scope: {
            name: '@'
        },
        template: "Hi <b>{{name}}</b>",
        restrict: "E"
    }
})

In the above directive we are using the @ local scope property to read the string from outside the directive. Directive is used as shown in the listing below,

<div class="container" ng-controller="studentscontroller">
    <a-student-directive name="{{student.name}}"></a-student-directive>
</div>

In the directive student.name from the studentscontroller can be used. We are passing data to the directive as the string.

If you want local scope property name in the directive to be different than the property name that can be done as shown in the listing below,

myApp.directive('aStudentDirective', function () {
    return {
        scope: {
            name: '@studentname'
        },
        template: "Hi <b>{{name}}</b>",
        restrict: "E"
    }
})

Directive can be used as shown in the listing below,

 

<div class="container" ng-controller="studentscontroller">
    <a-student-directive studentname="{{student.name}}"></a-student-directive>
</div>

If we change value of student name in the controller, directive will be updated. However any change in the directive local property will not make any change in the controller.

=local scope property

Character = local scope property is used to pass object to the directive from the outside world. It also supports two way binding.

To understand it let us go ahead and recreate the controller as shown in the listing below,

myApp.controller('studentscontroller', ['$scope', function ($scope) {
    $scope.student1 = {
        name: "dj",
        age: 32,
        guardian: {
            mother: "abc",
            father: "xyz"
        },
        subject: [
            "math",
            "geography"
        ]
    }
    $scope.student2 = {
        name: "foo",
        age: 14,
        guardian: {
            mother: "pqw",
            father: "rty"
        },
        subject: [
            "physics",
            "geography"
        ]
    }
    $scope.student3 = {
        name: "loo",
        age: 21,
        guardian: {
            mother: "mnq",
            father: "wsy"
        },
        subject: [
            "math",
            "bilogy"
        ]
    }
}
]);

We have added more students to the $scope. This is pretty straight forward controller. We are going to use this controller to understand the local scope property =.

Next let us create a directive,

  • Using the isolated scope
  • Using the = local scope property to pass object to the directive from the outside world

Directive can be created as shown in the listing below,

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        scope: {
            student: '='
        },
        controller: function ($scope) {
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
        }

    }
});

On the HTML we can use the directive passing different value for the student as shown in listing below,

<div class="container" ng-controller="studentscontroller">
    <student-detail student="student1"></student-detail>
    <student-detail student="student2"></student-detail>
    <student-detail student="student3"></student-detail>
</div>

I have also modified the template in studentDetail.html as shown in below listing.

<div class="panel panel-primary">
    <div class="panel-heading">
        <h2>{{student.name}} {{student.age}}</h2>
    </div>
    <div class="panel-body">
    <div ng-show='!!student.subject'>
        Subjects:
        <ul ng-repeat="s in student.subject">
            <li>{{s}}</li>
        </ul>
    </div>
    <div class="well">
        Guardians:<br />
        Father : {{student.guardian.father}}<br />
        Mother : {{student.guardian.mother}}<br />
    </div>
    <div ng-show='!!student.grade'>
        {{student.grade}}
    </div>
    <div ng-show='!student.grade'>
        <button class="btn btn-success" ng-click="setGrade(student)">set grade</button>
    </div>
</div>
</div>

On running the application in the browser, we will find that data of three different students have been rendered. Now when you set grade of one student, another students will not get impacted.

image

Inherited scope example

Now to understand the inherited scope in the better way, let us say we have a requirement to collapse and expand the each student panel when user will click on the panel header. To do this,

  • Add ng-click directive on the panel header. When user click on the header function will be called in the directive controller.
  • Add ng-hide directive to the panel body. On clicking of the panel header panel body will be either collapsed or expanded.

Let us start with adding the ng-click and the ng-hide directive.


<div class="panel panel-primary">
    <div class="panel-heading" ng-click="hide()">

        <h2>{{student.name}} {{student.age}}</h2>
    </div>
    <div class="panel-body" ng-hide="isHidden">
        <div ng-show='!!student.subject'>
            Subjects:
            <ul ng-repeat="s in student.subject">
                <li>{{s}}</li>
            </ul>
        </div>
        <div class="well">
            Guardians:<br />
            Father : {{student.guardian.father}}<br />
            Mother : {{student.guardian.mother}}<br />
        </div>
        <div ng-show='!!student.grade'>
            {{student.grade}}
        </div>
        <div ng-show='!student.grade'>
            <button class="btn btn-success" ng-click="setGrade(student)">set grade</button>
        </div>
    </div>
</div>

In the above code listing, we have added ng-click directive (see panel-header div). On click event hide() function in the directive controller will be called. We also added ng-hide directive (see panel-body div)

Next in the directive we need to add hide() function and isHidden data to the $scope object of the controller which is the part of the directive.

myApp.directive('studentDetail', function () {
    return {
        templateUrl: "studentDetail.html",
        restrict: "E",
        replace: true,
        scope: {
            student: '='
        },
        controller: function ($scope) {
            $scope.isHidden = false;
            $scope.setGrade = function (student) {
                student.grade = "B+"
            }
            $scope.hide = function () {
                $scope.isHidden = !$scope.isHidden;
            }
        }

    }
});

On running the application in the browser, we will find that data of three different students have been rendered and we can expand and collapse student panel as shown in image below:

image

Parent and child directives

We notice that studentDetail directive is getting large. As application grows, it will be tough to manage a too big directive. Let us go ahead and take the guardian data and move it to a separate directive called studentGuardian.

We have created template as shown in the listing below:

studentGuardian.html

 


<div class="well">
    Guardians:<br />
    Father : {{student.guardian.father}}<br />
    Mother : {{student.guardian.mother}}<br />
</div>

Let us go ahead and create directive with the shared scope as shown in the listing below:

myApp.directive('studentGuardian', function () {
    return {
        templateUrl: "studentGuardian.html",
        restrict: "E"
    }
})

In the studentDetail directive, we can use the studentGuardian directive as shown in listing below,

<div class="panel panel-primary">
    <div class="panel-heading" ng-click="hide()">

        <h2>{{student.name}} {{student.age}}</h2>
    </div>
    <div class="panel-body" ng-hide="isHidden">
        <div ng-show='!!student.subject'>
            Subjects:
            <ul ng-repeat="s in student.subject">
                <li>{{s}}</li>
            </ul>
        </div>
        <student_guardian></student_guardian>
        <div ng-show='!!student.grade'>
            {{student.grade}}
        </div>
        <div ng-show='!student.grade'>
            <button class="btn btn-success" ng-click="setGrade(student)">set grade</button>
        </div>
    </div>
</div>

Now we have a nested directive studentGuardian inside the main directive studentDetail. And the studentGuardain directive working with the shared scope. We got a similar kind of requirement to expand and collapse the guardian section.

Let us start with adding ng-click and ng-show to the studentGuardian directive template. Now we have two div on the studentGuardian directive template. One will be displayed when in the expanded and other in the collapsed mode.

<div class="well" ng-show='isHidden' ng-click="showGuardian()">
    Guardians::::::
</div>
<div class="well" ng-show='!isHidden' ng-click="hideGuardian()">
    Guardians:<br />
    Father : {{student.guardian.father}}<br />
    Mother : {{student.guardian.mother}}<br />
</div>

Next in the studentGuardian directive,

  • Add a controller
  • Add isHidden data to the $scope of the studentGauradian template. We have isHidden data in the $scope of the studentDetail directive also. On purpose we are giving name same name to see the behavior of inherited scope
  • Added two functions hideGuardain and showGuardain to set the vale if isHidden false and true respectively
  • Value of scope property is set to true.

Modified directive is listed next,

myApp.directive('studentGuardian', function () {
    return {
        templateUrl: "studentGuardian.html",
        restrict: "E",
        scope: true,
        controller: function ($scope) {
            $scope.isHidden = false;
            $scope.hideGuardian = function () {
                $scope.isHidden = true;
                console.log('h' + $scope.isHidden);
            };
            $scope.showGuardian = function () {
                $scope.isHidden = false;
                console.log('s ' + $scope.isHidden);
            }

        }
    }
});

On running the application in the browser, we will find that now guardian can be expanded and collapsed separately. If we change the value of scope to false then when we click n guardian the whole panel will be collapsed/expanded since the isHidden variable is shared in the shared scope.

Summary

In this post we learnt about directive, custom directive, and scopes in the anularJS. In next post we will focus on decorator directives, link function etc. I hope you find this post useful. Thanks for reading.

Advertisements

2 thoughts on “Directives in AngularJS simplified with examples: Part 1

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