Couple of AngularJS tricks

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!

1. $parsers and $formatters

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.

2. Event bubbling

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.

3. Using $parse service

$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.

4. Setting page title in route configuration

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; }); }]);

5. Page not found and $routeProvider

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:

Loading plunk...

Now you only need to make your 404 page look fresh and unique.

Conclusion

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!

Please enable JavaScript to view the comments powered by Disqus.

Disqus