Code modularization is popular and very important in the most modern programming techniques. AngularJS is not an exception. A well-structured code should consist of modules and components that have separate responsibilities and could be refactored or replaced without knowing the whole application. Such architecture makes code maintenance easier.
This article shows how to create a custom module in AngularJS. A module that can be reused in the application in many places. It shows how to separate the global scope from the module scope, how to write a custom directive that exchange data with the outside world and how to link it to a controller.
The community has developed many AngularJS components that can be used in our projects. A few weeks ago, I wrote how an external module can be added to a project. This time, I will create my own module that will be used inside my sample application.
What I am going to build
At the starting point, I have a simple AngularJS application with an index.html file with nothing special in it. It just has AngularJS configured with a basic controller.
<!doctype html>
<html lang="en" ng-app="angularTest">
<head>
<meta charset="utf-8">
<title>AngularTest</title>
<!-- build:js scripts/scripts.js -->
<script src="bower_components/angular/angular.js"></script>
<script src="app/app.js"></script>
<script src="app/controllers.js"></script>
<!-- endbuild -->
</head>
<body ng-controller="AngularTestController">
</body>
</html>
I am going to create a questionnaire with three yes/no questions. Choosing an answer is going to automatically present it in a different place on the page. The module that is going to be developed is a combination of a question and yes/no radio buttons. As there are three questions, the module will be used three times on the page.
Create an HTML template
As described above, the module will represent a question and radios for answers. I choose dba-question as a name of the module. For better separation, all the code related to it will be encapsulated in the app/modules/dba-question directory.
I create an HTML file that will be a template of the directive. In fact, it will be inserted into the main index.html file to display a question and radios for yes and no answers. I call the file dbaQuestionTemplate.html and put it into the app/modules/dba-question directory:
<form>
<span>{{question}}</span>
<input type="radio" ng-model="answer" value="Yes" />Yes
<input type="radio" ng-model="answer" value="No" />No
</form>
When displayed it looks like this:
Of course, it does nothing smart yet, but I hope you get my point. If {{question}} is replaced with a text of question and something useful was done with the answer it would be a good step forward. But for that, I will create a controller and a directive.
Create a controller
My simple example can do without a controller but most useful modules need one so I create it just for completeness of this example. A controller is an object that creates its own scope and takes care of the logic side of the module. I create app/modules/dba-question/dbaQuestionController.js with the following content:
angular.module('angularTest.dbaQuestion', [])
.controller('DbaQuestionController', ['$scope', function($scope) {
// controller's content goes here
}]);
My controller is called dbaQuestion in the JavaScript code. DbaQuestionController is a name of the module controller. The brackets ([]) in the first line indicates that new instance of the module is created by this code. Without the brackets, Angular would try to find already existing one but it has not yet been instantiated. That is why the brackets are necessary here.
Create a directive
While the controller created above does nothing big, a directive is very important in this case. The directive definition is put into a separate file - app/modules/dba-question/dbaQuestionDirective.js:
angular.module('angularTest.dbaQuestion')
.directive('dbaQuestion', function() {
return {
scope: {
question: '@',
answer: '='
},
restrict: 'AE',
templateUrl: 'app/modules/dba-question/dbaQuestionTemplate.html',
controller: 'DbaQuestionController'
}
});
The directive is called dbaQuestion. There are no brackets in the first line so the module command will not create a new variable but will return the already existing one (the one created in the dbaQuestionController.js file).
The scope option in the above definition means that the directive will use an isolated scope which is a separate one from the parent scope. Anything created inside the isolated scope stays there and the directive has not access to variables from the parent scope. It means there is no polluting the parent scope with the directive scope variables. But I cannot afford not exchanging data between the directive and the rest of the application - I have to somehow pass a question to the directive to print it and return an answer chosen by a user. The first problem (passing a question) is solved by question: '@'. It tells AngularJS to create a question variable inside the module scope and pass the question's value from the parent scope to the variable of the directive's scope.
The answer variable case is similar except that there is two-way data binding used (the = sign). Answer can be passed from the application to the directive and from the directive to the application.
The restrict option (AE) defines how the directive can be used. In this case, dbaQuestion can be an HTML attribute (A) or an element (E).
The directive is represented by HTML defined in dbaQuestionTemplate.html.
Logic is handled by DbaQuestionController.
Add module's dependency
At this point, the module is pretty much ready. Before using it, a dependency on the dbaQuestion module has to be added to the angularTest module. In my case, it is adding one line into app.js:
angular.module('angularTest', [
'angularTest.controllers',
'angularTest.dbaQuestion'
]);
The above code means that the angularTest depends on the controllers and the dbaQuestion modules.
Use it
Once the dependency is added on the main module, I need to make sure it is loaded. It can be done by adding proper <script> tags in the index.htm file. It is important to dbaQuestionController.js be placed before dbaQuestionDirective.js.
The dbaQuestion directive can be used in HTML code as <dba-question> tag.
This is how index.html looks:
<!doctype html>
<html lang="en" ng-app="angularTest">
<head>
<meta charset="utf-8">
<title>AngularTest</title>
<!-- build:js scripts/scripts.js -->
<script src="bower_components/angular/angular.js"></script>
<script src="app/modules/dba-question/dbaQuestionController.js"></script>
<script src="app/modules/dba-question/dbaQuestionDirective.js"></script>
<script src="app/app.js"></script>
<script src="app/controllers.js"></script>
<!-- endbuild -->
</head>
<body ng-controller="AngularTestController">
<h3>Questions</h3>
<dba-question question="Do you work in IT?" answer="answer0"></dba-question>
<dba-question question="Do you like dba-presents.com?" answer="answer1"></dba-question>
<dba-question question="Do you participate in meetups?" answer="answer2"></dba-question>
<br />
<h3>Answers</h3>
<p>IT? - {{answer0}}</p>
<p>dba-presents.com? - {{answer1}}</p>
<p>meetups? - {{answer2}}</p>
</body>
</html>
As you can notice that two arguments (question and answer) are passed to the dba-question tag. If you remember the directive's code, there were two variables declared in the scope: question and answer. Now, the arguments of the HTML tag are the missing links between the application scope and the directive scope.
The question variable is passed in one direction - from the application to the directive. That is why a text of a question can be displayed by the directive. The answer variable is passed in both ways. Here, it is important to send an answer from the directive to the application and to display it by {{answer0}}, {{answer1}}, {{answer2}} outside the directive.
There is nothing else to do besides showing how it works. The page after choosing answers for two questions looks like below.
This module is just an example of creating a custom module and a directive that can be reused in the application. It makes the code clean and easy to maintenance. The example shows the idea and is a good starting point to build more complex custom directives.