From 603fe0d19608ffe1915d8bc23bf412912e7ee1ac Mon Sep 17 00:00:00 2001 From: Julie Date: Thu, 21 Feb 2013 11:55:16 -0800 Subject: [PATCH] feat(angular.bootstrap): support deferred bootstrap This features enables tools like Batarang and test runners to hook into angular's bootstrap process and sneak in more modules into the DI registry which can replace or augment DI services for the purpose of instrumentation or mocking out heavy dependencies. If window.name contains prefix NG_DEFER_BOOTSTRAP! when angular.bootstrap is called, the bootstrap process will be paused until angular.resumeBootstrap is called. angular.resumeBootstrap takes an optional array of modules that should be added to the original list of modules that the app was about to be bootstrapped with. --- src/Angular.js | 50 +++++++++++++++++++----------- test/AngularSpec.js | 74 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 5de57075eb5c..5d9d2e12fffe 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -58,7 +58,7 @@ var /** holds major version number for IE or NaN for real browsers */ toString = Object.prototype.toString, - _angular = window.angular, + _angular = window.angular, /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, @@ -964,22 +964,38 @@ function angularInit(element, bootstrap) { * @returns {AUTO.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { - element = jqLite(element); - modules = modules || []; - modules.unshift(['$provide', function($provide) { - $provide.value('$rootElement', element); - }]); - modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke( - ['$rootScope', '$rootElement', '$compile', '$injector', function(scope, element, compile, injector){ - scope.$apply(function() { - element.data('$injector', injector); - compile(element)(scope); - }); - }] - ); - return injector; + var resumeBootstrapInternal = function() { + element = jqLite(element); + modules = modules || []; + modules.unshift(['$provide', function($provide) { + $provide.value('$rootElement', element); + }]); + modules.unshift('ng'); + var injector = createInjector(modules); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function(scope, element, compile, injector) { + scope.$apply(function() { + element.data('$injector', injector); + compile(element)(scope); + }); + }] + ); + return injector; + }; + + var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { + return resumeBootstrapInternal(); + } + + window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); + angular.resumeBootstrap = function(extraModules) { + forEach(extraModules, function(module) { + modules.push(module); + }); + resumeBootstrapInternal(); + }; } var SNAKE_CASE_REGEXP = /[A-Z]/g; diff --git a/test/AngularSpec.js b/test/AngularSpec.js index cc752b6c00bd..7f1ab8a32c26 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -658,7 +658,7 @@ describe('angular', function() { var element = jqLite('
{{1+2}}
'); var injector = angular.bootstrap(element); expect(injector).toBeDefined(); - expect(element.data('$injector')).toBe(injector); + expect(element.injector()).toBe(injector); dealoc(element); }); @@ -672,6 +672,78 @@ describe('angular', function() { expect(element.html()).toBe('{{1+2}}'); dealoc(element); }); + + + describe('deferred bootstrap', function() { + var originalName = window.name, + element; + + beforeEach(function() { + window.name = ''; + element = jqLite('
{{1+2}}
'); + }); + + afterEach(function() { + dealoc(element); + window.name = originalName; + }); + + + it('should wait for extra modules', function() { + window.name = 'NG_DEFER_BOOTSTRAP!'; + angular.bootstrap(element); + + expect(element.html()).toBe('{{1+2}}'); + + angular.resumeBootstrap(); + + expect(element.html()).toBe('3'); + expect(window.name).toEqual(''); + }); + + + it('should load extra modules', function() { + element = jqLite('
{{1+2}}
'); + window.name = 'NG_DEFER_BOOTSTRAP!'; + + var bootstrapping = jasmine.createSpy('bootstrapping'); + angular.bootstrap(element, [bootstrapping]); + + expect(bootstrapping).not.toHaveBeenCalled(); + expect(element.injector()).toBeUndefined(); + + angular.module('addedModule', []).value('foo', 'bar'); + angular.resumeBootstrap(['addedModule']); + + expect(bootstrapping).toHaveBeenCalledOnce(); + expect(element.injector().get('foo')).toEqual('bar'); + }); + + + it('should not defer bootstrap without window.name cue', function() { + angular.bootstrap(element, []); + angular.module('addedModule', []).value('foo', 'bar'); + + expect(function() { + element.injector().get('foo'); + }).toThrow('Unknown provider: fooProvider <- foo'); + + expect(element.injector().get('$http')).toBeDefined(); + }); + + + it('should restore the original window.name after bootstrap', function() { + window.name = 'NG_DEFER_BOOTSTRAP!my custom name'; + angular.bootstrap(element); + + expect(element.html()).toBe('{{1+2}}'); + + angular.resumeBootstrap(); + + expect(element.html()).toBe('3'); + expect(window.name).toEqual('my custom name'); + }); + }); });