Skip to content

Commit ac3cdef

Browse files
feat(lazyLoad): Allow loadChildren for non-future states.
Relax future state replacement requirement. This allows existing states to be manually manually in the child module's config function. See "should support loadChildren on non-future state" unit test for an example. Relates to #164
1 parent 3d7ce44 commit ac3cdef

File tree

3 files changed

+107
-16
lines changed

3 files changed

+107
-16
lines changed

src/lazyLoad/lazyLoadNgModule.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { NgModuleRef, Injector, NgModuleFactory, Type, Compiler, NgModuleFactoryLoader } from "@angular/core";
44
import {
55
Transition, LazyLoadResult, UIRouter, Resolvable, NATIVE_INJECTOR_TOKEN, isString, unnestR, inArray, StateObject,
6-
uniqR
6+
uniqR, StateDeclaration
77
} from "@uirouter/core";
88
import { RootModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN, StatesModule } from "../uiRouterNgModule";
99
import { applyModuleConfig } from "../uiRouterConfig";
@@ -71,15 +71,15 @@ export type NgModuleToLoad = string | ModuleTypeCallback;
7171
* - Finds the "replacement state" for the target state, and adds the new NgModule Injector to it (as a resolve)
7272
* - Returns the new states array
7373
*/
74-
export function loadNgModule(moduleToLoad: NgModuleToLoad): (transition: Transition) => Promise<LazyLoadResult> {
75-
return (transition: Transition) => {
74+
export function loadNgModule(moduleToLoad: NgModuleToLoad): (transition: Transition, stateObject: StateDeclaration) => Promise<LazyLoadResult> {
75+
return (transition: Transition, stateObject: StateDeclaration) => {
7676
const ng2Injector = transition.injector().get(NATIVE_INJECTOR_TOKEN);
7777

7878
const createModule = (factory: NgModuleFactory<any>) =>
7979
factory.create(ng2Injector);
8080

8181
const applyModule = (moduleRef: NgModuleRef<any>) =>
82-
applyNgModule(transition, moduleRef);
82+
applyNgModule(transition, moduleRef, ng2Injector, stateObject);
8383

8484
return loadModuleFactory(moduleToLoad, ng2Injector)
8585
.then(createModule)
@@ -131,13 +131,12 @@ export function loadModuleFactory(moduleToLoad: NgModuleToLoad, ng2Injector: Inj
131131
*
132132
* @internalapi
133133
*/
134-
export function applyNgModule(transition: Transition, ng2Module: NgModuleRef<any>): LazyLoadResult {
134+
export function applyNgModule(transition: Transition, ng2Module: NgModuleRef<any>, parentInjector: Injector, lazyLoadState: StateDeclaration): LazyLoadResult {
135135
let injector = ng2Module.injector;
136-
let parentInjector = <Injector> ng2Module.injector['parent'] || ng2Module.injector['_parent'];
137136
let uiRouter: UIRouter = injector.get(UIRouter);
138137
let registry = uiRouter.stateRegistry;
139138

140-
let originalName = transition.to().name;
139+
let originalName = lazyLoadState.name;
141140
let originalState = registry.get(originalName);
142141
// Check if it's a future state (ends with .**)
143142
let isFuture = /^(.*)\.\*\*$/.exec(originalName);
@@ -159,13 +158,15 @@ export function applyNgModule(transition: Transition, ng2Module: NgModuleRef<any
159158
.reduce(unnestR, [])
160159
.reduce(uniqR, []);
161160

162-
let replacementState = registry.get(replacementName);
163-
if (!replacementState || replacementState === originalState) {
164-
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. ` +
165-
`The lazy loaded NgModule must have a state named '${replacementName}' ` +
166-
`which replaces the (placeholder) '${originalName}' Future State. ` +
167-
`Add a '${replacementName}' state to the lazy loaded NgModule ` +
168-
`using UIRouterModule.forChild({ states: CHILD_STATES }).`);
161+
if (isFuture) {
162+
let replacementState = registry.get(replacementName);
163+
if (!replacementState || replacementState === originalState) {
164+
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. ` +
165+
`The lazy loaded NgModule must have a state named '${replacementName}' ` +
166+
`which replaces the (placeholder) '${originalName}' Future State. ` +
167+
`Add a '${replacementName}' state to the lazy loaded NgModule ` +
168+
`using UIRouterModule.forChild({ states: CHILD_STATES }).`);
169+
}
169170
}
170171

171172
// Supply the newly loaded states with the Injector from the lazy loaded NgModule.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { StatesModule, UIRouterModule } from '../../../src/uiRouterNgModule';
2+
import { Component, Injector, NgModule } from '@angular/core';
3+
import { UIRouter } from '@uirouter/core';
4+
import { Ng2StateDeclaration } from '../../../src/interface';
5+
6+
@Component({
7+
selector: 'component1',
8+
template: '<h1>Component 1</h1><ui-view></ui-view>'
9+
}) export class Component1 { }
10+
11+
@Component({
12+
selector: 'component2',
13+
template: '<h1>Component 2</h1>',
14+
}) export class Component2 { }
15+
16+
17+
export const augment1 = { name: 'augment1', component: Component1 };
18+
export const augment2 = { name: 'augment1.augment2', component: Component2 };
19+
export const states: Ng2StateDeclaration[] = [augment1, augment2];
20+
21+
export function config(router: UIRouter, injector: Injector, module: StatesModule) {
22+
const registry = router.stateRegistry;
23+
24+
// copy urls from old state to new
25+
states.forEach(state => state.url = registry.get(state.name).url);
26+
registry.deregister('augment1');
27+
module.states = states;
28+
}
29+
30+
@NgModule({
31+
imports: [UIRouterModule.forChild({ config })],
32+
declarations: [Component1, Component2],
33+
entryComponents: [Component1, Component2],
34+
})
35+
export class AugmentModule {}

test/ngModule/lazyModule.spec.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,30 @@ let futureFoo = {
1212
loadChildren: () => System.import('./foo/foo.module').then(x => x.FooModule)
1313
};
1414

15+
let futureBar = {
16+
name: 'bar.**',
17+
url: '/bar',
18+
loadChildren: () => System.import('./foo/foo.module').then(x => x.FooModule)
19+
};
20+
21+
let augment1 = {
22+
name: 'augment1',
23+
url: '/augment1',
24+
loadChildren: () => System.import('./augment/augment.module').then(x => x.AugmentModule)
25+
};
26+
27+
let augment2 = {
28+
name: 'augment1.augment2',
29+
url: '/augment2',
30+
};
31+
1532
function configFn(router: UIRouter) {
1633
router.plugin(memoryLocationPlugin);
1734
}
1835

1936
describe('lazy loading', () => {
2037
beforeEach(() => {
21-
let routerModule = UIRouterModule.forRoot({ useHash: true, states: [futureFoo], config: configFn });
38+
let routerModule = UIRouterModule.forRoot({ useHash: true, states: [], config: configFn });
2239

2340
TestBed.configureTestingModule({
2441
declarations: [],
@@ -30,10 +47,12 @@ describe('lazy loading', () => {
3047
});
3148

3249
it('should lazy load a module', async(inject([UIRouter], (router: UIRouter) => {
50+
let { stateRegistry, stateService, globals } = router;
51+
stateRegistry.register(futureFoo);
52+
3353
const fixture = TestBed.createComponent(UIView);
3454
fixture.detectChanges();
3555

36-
let { stateRegistry, stateService, globals } = router;
3756

3857
let names = stateRegistry.get().map(state => state.name).sort();
3958
expect(names.length).toBe(2);
@@ -48,6 +67,42 @@ describe('lazy loading', () => {
4867
});
4968
})));
5069

70+
it('should throw if no future state replacement is lazy loaded', async(inject([UIRouter], (router: UIRouter) => {
71+
let { stateRegistry, stateService } = router;
72+
stateService.defaultErrorHandler(() => null);
73+
stateRegistry.register(futureBar);
74+
75+
const fixture = TestBed.createComponent(UIView);
76+
fixture.detectChanges();
77+
78+
let names = stateRegistry.get().map(state => state.name).sort();
79+
expect(names.length).toBe(2);
80+
expect(names).toEqual(['', 'bar.**']);
81+
82+
const success = () => { throw Error('success not expected') };
83+
const error = (err) => {
84+
expect(err.detail.message).toContain("The lazy loaded NgModule must have a state named 'bar'"); };
85+
stateService.go('bar').then(success, error);
86+
})));
87+
88+
it('should support loadChildren on non-future state (manual state cleanup)', async(inject([UIRouter], (router: UIRouter) => {
89+
let { stateRegistry, stateService } = router;
90+
stateRegistry.register(augment1);
91+
stateRegistry.register(augment2);
92+
93+
const fixture = TestBed.createComponent(UIView);
94+
fixture.detectChanges();
95+
96+
let names = stateRegistry.get().map(state => state.name).sort();
97+
expect(names).toEqual(['', 'augment1', 'augment1.augment2']);
98+
99+
const wait = (delay) => new Promise(resolve => setTimeout(resolve, delay));
100+
stateService.go('augment1.augment2').then(() => {
101+
fixture.detectChanges();
102+
expect(stateService.current.name).toBe('augment1.augment2');
103+
expect(fixture.debugElement.nativeElement.textContent.replace(/\s+/g, " ").trim()).toBe('Component 1 Component 2');
104+
})
105+
})));
51106

52107
});
53108

0 commit comments

Comments
 (0)