Showing posts with label angularjs. Show all posts
Showing posts with label angularjs. Show all posts

Monday, December 23, 2013

Data binding in AngularJS

One of the most important things in AngularJS is the data binding. In fact I think it is so important it should be one of the first things you should get to know if you start to write Angular apps. It should be explained more and it shouldn't be just "the magic that we won't cover right now".

The real magic behind Angular's data binding is called dirty checking. Basically every time you write {{myModel}} in the background a new watch for the expression myModel is being created that on every $digest cycle will be evaluated and if the value of the model is changed, the DOM will be updated. Thanks to this dirty checking we are also able to use plain old JS object as our model. How data binding works is explained in detail in this great article - http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/. I also recommend to read Misko's answer to this StackOverflow question on why dirty checking is not a bad thing, unless of course it is used in a bad way. This is why we should understand how data binding works.

An example of such a bad use would be to bind to a function that makes some complex computations. In this example not so complex, but lets have have an input that will represent a value for radius of a circle and we will calculate it's area and show it. We can create a function and bind to it
However if you look at the console output, you will see that on every radius change, it is called twice, once on the first loop of the $digest cycle and because the area value was changed it will called once again in the second loop. We can even bind to this function in multiple places in the view, and unlike I expected it to work, for each one of them it will add a watch in the $watch list, and the function will be called multiple times. For example if we add
<div>Area {{calculateArea()}}</div>
on 3 places, then when the value of radius is changed, calculateArea() will be called 2*3=6 times. So if this was computing something more complex it would really be an issue.

 We can optimize this by listening the radius for change, compute the area based on the new radius value and assign the result to the scope. That way the computation will be done only once.
Of course if we include
<div>Area {{area}}</div>
 again the 3 watches will cause the evaluation of the area model value, but since it is assigned to the scope it will be much faster.

The book Mastering Web Application Development with AngularJS explains really well how data binding works, how the DOM and model are synchronized, what is scope.$apply, how $watches are registered and how the $digest cycle executes. It also has a nice step by step explanation of how the 2-way data binding is achieved in Angular. A must-read book!

Wednesday, November 27, 2013

Animate ngView transitions in AngularJS

Let me just start this post by saying that AngularJS is awesome! I have been playing with Angular recently and I must say that it has the things I loved in Flex and I missed in JavaScript frameworks. Things like declarative UI, bi-directional binding and a lot more, but now I will not write why Angular is cool.

This post will be covering something that I recently wanted to implement, but it seemed that it not so straight forward - animating ng-views when they change.

http://maverix7.appspot.com/files/ngview/index.html#/viewA


An example of such transitions you can check here  http://code.angularjs.org/1.1.4/docs/api/ng.directive:ngView#animations . As you can see when you click on a link, the view is changed by making this slick slide animation. One thing that we notice is that the view is sliding only in one direction, but it will be really nice to have it both ways, based on the hierarchy of the views.

In this great post http://blog.revolunet.com/blog/2013/04/30/angularjs-animations-mobile-applications/ we can see how we can make the sliding bi-directional, and it is looking much better.

However we also want the app to respond to browser's Back and Forward button, so we have to handle route changes and switch the animation CSS class based on where we were now and where are we going, so we can have this next and previous interaction.

In AngularJS this is easy we just add a route change handler

var oldLocation = '';
$scope.$on('$routeChangeStart', function(angularEvent, next) {
  console.log("routeChangeStart");
  var isDownwards = true;
  if (next && next.$$route) {
    var newLocation = next.$$route.originalPath;
    if (oldLocation !== newLocation && oldLocation.indexOf(newLocation) !== -1) {
      isDownwards = false;
    }
    
    oldLocation = newLocation;
  }
  
  $scope.isDownwards = isDownwards;

And having the following set for our view
<div ng-view ng-class="{slide: true, left: isDownwards, right: !isDownwards}"></div>

We have some kind of sliding animation when we switch between views. But there is a "gotcha". When the view is changed, the old view slides to the opposite direction, say "right" and disappears, then the new view slides from the "right" direction, where we expect the old view to slide to the "left" and disappear and the new view to appear and slide from the "right". This is because both the switching of views and assigning of "isDownwards" to the model is done in the same $digest cycle, and therefore the old view, when removed, does not have the new direction class applied, and is animated to the opposite (old) direction.

So in order to fix this we have to switch the views, after the cycle in which the "isDownwards" is applied to the model. AFAIK to invoke something after the current digest cycle you can invoke $timeout and pass 0 for delay. And to delay the view switching, we can pass a resolve map to the route parameter of $routeProvider, and if any of it's dependencies return a promise, Angular will wait until this promise is resolved. So our code for this will look like:

var resolve = {
  delay: function($q, $timeout) {
    var delay = $q.defer();
    $timeout(delay.resolve, 0, false);
    return delay.promise;
  }
};

angular.module('viewTransitionApp', ['ngRoute', 'ngAnimate'])
  .config(function ($routeProvider) {
    $routeProvider
      .when('/viewA', {
        templateUrl: 'viewA.html',
        resolve: resolve
      });
  });


Having all this, we can have an application with views that when switched are animated in a given direction, and this direction is based on the current location, and the location that we are changing to. Of course this sliding animation can be changed with any other animation that makes sense to have forward and backward behavior.

The final example can be seen here:
http://maverix7.appspot.com/files/ngview/index.html

You can right click and view its source or see this gist https://gist.github.com/tgeorgiev/7667648