In this last directives section, we again return to our analogy (a team of laborers completing some building works), as we have yet to cover any decorative directives.
Rather than strictly being about styling and CSS, by decorative I mean in the sense of HTML elements being decorated with additional behaviour. This is like having a new kitchen fitted, which isn’t stuctural “bricks and mortarâ€, nor is it plumbing as the pipes have already been laid and are waiting to be used. But a new kitchen definitely gives a house additional behavior, especially if it has one of those fancy taps producing instant boiling water.
Lets now look at a couple of example decorative directives, namely ng-click
and ng-checked
.
ng-click
ng-click
decorates HTML elements with the ability to invoke a model behaviour when they are clicked, which I suspect is fairly self-explanatory.
In the previous section, we introduced a factory service calculateCategoryPercentage
and used it with a hard-coded list of articles. Let’s make our application a little more interesting by allowing new articles to be created.
Edit index.html
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<html ng-app="tutsocean"> <head> <title>tutsocean - admin area</title> </head> <body ng-controller="articleCtrl"> <input type="text" ng-model="newTitle" placeholder="Enter article name..." /> <button name="Add" ng-click="addArticle()">Add</button> <hr /> Number of articles to display: <input type="text" ng-model="numArticles" /> <ul> <li ng-repeat="article in articles | limitTo:numArticles"> {{article.title}} </li> </ul> <span>Percentage of categories used: {{categoryPercentage}}</span> <script src="angular.js"></script> <script src="app.js"></script> <script src="controllers.js"></script> <script src="services.js"></script> </body> </html> |
In the snippet above, we’ve added two new HTML elements. The text input element will update a model property newTitle
via the ng-model
directive (which has been discussed in a previous section on “Bindingsâ€). The button element will invoke a model behaviour addArticle
when clicked via ng-click
.
Edit controllers.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
angular.module('tutsocean').controller('articleCtrl', function($scope, pageSize, calculateCategoryPercentage) { $scope.articles = [ { title: "HTML Tutorial", categories: ['tutorial', 'hardware'] }, { title: "CSS Tutorial", categories: ['tutorial', 'graphics'] }, { title: "jQuery Tutorial", categories: ['tutorial'] } ]; $scope.newTitle = ''; $scope.addArticle = function() { $scope.articles.push({ title: $scope.newTitle, categories: [] }); }; $scope.numArticles = pageSize; $scope.categoryPercentage = calculateCategoryPercentage($scope.articles); }); |
In the above snippet, addArticle
uses $scope.newTitle
when invoked to push a new article onto $scope.articles
. Note that the new article has an empty categories array.
ng-checked
Our blog admin application needs to allow articles, new and old alike, to be re-categorized. Wouldn’t it be great if when this happens $scope.categoryPercentage
is re-calculated? This can be accomplished with another usage of ng-click
and a new directive ng-checked
. Let’s explore how now.
Edit controllers.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
angular.module('tutsocean').controller('articleCtrl', function($scope, calculateCategoryPercentage, pageSize, availableCategories) { $scope.categories = availableCategories; $scope.articles = [ { title: "HTML Tutorial", categories: ['tutorial', 'hardware'] }, { title: "CSS Tutorial", categories: ['tutorial', 'graphics'] }, { title: "jQuery Tutorial", categories: ['tutorial'] } ]; $scope.$watch('articles', function(articles) { $scope.categoryPercentage = calculateCategoryPercentage(articles); }, true); $scope.containsCategory = function(article, category) { return article.categories.indexOf(category) >= 0; }; $scope.toggleCategory = function(article, category) { var index = article.categories.indexOf(category); if (index == -1) { article.categories.push(category); } else { article.categories.splice(index, 1); } }; $scope.newTitle = ''; $scope.addArticle = function() { $scope.articles.push({ title: $scope.newTitle, categories: [] }); }; $scope.numArticles = pageSize; }); |
We’ve introduced quite a lot in the above snippet. Let’s discuss each in turn.
We previously mentioned that we would find another use for availableCategories
. It is now also injected into articleCtrl
and set as $scope.categories
.
When we create bindings in our HTML (e.g., via ng-bind
, {{ }}
, ng-model
and many others), we are implicitly creating watches. At a simplified level, whenever AngularJS detects something that might affect an application (be it a browser event, HTTP response, and many others), it checks all of its watches against their previous values and updates bindings if there is a difference. We use $scope.$watch
to create a manual watch of the articles
array and re-calculate$scope.categoryPercentage
if there is a difference. The third parameter to $scope.$watch
indicates that we want a “deep†watch. For more information, please have a look at this excellent StackOverflow answer.
Finally, we introduce two new model behaviors for interacting with an article’s categories. $scope.containsCategory
simply saves us from having an ugly expression in our HTML. $scope.toggleCategory
either adds or removes a category based on whether an article is already categorized as such.
To see all of this in action, edit index.html
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<html ng-app="tutsocean"> <head> <title>tutsocean - admin area</title> </head> <body ng-controller="articleCtrl"> <input type="text" ng-model="newTitle" placeholder="Enter article name..." /> <button name="Add" ng-click="addArticle()">Add</button> <hr /> Number of articles to display: <input type="text" ng-model="numArticles" /> <div ng-repeat="article in articles | limitTo:numArticles"> <p>{{article.title}}</p> <label ng-repeat="category in categories"> <input type="checkbox" ng-checked="containsCategory(article, category)" ng-click="toggleCategory(article, category)" /> {{category}} </label> </div> <span>Percentage of categories used: {{categoryPercentage}}</span> <script src="angular.js"></script> <script src="app.js"></script> <script src="controllers.js"></script> <script src="services.js"></script> </body> </html> |
In the above snippet, we come across the usage of ng-click
and ng-checked
mentioned a little while ago. We’ve introduced a second ng-repeat
loop to create a checkbox for each category. This ng-repeat
loop is nested and, as such, creates the set of checkboxes for each article. ng-checked
is used to ensure that the underlying state of the HTML checkbox element is kept in sync with the model (this is very similar to ng-model
). ng-click
is used to invoke toggleCategory
with the appropriate article and category.
If you try this out, you should see the percentage of categories used being updated as you categorize articles.