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))