Skip to content

Commit 2a4b174

Browse files
feat(lazyLoad): Allow loadChildren: sugar on a state definition
feat(lazyLoad): automatically unwrap default export (`__esModule`) (allows easier default export of NgModule) fix(lazyLoad): Update lazyLoad for ui-router-core 3.1 Instead of: ```js import { loadNgModule } from "ui-router-ng2"; var futureState = { name: 'lazy.state', url: '/lazy', lazyLoad: loadNgModule('./lazy/lazy.module') } ``` Instead of: ```js import { loadNgModule } from "ui-router-ng2"; var futureState = { name: 'lazy.state', url: '/lazy', lazyLoad: loadNgModule(() => System.import('./lazy/lazy.module').then(result => result.LazyModule)) } ``` Switch to: ```js var futureState = { name: 'lazy.state.**', url: '/lazy', loadChildren: './lazy/lazy.module#LazyModule' } ``` --- This change is an incremental step towards seamless AoT and lazy load support within the angular-cli. Currently the cli only supports lazy loading of angular router routes. We expect the AoT compiler and cli (@ngtools/webpack) will be updated to support AoT lazy loading by other routers (such as ui-router) soon.
1 parent 82a52e5 commit 2a4b174

File tree

8 files changed

+212
-49
lines changed

8 files changed

+212
-49
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"main": "lib/index.js",
5151
"typings": "lib/index.d.ts",
5252
"dependencies": {
53-
"ui-router-core": "=3.1.0"
53+
"ui-router-core": "=3.1.1"
5454
},
5555
"peerDependencies": {
5656
"@angular/common": "^2.0.0",

src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import 'rxjs/add/operator/concat';
1414
import 'rxjs/add/operator/map';
1515

1616
export * from "./interface";
17-
export * from "./lazyLoadNgModule";
18-
export * from "./rx";
1917
export * from "./providers";
20-
export * from "./location/uiRouterLocation";
21-
export * from "./directives/directives";
22-
export * from "./statebuilders/views";
2318
export * from "./uiRouterNgModule";
2419
export * from "./uiRouterConfig";
20+
export * from "./directives/directives";
21+
export * from "./location/uiRouterLocation";
22+
export * from "./statebuilders/views";
23+
export * from "./lazyLoad/lazyLoadNgModule";
24+
export * from "./rx";

src/interface.ts

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/** @ng2api @module state */ /** */
2-
import {StateDeclaration, _ViewDeclaration} from "ui-router-core";
3-
import {Transition} from "ui-router-core";
4-
import {Type, OpaqueToken} from "@angular/core";
5-
import {HookResult} from "ui-router-core";
1+
/** @ng2api @module state */
2+
/** */
3+
4+
import { StateDeclaration, _ViewDeclaration, Transition, HookResult } from "ui-router-core";
5+
import { Type } from "@angular/core";
6+
import { NgModuleToLoad } from "./lazyLoad/lazyLoadNgModule";
67

78
/**
89
* The StateDeclaration object is used to define a state or nested state.
@@ -12,16 +13,18 @@ import {HookResult} from "ui-router-core";
1213
* ```js
1314
* import {FoldersComponent} from "./folders";
1415
*
16+
* export function getAllFolders(FolderService) {
17+
* return FolderService.list();
18+
* }
19+
*
1520
* // StateDeclaration object
16-
* var foldersState = {
21+
* export let foldersState = {
1722
* name: 'folders',
1823
* url: '/folders',
1924
* component: FoldersComponent,
20-
* resolve: {
21-
* allfolders: function(FolderService) {
22-
* return FolderService.list();
23-
* }
24-
* }
25+
* resolve: [
26+
* { token: 'allfolders', deps: [FolderService], resolveFn: getAllFolders }
27+
* ]
2528
* }
2629
* ```
2730
*/
@@ -130,6 +133,72 @@ export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaratio
130133
* the `views` object.
131134
*/
132135
views?: { [key: string]: Ng2ViewDeclaration; };
136+
137+
/**
138+
* A string or function used to lazy load an `NgModule`
139+
*
140+
* The `loadChildren` property should be added to a Future State (a lazy loaded state whose name ends in `.**`).
141+
* The Future State is a placeholder for a tree of states that will be lazy loaded in the future.
142+
*
143+
* When the future state is activated, the `loadChildren` property will lazy load an `NgModule`
144+
* which contains the fully loaded states.
145+
* The `NgModule` should contain the fully loaded states which will be registered.
146+
* The fully loaded states will replace the temporary future states once lazy loading is complete.
147+
*
148+
* ---
149+
*
150+
* When `loadChildren` is a string, it should be a relative path to the module code that will be lazy loaded.
151+
* It should follow the semantics of the Angular Router's `loadChildren` property.
152+
* The string will be split in half on the hash character (`#`).
153+
* The first half is the path to the module.
154+
* The last half is the named export of the `NgModule` inside the ES6 module.
155+
*
156+
* #### Example:
157+
*
158+
* home.module.ts
159+
*
160+
* ```
161+
* @NgModule({... })
162+
* export class HomeModule {};
163+
* ```
164+
*
165+
* ```js
166+
* var futureState = {
167+
* name: 'home.**',
168+
* url: '/home',
169+
* loadChildren: './home/home.module#HomeModule')
170+
* }
171+
* ```
172+
*
173+
*
174+
* As a function, it should return a promise for the `NgModule`
175+
*
176+
* #### Example:
177+
* ```js
178+
* var futureState = {
179+
* name: 'home.**',
180+
* url: '/home',
181+
* loadChildren: () => System.import('./home/home.module')
182+
* .then(result => result.HomeModule);
183+
* }
184+
* ```
185+
*
186+
* #### Example:
187+
* This shows the load function being exported for compatibility with the AoT compiler.
188+
* ```js
189+
* export function loadHomeModule() {
190+
* return System.import('./home/home.module')
191+
* .then(result => result.HomeModule);
192+
* }
193+
*
194+
* var futureState = {
195+
* name: 'home.**',
196+
* url: '/home',
197+
* loadChildren: loadHomeModule
198+
* }
199+
* ```
200+
*/
201+
loadChildren?: NgModuleToLoad;
133202
}
134203

135204
export interface Ng2ViewDeclaration extends _ViewDeclaration {

src/lazyLoadNgModule.ts renamed to src/lazyLoad/lazyLoadNgModule.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,66 @@
1-
/** @ng2api @module core */ /** */
2-
import {NgModuleFactoryLoader, NgModuleRef, Injector, NgModuleFactory, Type, Compiler} from "@angular/core";
3-
import {Transition, LazyLoadResult, UIRouter, Resolvable, NATIVE_INJECTOR_TOKEN, isString} from "ui-router-core";
4-
import {RootModule, StatesModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN} from "./uiRouterNgModule";
5-
import {applyModuleConfig} from "./uiRouterConfig";
1+
/** @ng2api @module core */
2+
/** */
3+
import { NgModuleRef, Injector, NgModuleFactory, Type, Compiler, NgModuleFactoryLoader } from "@angular/core";
4+
import { Transition, LazyLoadResult, UIRouter, Resolvable, NATIVE_INJECTOR_TOKEN, isString } from "ui-router-core";
5+
import { RootModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN } from "../uiRouterNgModule";
6+
import { applyModuleConfig } from "../uiRouterConfig";
67

8+
/**
9+
* A function that returns an NgModule, or a promise for an NgModule
10+
*
11+
* #### Example:
12+
* ```js
13+
* export function loadFooModule() {
14+
* return System.import('../foo/foo.module').then(result => result.FooModule);
15+
* }
16+
* ```
17+
*/
718
export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
19+
/**
20+
* A string or a function which lazy loads a module
21+
*
22+
* If a string, should conform to the Angular Router `loadChildren` string.
23+
* #### Example:
24+
* ```
25+
* var ngModuleToLoad = './foo/foo.module#FooModule'
26+
* ```
27+
*
28+
* For functions, see: [[ModuleTypeCallback]]
29+
*/
830
export type NgModuleToLoad = string | ModuleTypeCallback;
931

1032
/**
1133
* Returns a function which lazy loads a nested module
1234
*
13-
* Use this function as a [[StateDeclaration.lazyLoad]] property to lazy load an NgModule and its state.
35+
* This is primarily used by the [[ng2LazyLoadBuilder]] when processing [[Ng2StateDeclaration.loadChildren]].
1436
*
15-
* Example using `System.import()`:
37+
* It could also be used manually as a [[StateDeclaration.lazyLoad]] property to lazy load an `NgModule` and its state(s).
38+
*
39+
* #### Example:
40+
* Using `System.import()` and named export of `HomeModule`
1641
* ```js
17-
* {
18-
* name: 'home',
42+
* declare var System;
43+
* var futureState = {
44+
* name: 'home.**',
1945
* url: '/home',
20-
* lazyLoad: loadNgModule(() => System.import('./home.module').then(result => result.HomeModule))
46+
* lazyLoad: loadNgModule(() => System.import('./home/home.module').then(result => result.HomeModule))
2147
* }
2248
* ```
2349
*
24-
* Example using `NgModuleFactoryLoader`:
50+
* #### Example:
51+
* Using a path (string) to the module
2552
* ```js
26-
* {
27-
* name: 'home',
53+
* var futureState = {
54+
* name: 'home.**',
2855
* url: '/home',
29-
* lazyLoad: loadNgModule('./home.module')
56+
* lazyLoad: loadNgModule('./home/home.module#HomeModule')
3057
* }
3158
* ```
3259
*
33-
* @param moduleToLoad
34-
* If a string, it should be the path to the NgModule code, which will then be loaded by the `NgModuleFactoryLoader`.
35-
* If a function, the function should load the NgModule code and return a reference to the `NgModule` class being loaded.
60+
*
61+
* @param moduleToLoad a path (string) to the NgModule to load.
62+
* Or a function which loads the NgModule code which should
63+
* return a reference to the `NgModule` class being loaded (or a `Promise` for it).
3664
*
3765
* @returns A function which takes a transition, which:
3866
* - Gets the Injector (scoped properly for the destination state)
@@ -76,10 +104,13 @@ export function loadModuleFactory(moduleToLoad: NgModuleToLoad, ng2Injector: Inj
76104

77105
const compiler: Compiler = ng2Injector.get(Compiler);
78106
const offlineMode = compiler instanceof Compiler;
79-
const loadChildrenPromise = Promise.resolve(moduleToLoad());
107+
108+
const unwrapEsModuleDefault = x =>
109+
x && x.__esModule && x['default'] ? x['default'] : x;
80110
const compileAsync = (moduleType: Type<any>) =>
81111
compiler.compileModuleAsync(moduleType);
82112

113+
const loadChildrenPromise = Promise.resolve(moduleToLoad()).then(unwrapEsModuleDefault);
83114
return offlineMode ? loadChildrenPromise : loadChildrenPromise.then(compileAsync);
84115
}
85116

@@ -101,23 +132,31 @@ export function applyNgModule(transition: Transition, ng2Module: NgModuleRef<any
101132
let injector = ng2Module.injector;
102133
let parentInjector = <Injector> ng2Module.injector['parent'];
103134
let uiRouter: UIRouter = injector.get(UIRouter);
135+
let registry = uiRouter.stateRegistry;
104136

105137
let originalName = transition.to().name;
106-
let originalState = uiRouter.stateRegistry.get(originalName);
138+
let originalState = registry.get(originalName);
139+
// Check if it's a future state (ends with .**)
140+
let isFuture = /^(.*)\.\*\*$/.exec(originalName);
141+
// Final name (without the .**)
142+
let replacementName = isFuture && isFuture[1];
107143

108144
let newRootModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_ROOT_MODULE);
109-
110145
if (newRootModules.length) {
111146
console.log(newRootModules);
112147
throw new Error('Lazy loaded modules should not contain a UIRouterModule.forRoot() module');
113148
}
114149

115-
let newModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_MODULE_TOKEN);
116-
newModules.forEach(module => applyModuleConfig(uiRouter, injector, module));
150+
let newChildModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_MODULE_TOKEN);
151+
newChildModules.forEach(module => applyModuleConfig(uiRouter, injector, module));
117152

118-
let replacementState = uiRouter.stateRegistry.get(originalName);
119-
if (replacementState === originalState) {
120-
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. That NgModule should also have a UIRouterModule.forChild() state named '${originalName}' to replace the Future State, but it did not.`);
153+
let replacementState = registry.get(replacementName);
154+
if (!replacementState || replacementState === originalState) {
155+
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. ` +
156+
`The lazy loaded NgModule must have a state named '${replacementName}' ` +
157+
`which replaces the (placeholder) '${originalName}' Future State. ` +
158+
`Add a '${replacementName}' state to the lazy loaded NgModule ` +
159+
`using UIRouterModule.forChild({ states: CHILD_STATES }).`);
121160
}
122161

123162
// Supply the newly loaded states with the Injector from the lazy loaded NgModule

src/location/locationService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @module ng2 */
22
/** */
33
import { UIRouter } from "ui-router-core";
4-
import { BaseLocationServices } from "ui-router-core/lib/vanilla";
4+
import { BaseLocationServices } from "ui-router-core/lib/vanilla/baseLocationService";
55
import { parseUrl } from "ui-router-core/lib/vanilla/utils";
66
import { PlatformLocation, LocationStrategy } from "@angular/common";
77

src/providers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import { RootModule, StatesModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN }
9999
import { UIRouterRx } from "./rx";
100100
import { servicesPlugin } from "ui-router-core/lib/vanilla";
101101
import { ServicesPlugin } from "ui-router-core/lib/vanilla/interface";
102+
import { ng2LazyLoadBuilder } from "./statebuilders/lazyLoad";
102103

103104
/**
104105
* This is a factory function for a UIRouter instance
@@ -142,6 +143,7 @@ export function uiRouterFactory(location: UIRouterLocation, injector: Injector)
142143
// Apply statebuilder decorator for ng2 NgModule registration
143144
let registry = router.stateRegistry;
144145
registry.decorator('views', ng2ViewsBuilder);
146+
registry.decorator('lazyLoad', ng2LazyLoadBuilder);
145147

146148
// Prep the tree of NgModule by placing the root NgModule's Injector on the root state.
147149
let ng2InjectorResolvable = Resolvable.fromData(NATIVE_INJECTOR_TOKEN, injector);

src/statebuilders/lazyLoad.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/** @module ng2 */
2+
/** */
3+
import { LazyLoadResult, Transition, StateDeclaration } from "ui-router-core"; // has or is using
4+
import { BuilderFunction, State } from "ui-router-core";
5+
import { loadNgModule } from "../lazyLoad/lazyLoadNgModule";
6+
7+
/**
8+
* This is a [[StateBuilder.builder]] function for ngModule lazy loading in angular2.
9+
*
10+
* When the [[StateBuilder]] builds a [[State]] object from a raw [[StateDeclaration]], this builder
11+
* decorates the `lazyLoad` property for states that have a [[Ng2StateDeclaration.ngModule]] declaration.
12+
*
13+
* If the state has a [[Ng2StateDeclaration.ngModule]], it will create a `lazyLoad` function
14+
* that in turn calls `loadNgModule(loadNgModuleFn)`.
15+
*
16+
* #### Example:
17+
* A state that has a `ngModule`
18+
* ```js
19+
* var decl = {
20+
* ngModule: () => System.import('./childModule.ts')
21+
* }
22+
* ```
23+
* would build a state with a `lazyLoad` function like:
24+
* ```js
25+
* import { loadNgModule } from "ui-router-ng2";
26+
* var decl = {
27+
* lazyLoad: loadNgModule(() => System.import('./childModule.ts')
28+
* }
29+
* ```
30+
*
31+
* If the state has both a `ngModule:` *and* a `lazyLoad`, then the `lazyLoad` is run first.
32+
*
33+
* #### Example:
34+
* ```js
35+
* var decl = {
36+
* lazyLoad: () => System.import('third-party-library'),
37+
* ngModule: () => System.import('./childModule.ts')
38+
* }
39+
* ```
40+
* would build a state with a `lazyLoad` function like:
41+
* ```js
42+
* import { loadNgModule } from "ui-router-ng2";
43+
* var decl = {
44+
* lazyLoad: () => System.import('third-party-library')
45+
* .then(() => loadNgModule(() => System.import('./childModule.ts'))
46+
* }
47+
* ```
48+
*
49+
*/
50+
export function ng2LazyLoadBuilder(state: State, parent: BuilderFunction) {
51+
let loadNgModuleFn = state['loadChildren'];
52+
return loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
53+
}

src/uiRouterNgModule.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ import { _UIROUTER_INSTANCE_PROVIDERS, _UIROUTER_SERVICE_PROVIDERS } from "./pro
1212

1313
export function makeRootProviders(module: StatesModule): Provider[] {
1414
return [
15-
{ provide: UIROUTER_ROOT_MODULE, useValue: module, multi: true},
15+
{ provide: UIROUTER_ROOT_MODULE, useValue: module, multi: true},
1616
{ provide: UIROUTER_MODULE_TOKEN, useValue: module, multi: true },
17+
{ provide: UIROUTER_STATES, useValue: module.states || [], multi: true },
1718
{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: module.states || [], multi: true },
1819
];
1920
}
2021

2122
export function makeChildProviders(module: StatesModule): Provider[] {
2223
return [
2324
{ provide: UIROUTER_MODULE_TOKEN, useValue: module, multi: true },
25+
{ provide: UIROUTER_STATES, useValue: module.states || [], multi: true },
2426
{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: module.states || [], multi: true },
2527
];
2628
}
@@ -221,8 +223,6 @@ export interface StatesModule {
221223
configClass?: Type<any>;
222224
}
223225

224-
/** @hidden */
225-
export const UIROUTER_ROOT_MODULE = new OpaqueToken("UIRouter Root Module");
226-
227-
/** @hidden */
228-
export const UIROUTER_MODULE_TOKEN = new OpaqueToken("UIRouter Module");
226+
/** @hidden */ export const UIROUTER_ROOT_MODULE = new OpaqueToken("UIRouter Root Module");
227+
/** @hidden */ export const UIROUTER_MODULE_TOKEN = new OpaqueToken("UIRouter Module");
228+
/** @hidden */ export const UIROUTER_STATES = new OpaqueToken("UIRouter States");

0 commit comments

Comments
 (0)