fix(router): incorrect injector is used when instantiating components loaded lazily (#12817)

This commit is contained in:
Victor Savkin 2016-11-11 17:12:00 -08:00 committed by Victor Berchet
parent 69dfcf7385
commit 52be848f94
7 changed files with 110 additions and 75 deletions

View File

@ -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));
} }
} }
} }

View File

@ -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();

View File

@ -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(

View File

@ -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);
}); });
} }

View File

@ -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)};

View File

@ -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(

View File

@ -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;
} }