The main thing about AngularJS is that it allows us to extend the capabilities of HTML to serve the purpose of today's dynamic webpages. In this article, I will show you how you can use AngularJS's Directives to make your development faster, easier, and your code more maintainable.
Preparation
Step 1: HTML Template
To make things simpler we will write all of our code in one HTML file. Create it and put the basic HTML template in it:
<!DOCTYPE html> <html> <head> </head> <body> </body> </html>
Now add the angular.min.js
file from Google's CDN in the <head>
of the document:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script>
Step 2: Creating the Module
Now let's create the module for our directives. I will call it example, but you can choose whatever you want, just keep in mind that we will use this name as a namespace for the directives we create later.
Put this code in a script tag a the bottom of the <head>
:
var module = angular.module('example', []);
We don't have any dependencies, so the array in the second argument of angular.module()
is empty, but do not remove it completely or you will get a $injector:nomod error, because one-argument form of angular.module()
retrieves a reference to the already existing module instead of creating a new one.
You also have to add a ng-app="example"
attribute to the <body>
tag for the app to work. After that the file should look like this:
<!DOCTYPE html> <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script> <script> var module = angular.module('example', []); </script> </head> <body ng-app="example"> </body> </html>
The Attribute Directive: 1337 C0NV3R73R
First, we will create a simple directive that will work similarly to ngBind, but it will change the text to the leet speak.
Step 1: Directive Declaration
Directives are declared using module.directive()
method:
module.directive('exampleBindLeet', function () {
The first argument is the name of the directive. It must be in camelCase, but since HTML is case-insensitive you will use dash-delimited lower-case form of it (example-bind-leet) in your HTML code.
The function passed as the second argument must return an object describing the directive. For now it will only have one property: The link function:
return { link: link }; });
Step 2: The Link Function
You can either define the function before the return statement, or directly in the object that is returned. It is used to manipulate the DOM of the element that our directive was applied to and is called with three arguments:
function link($scope, $elem, attrs) {
$scope
is an Angular scope object, $elem
is the DOM element that this directive matched (it's wrapped in jqLite, AngularJS's subset of jQuery's most commonly used functions) and attrs
is an object with all of the element's attributes (with normalized names, so example-bind-leet will be available as attrs.exampleBindLeet
).
The simplest possible code for this function in our directive would look like this:
var leetText = attrs.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) { return leet[letter.toLowerCase()]; }); $elem.text(leetText); }
First, we replace some of the letters in the text provided in the example-bind-leet
attribute with their replacements from the leet table. The table looks like this:
var leet = { a: '4', b: '8', e: '3', g: '6', i: '!', l: '1', o: '0', s: '5', t: '7', z: '2' };
You should place it at the top of your <script>
tag. As you can see this is the most basic leet converter as it replaces only ten characters.
Afterwards, we converted the string to leet speak we use jqLite's text()
method to put it in the inner text of the element that this directive matched.
Now you can test it by putting this HTML code in the <body>
of the document:
<div example-bind-leet="This text will be converted to leet speak!"></div>
The output should look like this:
But this is not exactly how the ngBind
directive works. We will change that in the next steps.
Step 3: The Scope
First of all, the thing passed in the example-bind-leet
attribute should be a reference to a variable in the current scope, not the text that we want to convert. To do this we will have to create an isolated scope for the directive.
We can achieve that by adding a scope object to the return value of our directive function:
module.directive('exampleBindLeet', function () { ... return { link: link, scope: { } }; );
Each property in this object will be available in directive's scope. It's value will be determined by the value here. If we use '-' the value will be equal to the value of the attribute with the same name as the property. Using '=' will tell the compiler that we expect a variable from the current scope to be passed - which will work just like ngBind
:
scope: { exampleBindLeet: '=' }
You can also use anything as the property name, and put the normalized (converted to camelCase) attribute name after - or =:
scope: { text: '=exampleBindLeet' }
Choose what works best for you. Now we also have to change the link function to use the $scope
instead of attr
:
function link($scope, $elem, attrs) { var leetText = $scope.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) { return leet[letter.toLowerCase()]; }); $elem.text(leetText); }
Now use ngInit or create a controller and change the value of the div
's example-bind-leet
attribute to the name of the variable you used:
<body ng-app="example" ng-init="textToConvert = 'This text will be converted to leet speak!'"> <div example-bind-leet="textToConvert"></div> </body>
Step 4: Detecting Changes
But that is still not how ngBind
works. To see that let's add an input field to change the value of textToConvert after the page has been loaded:
<input ng-model="textToConvert">
Now if you open the page and try to change text in the input you will see that nothing changes in our div
. This is because the link()
function is called once per directive at compilation time, so it can't change the content of the element every time something changes in the scope.
To change that we will use the $scope.$watch() method. It accepts two parameters: first one is Angular expression which will be evaluated every time the scope is modified, second is a callback function that will be called when the expression's value has changed.
First, let's put the code we had in the link()
function in a local function inside it:
function link($scope, $elem, attrs) { function convertText() { var leetText = $scope.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) { return leet[letter.toLowerCase()]; }); $elem.text(leetText); } }
Now after that function we will call $scope.$watch()
like this:
$scope.$watch('exampleBindLeet', convertLeet);
If you open the page now and change something in the input field you will see that the content of our div
also changes, as expected.
The Element Directive: Progress Bar
Now we will write a directive that will create a progress bar for us. To do that we will use a new element: <example-progress>
.
Step 1: Style
To make our progress bar look like a progress bar we will have to use some CSS. Put this code in a <style>
element in the <head>
of the document:
example-progress { display: block; width: 100%; position: relative; border: 1px solid black; height: 18px; } example-progress .progressBar { position: absolute; top: 0; left: 0; bottom: 0; background: green; } example-progress .progressValue { position: absolute; top: 0; left: 0; right: 0; bottom: 0; text-align: center; }
As you can see it's pretty basic - we use a combination of position: relative
and position: absolute
to position the green bar and the value inside our <example-progress>
element.
Step 2: Directive's Properties
This one will require few more options than the previous one. Take a look at this code (and insert it into your <script>
tag):
module.directive('exampleProgress', function () { return { restrict: 'E', scope: { value: '=', max: '=' }, template: '', link: link }; });
As you can see we are still using a scope (with two properties this time - value for the current value and max for the maximum value) and the link() function, but there are two new properties:
- restrict: 'E' - this one tells the compiler to look for elements instead of attributes. Possible values are:
- 'A' - matches only attribute names (this is the default behavior, so you don't need to set it if you want to match only attributes)
- 'E' - matches only element names
- 'C' - matches only class names
- You can combine them, for example 'AEC' would match attribute, element and class names.
- template: '' - this allows us to change the inner HTML of our element (there is also templateUrl if you want to load your HTML from separate file)
Of course, we will not leave template blank. Put this HTML there:
<div class="progressBar"></div><div class="progressValue">{{ percentValue }}%</div>
As you can see we can also use Angluar expressions in the template - percentValue
will be taken from directive's scope.
Step 3: The Link Function
This function will be similar to the one in previous directive. First, create a local function that will perform the directive's logic - in this case update the percentValue
and set div.progressBar
's width:
function link($scope, $elem, attrs) { function updateProgress() { var percentValue = Math.round($scope.value / $scope.max * 100); $scope.percentValue = Math.min(Math.max(percentValue, 0), 100); $elem.children()[0].style.width = $scope.percentValue + '%'; } }
As you can see, we can't use .css()
to change the div.progressBar's width because jqLite doesn't support selectors in .children()
. We also need to use Math.min()
and Math.max()
to keep the value between 0% and 100% - Math.max()
will return 0 if precentValue is lower than 0 and Math.min()
will return 100 if percentValue
is higher than 100.
Now instead of two $scope.$watch()
calls (we have to watch for changes in $scope.value
and $scope.max
) let's use $scope.$watchCollection()
, which is similar but works on collections of properties:
$scope.$watchCollection('[value, max]', updateProgress);
Note that we are passing string which looks like an array as the first paramter, not JavaScript's Array.
To see how it works first change ngInit
to initialize two more variables:
<body ng-app="example" ng-init="textToConvert = 'This text will be converted to leet speak!'; progressValue = 20; progressMax = 100">
And then add the <example-progress>
element below the div
we used earlier:
<example-progress value="progressValue" max="progressMax"></example-progress>
The <body>
should look like this now:
<body ng-app="example" ng-init="textToConvert = 'This text will be converted to leet speak!'; progressValue = 20; progressMax = 100"> <div example-bind-leet="textToConvert"></div> <example-progress value="progressValue" max="progressMax"></example-progress> </body>
And this is the result:
Step 4: Adding Animations Using jQuery
If you add inputs for progressValue
and progressMax
like this:
<input ng-model="progressValue"> <input ng-model="progressMax">
You will notice that when you change any of the values the change in width is immediate. To make it look a bit nicer let's use jQuery to animate it. The nice thing about using jQuery with AngularJS is that when you include jQuery's <script>
Angular will automatically replace jqLite with it, making $elem
a jQuery object.
So let's begin with adding the jQuery script to the <head>
of the document, before AngularJS:
<script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
Now we can change our updateProgress()
function to use jQuery's .animate()
method. Change this line:
$elem.children()[0].style.width = $scope.percentValue + '%';
To this:
$elem.children('.progressBar').stop(true, true).animate({ width: $scope.percentValue + '%' });
And you should have a beautifully animated progress bar. We had to use .stop() method to stop and finish any pending animations in case we change any value while the animation is in progress (try to remove it and change the values in inputs rapidly to see why it was needed).
Of course you should change the CSS and probably use some other easing function in your app to match your style.
Conclusion
AngularJS's directives are a powerful tool for any web developer. You can create a set of your own directives to simplify and boost your developing process. What you can create is only limited by your imagination, you can pretty much convert all of your server-side templates to AngularJS directives.
Useful Links
Here are some links to AngularJS documentation:
Comments