Saturday, August 9, 2014

AngularJS hack/tip :: Invoking JS code after DOM is ready


When working with AngularJS, you frequently update the DOM after the DOM was already 'ready'.
What I mean by that is that the browser will load the DOM, and the template will completely load. BUT, your template might have an 'ng-if' or 'ng-repeat' directive that will only be attached to the DOM slightly after, since you might be setting it with an ajax response inside the control.

This will happen when your code is similar to this pattern :
app.controller('MyAngularController', function($scope, $http) {
    $http.get('www.someURL.com/api').success(function(response) {
        // Add some data to the scope
        $scope.Data = response;

        // This caused the DOM to change
        // so invoke some js that will take care of the new DOM changes
        DoSomeJS();
    });
});

The main problem with this code is that most of the time when the method DoSomeJS() is invoked, the DOM changes caused by the changes to $scope won't be 'ready'.

This is because the way angularJS is built -
Each property on the scope has a 'watcher' attached to it, checking it for changes. Once the property is changed, it invokes a '$digest' loop which is responsible for updating the model and view as well. This is invoked asynchronously (for performance reasons i guess), and this actually gives you the great ability of invoking js code immediately after updating the scope without waiting for the DOM to be updated - something you'll probably want as well from time to time. (The nitty gritty details of how this works behind the scenes is interesting, but will take me too long to go through in this post. For the brave ones among us, I encourage you to look a bit into the code yourself --> https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L667)


So, how can we invoke some JS code, and make sure it runs only after the DOM was updated ?
Well, one quick and hacky way to do this is to let a js timer invoke your code with a '0' delay. Since JS is single-threaded, running a timer with a 0ms delay doesn't always mean the JS runs immediately. What will happen in this case, it will push the code to 'the end of the line' and invoke it once the JS thread is ready.

The updated code looks like this :
app.controller('MyAngularController', function($scope, $http, $timeout) {
    $http.get('www.someURL.com/api').success(function(response) {
        // Add some data to the scope
        $scope.Data = response;

        // This caused the DOM to change
        // so invoke some js that will take care of the new DOM changes
        $timeout(DoSomeJS);
    });
});
Note: invoking '$timeout()' like we did is just like invoking 'setTimeout(fn, 0);' - $timeout is an angularJS service that wraps setTimeout.
A great read on how JS timers are invoked : Understanding Javascript timers

But wait, This whole solution is a hack, isn't it ?!...
Yes, and truth be told, when I first ran into this problem, this was the first solution I came up with. It was only after that I realized I don't want any js code in my controller touching my DOM.
I still decided to write this post though, to explain a little about JS timers and angular $digest.

The solution I would favor more in this case would be to have a custom directive on the DOM being inserted dynamically. Then adding the code modifying the DOM in the 'link' method of the directive.

And the code should look more like this :
app.directive('myDirective', function() {
    return {
        restrict: 'A',
        link: function(scope, elem, attrs) {
            // DO WHATEVER WE WANT HERE...
        }
    };
});

In angular directives describe various elements of the templates, and therefore I feel like they are the 'right' place for most of the code we need to modify our DOM. I like to keep my controllers clean from touching the DOM, and just have them construct the models they need to pass on to the template.