Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($injector): ability to load new modules after bootstrapping
Browse files Browse the repository at this point in the history
The new method `$injector.loadNewModules(modules)` will add each of the
injectables to the injector and execute all of the config and run blocks
for each module passed to the method.

* The application developer is responsible for loading the code containing
the modules.
* Modules cannot be unloaded.
* Previously loaded modules will not be reloaded.
* Previously compiled HTML will not be affected by newly loaded directives,
filters and components.
  • Loading branch information
Craig Leyshan authored and petebacondarwin committed Sep 21, 2017
1 parent 6a99eaf commit 6e78fee
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 2 deletions.
46 changes: 45 additions & 1 deletion src/auto/injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,46 @@ function annotate(fn, strictDi, name) {
*
* @returns {Array.<string>} The names of the services which the function requires.
*/

/**
* @ngdoc method
* @name $injector#loadNewModules
*
* @description
*
* **This is a dangerous API, which you use at your own risk!**
*
* Add the specified modules to the current injector.
*
* This method will add each of the injectables to the injector and execute all of the config and run
* blocks for each module passed to the method.
*
* If a module has already been loaded into the injector then it will not be loaded again.
*
* * The application developer is responsible for loading the code containing the modules; and for
* ensuring that lazy scripts are not downloaded and executed more often that desired.
* * Previously compiled HTML will not be affected by newly loaded directives, filters and components.
* * Modules cannot be unloaded.
*
* You can use {@link $injector#modules `$injector.modules`} to check whether a module has been loaded
* into the injector, which may indicate whether the script has been executed already.
*
* ## Example
*
* Here is an example of loading a bundle of modules, with a utility method called `getScript`:
*
* ```javascript
* app.factory('loadModule', function($injector) {
* return function loadModule(moduleName, bundleUrl) {
* return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
* };
* })
* ```
*
* @param {Array<String|Function|Array>=} mods an array of modules to load into the application.
* Each item in the array should be the name of a predefined module or a (DI annotated)
* function that will be invoked by the injector as a `config` block.
* See: {@link angular.module modules}
*/


/**
Expand Down Expand Up @@ -701,6 +740,11 @@ function createInjector(modulesToLoad, strictDi) {
instanceInjector.strictDi = strictDi;
forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });

instanceInjector.loadNewModules = function(mods) {
forEach(loadModules(mods), function(fn) { if (fn) instanceInjector.invoke(fn); });
};


return instanceInjector;

////////////////////////////////////
Expand Down
160 changes: 159 additions & 1 deletion test/auto/injectorSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('injector.modules', function() {
.info({ version: '1.2' })
.provider('test', ['$injector', function($injector) {
providerInjector = $injector;
return { $get: function() {} };
return {$get: function() {}};
}]);
module('test1');
// needed to ensure that the provider blocks are executed
Expand Down Expand Up @@ -152,6 +152,164 @@ describe('injector', function() {
expect($injector).not.toBe(providerInjector);
}));


describe('loadNewModules', function() {
it('should be defined on $injector', function() {
var injector = createInjector([]);
expect(injector.loadNewModules).toEqual(jasmine.any(Function));
});

it('should allow new modules to be added after injector creation', function() {
angular.module('initial', []);
var injector = createInjector(['initial']);
expect(injector.modules['initial']).toBeDefined();
expect(injector.modules['lazy']).toBeUndefined();
angular.module('lazy', []);
injector.loadNewModules(['lazy']);
expect(injector.modules['lazy']).toBeDefined();
});

it('should execute runBlocks of new modules', function() {
var log = [];
angular.module('initial', []).run(function() { log.push('initial'); });
var injector = createInjector(['initial']);
log.push('created');

angular.module('a', []).run(function() { log.push('a'); });
injector.loadNewModules(['a']);
expect(log).toEqual(['initial', 'created', 'a']);
});

it('should execute configBlocks of new modules', function() {
var log = [];
angular.module('initial', []).config(function() { log.push('initial'); });
var injector = createInjector(['initial']);
log.push('created');

angular.module('a', [], function() { log.push('config1'); }).config(function() { log.push('config2'); });
injector.loadNewModules(['a']);
expect(log).toEqual(['initial', 'created', 'config1', 'config2']);
});

it('should execute runBlocks and configBlocks in the correct order', function() {
var log = [];
angular.module('initial', [], function() { log.push(1); })
.config(function() { log.push(2); })
.run(function() { log.push(3); });
var injector = createInjector(['initial']);
log.push('created');

angular.module('a', [], function() { log.push(4); })
.config(function() { log.push(5); })
.run(function() { log.push(6); });
injector.loadNewModules(['a']);
expect(log).toEqual([1, 2, 3, 'created', 4, 5, 6]);
});

it('should load dependent modules', function() {
angular.module('initial', []);
var injector = createInjector(['initial']);
expect(injector.modules['initial']).toBeDefined();
expect(injector.modules['lazy1']).toBeUndefined();
expect(injector.modules['lazy2']).toBeUndefined();
angular.module('lazy1', ['lazy2']);
angular.module('lazy2', []);
injector.loadNewModules(['lazy1']);
expect(injector.modules['lazy1']).toBeDefined();
expect(injector.modules['lazy2']).toBeDefined();
});

it('should execute blocks of new modules in the correct order', function() {
var log = [];
angular.module('initial', []);
var injector = createInjector(['initial']);

angular.module('lazy1', ['lazy2'], function() { log.push('lazy1-1'); })
.config(function() { log.push('lazy1-2'); })
.run(function() { log.push('lazy1-3'); });
angular.module('lazy2', [], function() { log.push('lazy2-1'); })
.config(function() { log.push('lazy2-2'); })
.run(function() { log.push('lazy2-3'); });

injector.loadNewModules(['lazy1']);
expect(log).toEqual(['lazy2-1', 'lazy2-2', 'lazy1-1', 'lazy1-2', 'lazy2-3', 'lazy1-3']);
});

it('should not reload a module that is already loaded', function() {
var log = [];
angular.module('initial', []).run(function() { log.push('initial'); });
var injector = createInjector(['initial']);
expect(log).toEqual(['initial']);

injector.loadNewModules(['initial']);
expect(log).toEqual(['initial']);

angular.module('a', []).run(function() { log.push('a'); });
injector.loadNewModules(['a']);
expect(log).toEqual(['initial', 'a']);
injector.loadNewModules(['a']);
expect(log).toEqual(['initial', 'a']);

angular.module('b', ['a']).run(function() { log.push('b'); });
angular.module('c', []).run(function() { log.push('c'); });
angular.module('d', ['b', 'c']).run(function() { log.push('d'); });
injector.loadNewModules(['d']);
expect(log).toEqual(['initial', 'a', 'b', 'c', 'd']);
});

it('should be able to register a service from a new module', function() {
var injector = createInjector([]);
angular.module('a', []).factory('aService', function() {
return {sayHello: function() { return 'Hello'; }};
});
injector.loadNewModules(['a']);
injector.invoke(function(aService) {
expect(aService.sayHello()).toEqual('Hello');
});
});


it('should be able to register a controller from a new module', function() {
var injector = createInjector(['ng']);
angular.module('a', []).controller('aController', function($scope) {
$scope.test = 'b';
});
injector.loadNewModules(['a']);
injector.invoke(function($controller) {
var scope = {};
$controller('aController', {$scope: scope});
expect(scope.test).toEqual('b');
});
});


it('should be able to register a filter from a new module', function() {
var injector = createInjector(['ng']);
angular.module('a', []).filter('aFilter', function() {
return function(input) { return input + ' filtered'; };
});
injector.loadNewModules(['a']);
injector.invoke(function(aFilterFilter) {
expect(aFilterFilter('test')).toEqual('test filtered');
});
});


it('should be able to register a directive from a new module', function() {
var injector = createInjector(['ng']);
angular.module('a', []).directive('aDirective', function() {
return {template: 'test directive'};
});
injector.loadNewModules(['a']);
injector.invoke(function($compile, $rootScope) {
var elem = $compile('<div a-directive></div>')($rootScope); // compile and link
$rootScope.$digest();
expect(elem.text()).toEqual('test directive');
elem.remove();
});
});
});

it('should have a false strictDi property', inject(function($injector) {
expect($injector.strictDi).toBe(false);
}));
Expand Down

0 comments on commit 6e78fee

Please sign in to comment.