AngularJS scope and inheritance – Love/Hate relationship

When I was working on the TickerSrv, I wanted to support task auto-unregister. To accomplish that, in the module’s run phase, I added register & unregister functionality to the $rootScope.

Since all scopes prototypically inherit from $rootScope, all had this functionality and the user could use this scope API to register or unregister tasks. More over, when the scope is destroyed, I can intercept the event and unregister the tasks automatically.


angular.module('jsbb.angularTicker', [])
    .run(function($rootScope, TickerSrv) {
        // add the register task to the $rootScope. This will allow for autoUnregister when the
        // scope is destroyed to prevent tasks from leaking.
        $rootScope.registerTickerTask = function(id, tickHandler, interval, delay, isLinear) {
            TickerSrv.register(id, tickHandler, interval, delay, isLinear);

            this.$on('$destroy', function() {
                TickerSrv.unregister(id);
            });
        };

        $rootScope.unregisterTickerTask = TickerSrv.unregister;
    });

Yay!

Well, no quite.

As it turns out, isolated scopes behave differently.

First, for those who are not familiar with isolated scopes, those are the scopes that directives get, when you specify scope: { ... }. This creates an “isolate” scope that does not prototypically inherit from $rootScope.

This poses a real problem for my TickerSrv auto-unregister functionality, since I can no longer rely on this API to exist on the scope.

So, I had to resort to a bit of a hack.

Isolated scopes are created by calling $rootScope.new(true), true stands for isolate, so an isolated scope will be created.
So, overriding the $rootScope.new() method was the simple and obvious solution.

Refactor effort No. 1:

angular.module('jsbb.angularTicker', [])
    .run(function($rootScope, TickerSrv) {
        // add the register task to the $rootScope. This will allow for autoUnregister when the
        // scope is destroyed to prevent tasks from leaking.
        $rootScope.registerTickerTask = function(id, tickHandler, interval, delay, isLinear) {
            TickerSrv.register(id, tickHandler, interval, delay, isLinear);

            this.$on('$destroy', function() {
                TickerSrv.unregister(id);
            });
        };

        $rootScope.unregisterTickerTask = TickerSrv.unregister;

        // since isolated scopes do not inherit prototypically from $rootScope, we need to override $new
        // and add the functionality manually.
        $rootScope.$origNew = $rootScope.$new;

        $rootScope.$new = function(isolate, parent) {
            var newScope = this.$origNew(isolate, parent);

            if (isolate) {
                newScope.unregisterTickerTask = $rootScope.unregisterTickerTask;
                newScope.registerTickerTask = $rootScope.registerTickerTask;
            }

            return newScope;
        };
    });


However, this is still not enough!!!

Turns out, that isolated scopes that have a parent isolated scope, are created by calling the $new function on the parent isolated scope and not on the $rootScope.
So, for this test to pass:

            // new scope from scope
            expect(scope.$new().registerTickerTask).toBeDefined();

            // new isolated scope from scope
            expect(scope.$new(true).registerTickerTask).toBeDefined();

            // new scope from isolated scope
            expect(isolatedScope.$new().registerTickerTask).toBeDefined();

            // new isolated scope from isolated scope
            expect(isolatedScope.$new(true).registerTickerTask).toBeDefined();

The refactor effort No. 1 code doesn’t cut it.
Isolated scopes created from an isolated scope will not have the scope API and will fail the test.

Refactor effort No. 2:

angular.module('jsbb.angularTicker', [])
    .run(function($rootScope, TickerSrv) {
        // add the register task to the $rootScope. This will allow for autoUnregister when the
        // scope is destroyed to prevent tasks from leaking.
        $rootScope.registerTickerTask = function(id, tickHandler, interval, delay, isLinear) {
            TickerSrv.register(id, tickHandler, interval, delay, isLinear);

            this.$on('$destroy', function() {
                TickerSrv.unregister(id);
            });
        };

        $rootScope.unregisterTickerTask = TickerSrv.unregister;

        // since isolated scopes do not inherit prototypically from $rootScope, we need to override $new
        // and add the functionality manually.
        function applyScopeApi (targetScope) {
            if (!targetScope.$origNew) {
                targetScope.$origNew = targetScope.$new;
            }

            return function(isolate, parent) {
                var newScope = targetScope.$origNew(isolate, parent);

                if (isolate) {
                    newScope.unregisterTickerTask = $rootScope.unregisterTickerTask;
                    newScope.registerTickerTask = $rootScope.registerTickerTask;
                }

                newScope.$new = applyScopeApi(newScope);
                return newScope;
            };

        }

        $rootScope.$new = applyScopeApi($rootScope);

    });

This solution overrides $rootScope’s $new function with the function returned by the applyScopeApi function and will apply the scope API for all isolated scopes regardless of their parent.

* If you want to read more about scope inheritance, an excellent explanation on scopes and their inheritance can be found here.
I encourage you to read it, you’d be a better AngularJS developer for it.

You are welcomed to head over to the GitHub repository here, and read the README for the complete documentation on the TickerSrv.

As always, feel free to use and abuse 🙂
If you modify jsBlackBelt code, please let me know or submit a pull request for the benefit of others.

Leave a comment