fix(router): incorrect injector is used when instantiating components loaded lazily (#12817)
This commit is contained in:
parent
69dfcf7385
commit
52be848f94
|
@ -264,7 +264,7 @@ class ApplyRedirects {
|
||||||
|
|
||||||
private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
return of (new LoadedRouterConfig(route.children, injector, null));
|
return of (new LoadedRouterConfig(route.children, injector, null, null));
|
||||||
} else if (route.loadChildren) {
|
} else if (route.loadChildren) {
|
||||||
return mergeMap.call(runGuards(injector, route), (shouldLoad: any) => {
|
return mergeMap.call(runGuards(injector, route), (shouldLoad: any) => {
|
||||||
if (shouldLoad) {
|
if (shouldLoad) {
|
||||||
|
@ -281,7 +281,7 @@ class ApplyRedirects {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return of (new LoadedRouterConfig([], injector, null));
|
return of (new LoadedRouterConfig([], injector, null, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,9 @@ export class RouterOutlet implements OnDestroy {
|
||||||
|
|
||||||
ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this.name ? this.name : PRIMARY_OUTLET); }
|
ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this.name ? this.name : PRIMARY_OUTLET); }
|
||||||
|
|
||||||
|
get locationInjector(): Injector { return this.location.injector; }
|
||||||
|
get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; }
|
||||||
|
|
||||||
get isActivated(): boolean { return !!this.activated; }
|
get isActivated(): boolean { return !!this.activated; }
|
||||||
get component(): Object {
|
get component(): Object {
|
||||||
if (!this.activated) throw new Error('Outlet is not activated');
|
if (!this.activated) throw new Error('Outlet is not activated');
|
||||||
|
@ -74,9 +77,8 @@ export class RouterOutlet implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
activate(
|
activate(
|
||||||
activatedRoute: ActivatedRoute, loadedResolver: ComponentFactoryResolver,
|
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector,
|
||||||
loadedInjector: Injector, providers: ResolvedReflectiveProvider[],
|
providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {
|
||||||
outletMap: RouterOutletMap): void {
|
|
||||||
if (this.isActivated) {
|
if (this.isActivated) {
|
||||||
throw new Error('Cannot activate an already activated outlet');
|
throw new Error('Cannot activate an already activated outlet');
|
||||||
}
|
}
|
||||||
|
@ -86,15 +88,8 @@ export class RouterOutlet implements OnDestroy {
|
||||||
|
|
||||||
const snapshot = activatedRoute._futureSnapshot;
|
const snapshot = activatedRoute._futureSnapshot;
|
||||||
const component: any = <any>snapshot._routeConfig.component;
|
const component: any = <any>snapshot._routeConfig.component;
|
||||||
|
const factory = resolver.resolveComponentFactory(component);
|
||||||
|
|
||||||
let factory: ComponentFactory<any>;
|
|
||||||
if (loadedResolver) {
|
|
||||||
factory = loadedResolver.resolveComponentFactory(component);
|
|
||||||
} else {
|
|
||||||
factory = this.resolver.resolveComponentFactory(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
const injector = loadedInjector ? loadedInjector : this.location.parentInjector;
|
|
||||||
const inj = ReflectiveInjector.fromResolvedProviders(providers, injector);
|
const inj = ReflectiveInjector.fromResolvedProviders(providers, injector);
|
||||||
this.activated = this.location.createComponent(factory, this.location.length, inj, []);
|
this.activated = this.location.createComponent(factory, this.location.length, inj, []);
|
||||||
this.activated.changeDetectorRef.detectChanges();
|
this.activated.changeDetectorRef.detectChanges();
|
||||||
|
|
|
@ -1095,17 +1095,19 @@ class ActivateRoutes {
|
||||||
|
|
||||||
const config = parentLoadedConfig(future.snapshot);
|
const config = parentLoadedConfig(future.snapshot);
|
||||||
|
|
||||||
let loadedFactoryResolver: ComponentFactoryResolver = null;
|
let resolver: ComponentFactoryResolver = null;
|
||||||
let loadedInjector: Injector = null;
|
let injector: Injector = null;
|
||||||
|
|
||||||
if (config) {
|
if (config) {
|
||||||
loadedFactoryResolver = config.factoryResolver;
|
injector = config.injectorFactory(outlet.locationInjector);
|
||||||
loadedInjector = config.injector;
|
resolver = config.factoryResolver;
|
||||||
resolved.push({provide: ComponentFactoryResolver, useValue: loadedFactoryResolver});
|
resolved.push({provide: ComponentFactoryResolver, useValue: resolver});
|
||||||
|
} else {
|
||||||
|
injector = outlet.locationInjector;
|
||||||
|
resolver = outlet.locationFactoryResolver;
|
||||||
}
|
}
|
||||||
outlet.activate(
|
|
||||||
future, loadedFactoryResolver, loadedInjector, ReflectiveInjector.resolve(resolved),
|
outlet.activate(future, resolver, injector, ReflectiveInjector.resolve(resolved), outletMap);
|
||||||
outletMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private deactiveRouteAndItsChildren(
|
private deactiveRouteAndItsChildren(
|
||||||
|
|
|
@ -24,17 +24,19 @@ export const ROUTES = new OpaqueToken('ROUTES');
|
||||||
export class LoadedRouterConfig {
|
export class LoadedRouterConfig {
|
||||||
constructor(
|
constructor(
|
||||||
public routes: Route[], public injector: Injector,
|
public routes: Route[], public injector: Injector,
|
||||||
public factoryResolver: ComponentFactoryResolver) {}
|
public factoryResolver: ComponentFactoryResolver, public injectorFactory: Function) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RouterConfigLoader {
|
export class RouterConfigLoader {
|
||||||
constructor(private loader: NgModuleFactoryLoader, private compiler: Compiler) {}
|
constructor(private loader: NgModuleFactoryLoader, private compiler: Compiler) {}
|
||||||
|
|
||||||
load(parentInjector: Injector, loadChildren: LoadChildren): Observable<LoadedRouterConfig> {
|
load(parentInjector: Injector, loadChildren: LoadChildren): Observable<LoadedRouterConfig> {
|
||||||
return map.call(this.loadModuleFactory(loadChildren), (r: any) => {
|
return map.call(this.loadModuleFactory(loadChildren), (r: NgModuleFactory<any>) => {
|
||||||
const ref = r.create(parentInjector);
|
const ref = r.create(parentInjector);
|
||||||
|
const injectorFactory = (parent: Injector) => r.create(parent).injector;
|
||||||
return new LoadedRouterConfig(
|
return new LoadedRouterConfig(
|
||||||
flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver);
|
flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver,
|
||||||
|
injectorFactory);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,8 @@ describe('applyRedirects', () => {
|
||||||
describe('lazy loading', () => {
|
describe('lazy loading', () => {
|
||||||
it('should load config on demand', () => {
|
it('should load config on demand', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
const loader = {
|
const loader = {
|
||||||
load: (injector: any, p: any) => {
|
load: (injector: any, p: any) => {
|
||||||
if (injector !== 'providedInjector') throw 'Invalid Injector';
|
if (injector !== 'providedInjector') throw 'Invalid Injector';
|
||||||
|
@ -171,7 +172,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should load when all canLoad guards return true', () => {
|
it('should load when all canLoad guards return true', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
const guard = () => true;
|
const guard = () => true;
|
||||||
|
@ -191,7 +193,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should not load when any canLoad guards return false', () => {
|
it('should not load when any canLoad guards return false', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
const trueGuard = () => true;
|
const trueGuard = () => true;
|
||||||
|
@ -216,7 +219,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should not load when any canLoad guards is rejected (promises)', () => {
|
it('should not load when any canLoad guards is rejected (promises)', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
const trueGuard = () => Promise.resolve(true);
|
const trueGuard = () => Promise.resolve(true);
|
||||||
|
@ -237,7 +241,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should work with objects implementing the CanLoad interface', () => {
|
it('should work with objects implementing the CanLoad interface', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
const guard = {canLoad: () => Promise.resolve(true)};
|
const guard = {canLoad: () => Promise.resolve(true)};
|
||||||
|
@ -254,7 +259,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should work with absolute redirects', () => {
|
it('should work with absolute redirects', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
|
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
|
@ -269,7 +275,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should load the configuration only once', () => {
|
it('should load the configuration only once', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
|
|
||||||
let called = false;
|
let called = false;
|
||||||
const loader = {
|
const loader = {
|
||||||
|
@ -295,7 +302,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should load the configuration of a wildcard route', () => {
|
it('should load the configuration of a wildcard route', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
|
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
|
@ -308,7 +316,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should load the configuration after a local redirect from a wildcard route', () => {
|
it('should load the configuration after a local redirect from a wildcard route', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
|
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
|
@ -322,7 +331,8 @@ describe('applyRedirects', () => {
|
||||||
|
|
||||||
it('should load the configuration after an absolute redirect from a wildcard route', () => {
|
it('should load the configuration after an absolute redirect from a wildcard route', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig(
|
const loadedConfig = new LoadedRouterConfig(
|
||||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver');
|
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||||
|
<any>'injectorFactory');
|
||||||
|
|
||||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||||
|
|
||||||
|
|
|
@ -2024,54 +2024,78 @@ describe('Integration', () => {
|
||||||
expect(location.path()).toEqual('/lazy2/loaded');
|
expect(location.path()).toEqual('/lazy2/loaded');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
it('should use the injector of the lazily-loaded configuration',
|
|
||||||
fakeAsync(inject(
|
|
||||||
[Router, Location, NgModuleFactoryLoader],
|
|
||||||
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
|
|
||||||
class LazyLoadedServiceDefinedInModule {}
|
|
||||||
class LazyLoadedServiceDefinedInCmp {}
|
|
||||||
|
|
||||||
@Component({selector: 'lazy', template: 'lazy-loaded'})
|
describe('should use the injector of the lazily-loaded configuration', () => {
|
||||||
class LazyLoadedChildComponent {
|
class LazyLoadedServiceDefinedInModule {}
|
||||||
constructor(service: LazyLoadedServiceDefinedInCmp) {}
|
class LazyLoadedServiceDefinedInCmp {}
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lazy',
|
selector: 'eager-parent',
|
||||||
template: '<router-outlet></router-outlet>',
|
template: 'eager-parent <router-outlet></router-outlet>',
|
||||||
providers: [LazyLoadedServiceDefinedInCmp]
|
})
|
||||||
})
|
class EagerParentComponent {
|
||||||
class LazyLoadedParentComponent {
|
}
|
||||||
constructor(service: LazyLoadedServiceDefinedInModule) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
@Component({selector: 'lazy-parent', template: 'lazy-parent <router-outlet></router-outlet>'})
|
||||||
declarations: [LazyLoadedParentComponent, LazyLoadedChildComponent],
|
class LazyParentComponent {
|
||||||
imports: [RouterModule.forChild([{
|
}
|
||||||
path: '',
|
|
||||||
children: [{
|
|
||||||
path: 'loaded',
|
|
||||||
component: LazyLoadedParentComponent,
|
|
||||||
children: [{path: 'child', component: LazyLoadedChildComponent}]
|
|
||||||
}]
|
|
||||||
}])],
|
|
||||||
providers: [LazyLoadedServiceDefinedInModule]
|
|
||||||
})
|
|
||||||
class LoadedModule {
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.stubbedModules = {expected: LoadedModule};
|
@Component({selector: 'lazy-child', template: 'lazy-child'})
|
||||||
|
class LazyChildComponent {
|
||||||
|
constructor(
|
||||||
|
lazy: LazyParentComponent, // should be able to inject lazy/direct parent
|
||||||
|
lazyService: LazyLoadedServiceDefinedInModule, // should be able to inject lazy service
|
||||||
|
eager:
|
||||||
|
EagerParentComponent // should use the injector of the location to create a parent
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
const fixture = createRoot(router, RootCmp);
|
@NgModule({
|
||||||
|
declarations: [LazyParentComponent, LazyChildComponent],
|
||||||
|
imports: [RouterModule.forChild([{
|
||||||
|
path: '',
|
||||||
|
children: [{
|
||||||
|
path: 'lazy-parent',
|
||||||
|
component: LazyParentComponent,
|
||||||
|
children: [{path: 'lazy-child', component: LazyChildComponent}]
|
||||||
|
}]
|
||||||
|
}])],
|
||||||
|
providers: [LazyLoadedServiceDefinedInModule]
|
||||||
|
})
|
||||||
|
class LoadedModule {
|
||||||
|
}
|
||||||
|
|
||||||
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
|
@NgModule({
|
||||||
|
declarations: [EagerParentComponent],
|
||||||
|
entryComponents: [EagerParentComponent],
|
||||||
|
imports: [RouterModule]
|
||||||
|
})
|
||||||
|
class TestModule {
|
||||||
|
}
|
||||||
|
|
||||||
router.navigateByUrl('/lazy/loaded/child');
|
beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); });
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
expect(location.path()).toEqual('/lazy/loaded/child');
|
it('should use the injector of the lazily-loaded configuration',
|
||||||
expect(fixture.nativeElement).toHaveText('lazy-loaded');
|
fakeAsync(inject(
|
||||||
})));
|
[Router, Location, NgModuleFactoryLoader],
|
||||||
|
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
|
||||||
|
loader.stubbedModules = {expected: LoadedModule};
|
||||||
|
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([{
|
||||||
|
path: 'eager-parent',
|
||||||
|
component: EagerParentComponent,
|
||||||
|
children: [{path: 'lazy', loadChildren: 'expected'}]
|
||||||
|
}]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child');
|
||||||
|
expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child');
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
it('works when given a callback',
|
it('works when given a callback',
|
||||||
fakeAsync(inject(
|
fakeAsync(inject(
|
||||||
|
|
|
@ -289,9 +289,11 @@ export declare class RouterOutlet implements OnDestroy {
|
||||||
component: Object;
|
component: Object;
|
||||||
deactivateEvents: EventEmitter<any>;
|
deactivateEvents: EventEmitter<any>;
|
||||||
isActivated: boolean;
|
isActivated: boolean;
|
||||||
|
locationFactoryResolver: ComponentFactoryResolver;
|
||||||
|
locationInjector: Injector;
|
||||||
outletMap: RouterOutletMap;
|
outletMap: RouterOutletMap;
|
||||||
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
|
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
|
||||||
activate(activatedRoute: ActivatedRoute, loadedResolver: ComponentFactoryResolver, loadedInjector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
|
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
|
||||||
deactivate(): void;
|
deactivate(): void;
|
||||||
ngOnDestroy(): void;
|
ngOnDestroy(): void;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue