diff --git a/projects/scion/workbench/src/lib/spec/lazy-loaded-view-injection.spec.ts b/projects/scion/workbench/src/lib/spec/lazy-loaded-view-injection.spec.ts new file mode 100644 index 000000000..017427264 --- /dev/null +++ b/projects/scion/workbench/src/lib/spec/lazy-loaded-view-injection.spec.ts @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2018 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { async, fakeAsync, inject, TestBed } from '@angular/core/testing'; +import { Component, Inject, Injectable, InjectionToken, NgModule, NgModuleFactoryLoader, Optional } from '@angular/core'; +import { WorkbenchModule } from '../workbench.module'; +import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.spec'; +import { RouterTestingModule, SpyNgModuleFactoryLoader } from '@angular/router/testing'; +import { Router, RouterModule } from '@angular/router'; +import { WorkbenchRouter } from '../routing/workbench-router.service'; +import { CommonModule } from '@angular/common'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { advance, clickElement } from './util/util.spec'; +import { ActivityPartComponent } from '../activity-part/activity-part.component'; +import { By } from '@angular/platform-browser'; + +/** + * + * Testsetup: + * + * +--------------+ + * | Test Module | + * +--------------+ + * | + * feature + * | + * v + * +------------------------------------------+ + * | Feature Module | + * |------------------------------------------| + * | routes: | + * | | + * | 'activity' => Feature_Activity_Component | + * | 'view' => Feature_View_Component | + * +------------------------------------------+ + * + */ +// tslint:disable class-name +describe('Lazily loaded view', () => { + + beforeEach(async(() => { + jasmine.addMatchers(jasmineCustomMatchers); + + TestBed.configureTestingModule({ + imports: [AppTestModule] + }); + + TestBed.get(Router).initialNavigation(); + })); + + it('should get services injected from its child injector', fakeAsync(inject([WorkbenchRouter, NgModuleFactoryLoader], (wbRouter: WorkbenchRouter, loader: SpyNgModuleFactoryLoader) => { + loader.stubbedModules = { + './feature/feature.module#FeatureModule': FeatureModule, + }; + + const fixture = TestBed.createComponent(AppComponent); + advance(fixture); + + // Open 'feature/activity' + clickElement(fixture, ActivityPartComponent, 'a.activity'); + + // Verify injection token + const activityComponent: Feature_Activity_Component = fixture.debugElement.query(By.directive(Feature_Activity_Component)).componentInstance; + expect(activityComponent.featureService).not.toBeNull('(1)'); + expect(activityComponent.featureService).not.toBeUndefined('(2)'); + + // Open 'feature/view' + wbRouter.navigate(['/feature/view']).then(); + advance(fixture); + + // Verify injection token + const viewComponent: Feature_View_Component = fixture.debugElement.query(By.directive(Feature_View_Component)).componentInstance; + expect(viewComponent.featureService).not.toBeNull('(3)'); + expect(viewComponent.featureService).not.toBeUndefined('(4)'); + advance(fixture); + }))); + + /** + * Verifies that a service provided in the lazily loaded module should be preferred over the service provided in the root module. + */ + it('should get services injected from its child injector prior to from the root injector', fakeAsync(inject([WorkbenchRouter, NgModuleFactoryLoader], (wbRouter: WorkbenchRouter, loader: SpyNgModuleFactoryLoader) => { + loader.stubbedModules = { + './feature/feature.module#FeatureModule': FeatureModule, + }; + + const fixture = TestBed.createComponent(AppComponent); + advance(fixture); + + // Open 'feature/activity' + clickElement(fixture, ActivityPartComponent, 'a.activity'); + + // Verify injection token + const activityComponent: Feature_Activity_Component = fixture.debugElement.query(By.directive(Feature_Activity_Component)).componentInstance; + expect(activityComponent.injectedValue).toEqual('child-injector-value', '(1)'); + + // Open 'feature/view' + wbRouter.navigate(['/feature/view']).then(); + advance(fixture); + + // Verify injection token + const viewComponent: Feature_View_Component = fixture.debugElement.query(By.directive(Feature_View_Component)).componentInstance; + expect(viewComponent.injectedValue).toEqual('child-injector-value', '(2)'); + advance(fixture); + }))); +}); + +/**************************************************************************************************** + * Definition of App Test Module * + ****************************************************************************************************/ +@Component({ + template: ` + + + + + ` +}) +class AppComponent { +} + +const DI_TOKEN = new InjectionToken('TOKEN'); + +@Injectable() +export class FeatureService { +} + +@NgModule({ + imports: [ + WorkbenchModule.forRoot(), + NoopAnimationsModule, + RouterTestingModule.withRoutes([ + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'}, + ]), + ], + declarations: [AppComponent], + providers: [ + {provide: DI_TOKEN, useValue: 'root-injector-value'}, + ] +}) +class AppTestModule { +} + +/**************************************************************************************************** + * Definition of Feature Module * + ****************************************************************************************************/ +@Component({template: 'Injected value: {{injectedValue}}'}) +class Feature_Activity_Component { + constructor(@Inject(DI_TOKEN) public injectedValue: string, + @Optional() public featureService: FeatureService) { + } +} + +@Component({template: 'Injected value: {{injectedValue}}'}) +class Feature_View_Component { + constructor(@Inject(DI_TOKEN) public injectedValue: string, + @Optional() public featureService: FeatureService) { + } +} + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + {path: 'activity', component: Feature_Activity_Component}, + {path: 'view', component: Feature_View_Component} + ]), + ], + declarations: [ + Feature_Activity_Component, + Feature_View_Component + ], + providers: [ + {provide: DI_TOKEN, useValue: 'child-injector-value'}, + FeatureService, + ] +}) +class FeatureModule { +} diff --git a/projects/scion/workbench/src/lib/workbench-view-registry.service.ts b/projects/scion/workbench/src/lib/workbench-view-registry.service.ts index 8856e0564..7550421b4 100644 --- a/projects/scion/workbench/src/lib/workbench-view-registry.service.ts +++ b/projects/scion/workbench/src/lib/workbench-view-registry.service.ts @@ -26,8 +26,7 @@ export class WorkbenchViewRegistry implements OnDestroy { private readonly _destroy$ = new Subject(); private readonly _viewRegistry = new Map(); - constructor(private _injector: Injector, - private _componentFactoryResolver: ComponentFactoryResolver, + constructor(private _componentFactoryResolver: ComponentFactoryResolver, private _workbench: WorkbenchService) { } @@ -91,8 +90,17 @@ export class WorkbenchViewRegistry implements OnDestroy { injectionTokens.set(WorkbenchView, view); injectionTokens.set(InternalWorkbenchView, view); + // We must not use the root injector as parent injector of the portal component element injector. + // Otherwise, if tokens of the root injector are masked or extended in lazily loaded modules, they would not be resolved. + // + // This is by design of Angular injection token resolution rules of not checking module injectors when checking the element hierarchy for a token. + // See function `resolveDep` in Angular file `provider.ts`. + // + // Instead, we use a {NullInjector} which further acts as a barrier to not resolve workbench internal tokens declared somewhere in the element hierarchy. + const injector = new PortalInjector(Injector.NULL, injectionTokens); + portal.init({ - injector: new PortalInjector(this._injector, injectionTokens), + injector: injector, onActivate: (): void => view.activate(true), onDeactivate: (): void => view.activate(false), }); diff --git a/resources/site/_changelog-next-release.md b/resources/site/_changelog-next-release.md index 90c723086..1a6254e5f 100644 --- a/resources/site/_changelog-next-release.md +++ b/resources/site/_changelog-next-release.md @@ -6,3 +6,4 @@ ### Bug Fixes +* Allow lazily-loaded views to inject masked injection tokens ([#21](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/21)) ([xxx](https://github.com/SchweizerischeBundesbahnen/scion-workbench/commit/xxx))