Aug 21, 2014 in AngularJS, Javascript, directive
After having worked with AngularJS for some time, I realized that there are some recipes I use frequently. Some of them are trivial, some not, but I think they might also interest other people. Hope you find it amusing and share your favorite tips in comments. Also, feel free to comment on errors and improvements. Let's go!
I'm going to open this article with short explanation of how $parsers
and $formatters
are used. No magic here however I think some people still are still a little confused with these two (well I was).
When you have a directive that you want to interact somehow with the same element's ngModel
, you can use $parsers an $formatters to control actual model value or how model is rendered in the view. You can do that because ngModel
can contain any value you need while it may render in the view totally differently. For example, you store an object as the model value, but you render a number in input field.
Good example would be an input field to enter datetime string in ISO format, but hold timestamp in its model. Such directive then could be written like this:
.directive('datetime', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
ngModelController.$parsers.push(function(value) {
return new Date(value).getTime();
});
ngModelController.$formatters.push(function(value) {
return new Date(value).toString();
});
}
};
});
See this demo of this directive here. So $parsers
parse string value from input field to model value, $formatters
format model value to render in view.
Despite all the magic Angular is still Javascript, so everything you know about vanila JS is also applicable to Angular. Take events. They still propagate like they always do, there are capturing and bubbling phases, you can use delegation, prevent default and stop event propagation.
Consider the following real-life example:
<div class="col-xs-2">
<button class="btn btn-link" ng-hide="!isCollapsed" ng-click="switchSearch()">Advanced Search</button>
<button class="btn btn-link" ng-hide="isCollapsed" ng-click="switchSearch()">Basic Search</button>
</div>
Note, how both buttons declare ngClick directive and call the same function on this event. Knowing how event propagates in DOM tree, we can reduce number of event handlers by one, and add up to a little optimization:
<div class="col-xs-2" ng-click="switchSearch()">
<button class="btn btn-link" ng-hide="!isCollapsed">Advanced Search</button>
<button class="btn btn-link" ng-hide="isCollapsed">Basic Search</button>
</div>
Result is going to be the same, switchSearch
function doesn't even care about actual event target. In this case, catching click event on parent container makes perfect sense.
Let's build a directive that will use bubbling for more effective event handling. Instead of many ngClick
directives we will use only one to handle all descendant's click events. For example, there is a table with multiple rows and cells. We want each cell to trigger click event. Typical approach to this would be to put ngClick
on each TD element:
<table class="table table-bordered table-condensed">
<tr ng-repeat="row in rows">
<td ng-repeat="cell in row.cells" ng-click="activate(cell)" ng-class="{active: active}">{{cell.text}}</td>
</tr>
</table>
And define event handler in controller:
$scope.activate = function(cell) {
cell.active = true;
};
However, in case of many columns and rows it may be cleaner to use only one ngClick
. It is possible due to event bubbling. Then revised code becomes:
<table class="table table-bordered table-condensed" ng-click="activate($event)">
<tr ng-repeat="row in rows">
<td ng-repeat="cell in row.cells" ng-class="{active: active}">{{cell.text}}</td>
</tr>
</table>
with controller
$scope.activate = function($event) {
var scope = angular.element($event.target).scope();
scope.cell.active = true;
};
Line angular.element($event.target).scope()
can look intimidating at first, but it's just a way to get hold of current child scope object. Yes, it's a little bit more verbose, but it has an advantage of having less directives and event handlers.
Event delegation pattern is even more effective and powerful when used inside custom directives.
You can see above example here.
$parse
service is usually used inside of custom directive. Here is an example of situation when $parse can be useful. Let's say we have the object $scope.adapters
with can potentially have several nested levels like this:
$scope.adapters = {
outbound: {
tcp: {
status: 'on'
}
}
};
If we want to set a status of p2r inbound adapter, we need to check that all previous levels exist:
if ($scope.adapters && $scope.adapters.inbound && $scope.adapters.inbound && $scope.adapters.inbound.p2r) {
$scope.adapters.inbound.p2r = false;
}
With $parse
service it can be simpler:
$parse('adapters.inbound.p2r.status').assign($scope, false);
However, this is somewhat specific situation.
So we are building a modern single page applications. It means that technically there are no such things as “pages” in AngularJS application. There is only one page always. What often happens is that developers forgot to change a title of the current screen. It’s not big dial and definitely not a problem, but still it would be nice if page title corresponding to currently rendered app section. Fortunately, it’s not hard to do with Angular.
I find the most natural way to configure titles to be providing it with route definition config. For example profile route would state that document title should be "App | Profile":
$routeProvider.when("/profile", {
controller: "profileController",
templateUrl: "profile/profile.html",
title: "App | Profile"
});
Angular however will not automatically pick up "title" and set it for us, so we need to add a little code to make it work as it should. Put this snippet in application run block or main controller:
.run(['$rootScope', '$route', function($rootScope, $route) {
$rootScope.$on('$routeChangeSuccess', function() {
document.title = $route.current.title;
});
}]);
While defining routes using $routeProvider
, it's typically recommended to configure redirectTo
property in otherwise
section of the route configuration. If route is not found, then application will redirect to, say home page. However you can still use controller
and templateUrl
properties too in otherwise
. For example:
$routeProvider.otherwise({
controller: "404Controller",
templateUrl: "404.html"
});
Here is a demo of how it can look:
Now you only need to make your 404 page look fresh and unique.
Here we go. Those are some tricks for today. Hope you found something useful in this article. Share your ideas, comments and improvements are very welcome, and productive coding to everyone!
comments powered by Disqus