-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(router): route initializer #36084
feat(router): route initializer #36084
Conversation
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
@googlebot I signed it! |
Add an injection token ROUTE_INITIALIZE to provide one or more initialization functions. These function are injected when a route is loaded. If any of these functions returns a Promise, initialization does not complete until the Promise is resolved. If the initialization is completed the LoadedRouterConfig will be returned.
Did some code refactoring after feedback from frederikprijck.
9a374ab
to
f87441b
Compare
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
Hey @atscott, have you had a chance to checkout this implementation yet? :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this provide value beyond what CanActivate
at the forChild
root could do?
RouterModule.forChild([
{path: '', canActivate: [MyInitializerGuard], children: [...]}
]);
The canActivate
there would allow for asynchronous initialization before attempting to activate any child routes in the module. If you think of canActivate
conceptually like "can this route activate yet?" rather than "is it allowed to activate at all?" it makes a bit more sense to do this.
if (this.onLoadEndListener) { | ||
this.onLoadEndListener(route); | ||
} | ||
|
||
const module = factory.create(parentInjector); | ||
const routeInits: (() => any)[] = module.injector.get(ROUTE_INITIALIZER, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type should be consistent with the injection token: Array<() => void>
here and in runInitializers
})); | ||
} | ||
|
||
private runInitializers(routeInits: (() => any)[] = []) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default value []
isn't necessary here.
})); | ||
} | ||
|
||
private runInitializers(routeInits: (() => any)[] = []) { | ||
return from(Promise.all(routeInits.map(routeInit => routeInit()).filter(isPromise))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be easier to understand if you extracted some variables. I see what you're doing here and I do like how clever it is, but it might more immediately understandable if it were deconstructed a bit.
return from(Promise.all(routeInits.map(routeInit => routeInit()).filter(isPromise))); | |
const initResults = routeInits.map(routeInit => routeInit()); | |
const promiseInits = initResults.filter(isPromise); | |
return from(Promise.all(promiseInits)); |
flatten(module.injector.get(ROUTES)).map(standardizeConfig), module); | ||
return this.runInitializers(routeInits) | ||
.pipe( | ||
map(() => new LoadedRouterConfig( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the right operator here? While from(Promise.all)
would only emit once, this code shouldn't rely on the implementation details of the runInitializers
function. I think this should instead be last
or even finalize
/finally
. What happens when an initializer rejects isn't really discussed here or tested (please add a test or two for it).
/** | ||
* The [DI token](guide/glossary/#di-token) for a router configuration. | ||
* @see `ROUTES` | ||
* @publicApi | ||
*/ | ||
export const ROUTES = new InjectionToken<Route[][]>('ROUTES'); | ||
|
||
/** | ||
* An injection token that allows you to provide one or more initialization functions. | ||
* These function are injected when a route is loaded. If any of these functions returns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* These function are injected when a route is loaded. If any of these functions returns | |
* These function are injected when a module is loaded. If any of these functions returns |
These initializers have more to do with the modules being lazy loaded than they do with a "Route". This documentation is a bit misleading and seems to imply the initializers are linked to Route
s and not the act of lazy loading the module they're provided in. It's maybe a subtle distinction, but I think it's important.
That said, maybe the token token name needs some more bikeshedding
* a Promise, initialization does not complete until the Promise is resolved. | ||
* | ||
* You can, for example, create a factory function that loads language data | ||
* or an external configuration, and provide that function to the `ROUTER_INITIALIZER` token. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* or an external configuration, and provide that function to the `ROUTER_INITIALIZER` token. | |
* or an external configuration, and provide that function to the `ROUTE_INITIALIZER` token. |
@@ -0,0 +1,183 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are good, but it would also be nice to have coverage in integration.spec.ts
to verify correct behavior during route activation.
import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core'; | ||
import {isPromise} from '@angular/core/src/util/lang'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be imported from @angular/core
like what's done in utils/collection.ts
@@ -6,19 +6,37 @@ | |||
* found in the LICENSE file at https://angular.io/license | |||
*/ | |||
|
|||
// TODO(i): switch to fromPromise once it's expored in rxjs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's remove this comment
@atscott I think the canActivate hook will be to late. The loading of the module depends on the loading strategy and can be preloaded. The case I need it for is to make sure that I can fetch configuration for example before the the module and it's dependencies are loaded, because they can depend on that configuration. |
Outside of preloading, CanLoad could be used in this case. With preloading, it seems like you could actually build this functionality into your preloader. I think you’re going to need to provide more concrete examples before we can consider moving this PR forward. It seems like the existing router guards allow for handling the behavior desired. This also does not resolve the linked issue since modules can be loaded without the router. |
@atscott never looked this way at it, you could have a point. The desired behavior is the same as the APP_INITIALIZER I want to have, and ideal it shouldn't matter if the module is loaded immediately or trough a lazy route. |
That's right, I was really just suggesting Here's an [over-engineered] example of how this could all work: |
Closing this PR per the comments mentioned above, namely:
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This is an initial POC to provide an initialization hook for lazy loaded Modules. Implementation is Open for Discussion!
Additional Docs will be added when a consent about the solution is made.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
A lazy route is now just loaded without being block by initialization logic
Issue Number: #17606
What is the new behavior?
A lazy route is now loaded with an option being block by initialization logic. If all initialization logic has run, the route gets loaded
Does this PR introduce a breaking change?
Other information
It introduces a new injection toke ROUTE_INITIALIZER.