OdeToCode IC Logo

Debugging AngularJS Data Binding

Tuesday, May 27, 2014

One common question I’ve gotten about AngularJS revolves around the debugging of binding expressions like {{ message }}. How does one track down expressions that aren’t working?

There are two approaches I can recommend. One approach is a tool, the other approach is a service decorator.

Batarang

Batarang is a fairly well known Chrome extension. Once loaded, the extension can show you the contents of scope objects, as well as profiling and performance information.

image

For some problems Batarang requires too many mouse clicks, that’s when I like to use a simple service decorator.

Decorating $interpolate

In Angular, the $interpolate service is responsible for working with binding expression. The service returns a function you can invoke against a scope object to produce an interpolated string. The function is known as an interpolation function.

As an example, the following code will log “HELLO!” to the console.

app.run(function($interpolate){
    var fn = $interpolate("{{message | uppercase}}");
    var result = fn({message:"Hello!"});
    console.log(result);
});

The interpolation function is forgiving. For most scenarios the forgiveness is good, because we can write binding expressions in a template without worrying about null or undefined values.

As an example, the following code does not throw an exception or give any indication of an error even though message is misspelled as massage in the binding expression.

app.run(function($interpolate){
    var fn = $interpolate("{{massage.length}}");
    var result = fn({message:"Hello!"});
    console.log(result);
});

Instead of an error, the interpolation function yields an empty string. These are the types of cases we might want to know about. Unfortunately, the functions interpreting and executing the binding expressions are hidden and have many special case branches. However, we can decorate the $interpolate service and wrap the interpolation functions it produces.

app.config(function($provide){
    $provide.decorator("$interpolate", function($delegate){

        var interpolateWrap = function(){
            var interpolationFn = $delegate.apply(this, arguments);
            if(interpolationFn) {
                return interpolationFnWrap(interpolationFn, arguments);
            }
        };

        var interpolationFnWrap = function(interpolationFn, interpolationArgs){
            return function(){
                var result = interpolationFn.apply(this, arguments);
                var log = result ? console.log : console.warn;
                log.call(console, "interpolation of  " + interpolationArgs[0].trim(), 
                                  ":", result.trim());
                return result;
            };
        };

        angular.extend(interpolateWrap, $delegate);
        return interpolateWrap;

    });
});

The wrapping function will log every execution that produces a string and warn about every interpolation that produces a false looking empty string (which is what happens if there is a type or undefined member on scope).

Now let’s see what happens with the following markup in a template. Note that foobar is not a property on scope and represents a bad binding expression we want to catch.

<span>{{ message }}</span>
<span>{{1 + 2}}</span>
<span>{{getMessage() | uppercase}}</span>
<span>{{foobar}}</span>

<div ng-repeat="i in [0,1,2,3]">
    {{i * 2}}
</div>

The output in the developer tools looks like the following.

Warning On Bad Angular Bindings

Now we can see all the interpolation activity on a page, and the activity producing empty strings will stand out. You probably don’t want to ship with an active decorator like the one in this post, but it can make some debugging chores quick and easy.