8000 feat($injector): ability to load new modules after bootstrapping by petebacondarwin · Pull Request #16224 · angular/angular.js · GitHub
[go: up one dir, main page]

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

feat($injector): ability to load new modules after bootstrapping #16224

Merged

Conversation

petebacondarwin
Copy link
Contributor

The new method $injector.loadNewModules(modules) will add each of the
injectables to the injector and execute all of the 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 compiled HTML will not be affected by newly loaded directives,
    filters and components.

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

What is the current behavior? (You can also link to an open issue here)

What is the new behavior (if this is a feature change)?

Does this PR introduce a breaking change?

Please check if the PR fulfills these requirements

Other information:

@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for the commit author(s). If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

1 similar comment
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for the commit author(s). If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

@petebacondarwin
Copy link
Contributor Author

The original commit came from #4694 and the author did sign CLA.

Copy link
Member
@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments. LGTM overall.

*
* @description
*
* **This is an dangerous API, which you use at your own risk!**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an --> a

* });
* ```
*
* @param {Module[]} mods A collection of modules to load.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this should be an array of strings. (We also support functions, but this seems to be an undocumented feature.)
We also use the format Array.<...> for array arguments.

* Add the specified modules to the current injector.
*
* This method will add each of the injectables to the injector and execute all of the run blocks
* for each module passed to the method.
Copy link
Member
@gkalpak gkalpak Sep 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might (or might not) be worth also mentioning that:

  • Config blocks are executed as well. (Since we explicitly mention run blocks, it might sounds like config blockas are not executed.)

  • Loading already loaded modules has no effect (and their config/run blocks aren't executed either).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blockas -> blocks :-P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also test what happens with config blocks eh?

* Here is an example of loading a bundle of modules, with a utility method called `getScript`:
*
* ```javascript
* app.service('AmdLoader', function ($injector, $log) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service --> factory (?)
(service works but can be confusing for beginners.)

* $log.debug('AMD >> Loading Angular module [' + moduleName + '] ...');
*
* if (config.modules) {
* $injector.loadNewModules(config.modules);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this? Shouldn't the lazy-loaded module depend on these (as in angular.module('someModule', [...dependent modules here...])), in which case loadNewModules would automatically load them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to be honest, this was a bit of a cut-n-paste job.

* $injector.loadNewModules([moduleName]);
* $log.debug('AMD >> Angular module [' + moduleName + '] loaded.');
* }
* });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I find the example unnecessarily complicated. I would expect something like:

app.factory('loadModule', function($injector) {
  return function loadModule(moduleName, bundleUrl) {
    return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
  };
})

angular.module('b', ['a'], function() { log += 'b'; }).run(function() { log += 'B'; });
injector.loadNewModules([
'b',
valueFn(function() { log += 'C'; }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add some config fns here two 😁


it('should be able to register a service from a new module', function() {
var injector = createInjector([]);
angular.module('a');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean angular.module('a', [])?

@@ -701,6 +747,11 @@ function createInjector(modulesToLoad, strictDi) {
instanceInjector.strictDi = strictDi;
forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth putting this in a method to avoid duplicating it a few lines down?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original PR the loadNewModules was reused at application bootstrap, but this no longer works since we must modify the instanceInjector variable between loading the modules and running the run blocks.

  instanceInjector = protoInstanceInjector.get('$injector');

Are you suggesting that we just make a 'executeRunBlocks` method:

function executeRunBlocks(blocks) {
  forEach(blocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
}

which we then reuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to save much and there is very little logic involved so I am inclined not to bother.

@petebacondarwin
Copy link
Contributor Author

I have reworked following the comments.

@petebacondarwin petebacondarwin force-pushed the module-loading-after-bootstrap branch from 8a44f9c to b1c8a43 Compare September 14, 2017 20:16
Copy link
Member
@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One real (docs related) comment and a handful of nitpicks 😁
LGTM otherwise.

💯 for the added tests btw

* ```javascript
* app.factory('loadModule', function($injector) {
* return function loadModule(moduleName, bundleUrl) {
* return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something got really wrong with the indentation in the three lines above 😛

* })
* ```
*
* @param {Array<String|Function|Array>=} mods an array of modules to load into the application.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the ability to pass functions/arrays as modules (e.g. in angular.module()) is undocumented. I think it makes sense to be consistent between angular.module() and loadNewModules().
(Either documenting or undocumenting in both places is fine by me.)

Also, nit: In most places we use Array.<...> (notice the dot after Array) 😇

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cut and pasted this from the angular.bootstrap() API code: https://github.com/angular/angular.js/blob/master/src/Angular.js#L1773

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞 - I am not going to block the PR for that then 😛

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then why does it say mods instead of modules? 😛

* @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}*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the trailing * intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no

expect(log).toEqual(['initial', 'created', 'config1', 'config2']);
10000 });

it('should execute run blocks and config blocks in the correct order', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent wording: run/config blocks (vs run/configBlocks above) 😱


it('should execute run blocks and config blocks in the correct order', function() {
var log = [];
angular.module('initial', [], function() { log.push(1)})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon and space inside the function. 💣

it('should be able to register a service from a new module', function() {
var injector = createInjector([]);
angular.module('a', []);
angular.module('a').factory('aService', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason to have these two as separate lines (here and below) 😁

var injector = createInjector([]);
angular.module('a', []);
angular.module('a').factory('aService', function() {
return { sayHello: function() { return 'Hello'; } };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no space after/before {/} for objects 🏄

});
injector.loadNewModules(['a']);
injector.invoke(function($filter) {
var filter = $filter('aFilter');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save a line with injector.invoke(function(aFilterFilter) { :bowtie:
(Bonus points for the hidden filterFilter reference 😛)

var scope = $rootScope.$new();
var html = '<div a-directive></div>';
var elem = angular.element(html);
$compile(elem)(scope); // compile and link
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save up to three(3) lines with:

var elem = $compile('<div a-directive></div>')($rootScope);
$rootScope.$digest();

🌿

var injector = createInjector(['ng']);
angular.module('a', []);
angular.module('a').directive('aDirective', function() {
return { template: 'test directive' };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Way too much whitespace inside object 😇

@petebacondarwin petebacondarwin force-pushed the module-loading-after-bootstrap branch from b1c8a43 to 747384a Compare September 16, 2017 19:18
Copy link
Member
@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (except for a couple of indentation issues - including #16224 (comment) 😁)

var injector = createInjector(['initial']);
log.push('created');

angular.module('a', [], function() { log.push(4);})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing whitespace after ; 🔢

Copy link
Contributor
@Narretz Narretz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume one of the common use cases is to load additional modules when the user navigates inside the application, i.e. before a route is loaded.

This means the application must download the module and load it, but only for the first time (as the modules cannot be unloaded).

I don't see a mechanism for checking if a module has been loaded already. We don't expose the list in the injector, either. Should we add a convenience method hasModule() ?
You could always check if a specific service is in the DI, but this won't work for modules that only load components etc.
It's not enough that the injector won't load a module twice either, it's the download that must be avoided. Correct caching could handle that ofc.

Georgios pointed out that we have https://docs.angularjs.org/api/auto/service/$injector#modules for that

* ```javascript
* app.factory('loadModule', function($injector) {
* return function loadModule(moduleName, bundleUrl) {
* return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use fetch in-place here for getScript. Only thing that might happen is that people use it and complain that it doesn't work in IE9 ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Narretz I think the difference is that getScript also executes the script.

* })
* ```
*
* @param {Array<String|Function|Array>=} mods an array of modules to load into the application.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then why does it say mods instead of modules? 😛

@gkalpak
Copy link
Member
gkalpak commented Sep 18, 2017

I don't see a mechanism for checking if a module has been loaded already. We don't expose the list in the injector, either.

Doesn't $injector.modules cover that?

@Narretz
Copy link
Contributor
Narretz commented Sep 18, 2017

It does, thanks. I forgot we added that. Maybe we could add a note about the property to the docs.

@petebacondarwin
Copy link
Contributor Author
petebacondarwin commented Sep 18, 2017

@Narretz do you mean add a link to the modules property docs in the docs for the loadNewModules method?

@Narretz
Copy link
Contributor
Narretz commented Sep 18, 2017

@petebacondarwin that's right

@petebacondarwin petebacondarwin force-pushed the module-loading-after-bootstrap branch from 747384a to 860f9e9 Compare September 18, 2017 21:14
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.
@petebacondarwin petebacondarwin force-pushed the module-loading-after-bootstrap branch from 860f9e9 to 8186065 Compare September 21, 2017 12:52
@petebacondarwin petebacondarwin merged commit 34237f9 into angular:master Sep 21, 2017
@petebacondarwin
Copy link
Contributor Author

Cherry picked to 1.6.x as 6e78fee

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants 3956
0