Skip to content

Commit bef5257

Browse files
feat(lazyLoad): Add state.lazyLoad hook to lazy load a tree of states
Closes #146 Closes #2739
1 parent 4440811 commit bef5257

14 files changed

+216
-25
lines changed

src/hooks/lazyLoadStates.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {Transition} from "../transition/transition";
2+
import {TransitionService} from "../transition/transitionService";
3+
import {TransitionHookFn} from "../transition/interface";
4+
import {StateDeclaration} from "../state/interface";
5+
import {State} from "../state/stateObject";
6+
import {services} from "../common/coreservices";
7+
8+
/**
9+
* A [[TransitionHookFn]] that lazy loads a state tree.
10+
*
11+
* When transitioning to a state "abc" which has a `lazyLoad` function defined:
12+
* - Invoke the `lazyLoad` function
13+
* - The function should return a promise for an array of lazy loaded [[StateDeclaration]]s
14+
* - Wait for the promise to resolve
15+
* - Deregister the original state "abc"
16+
* - The original state definition is a placeholder for the lazy loaded states
17+
* - Register the new states
18+
* - Retry the transition
19+
*
20+
* See [[StateDeclaration.lazyLoad]]
21+
*/
22+
const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
23+
var toState = transition.to();
24+
25+
function retryOriginalTransition(newStates: State[]) {
26+
if (transition.options().source === 'url') {
27+
let loc = services.location;
28+
let path = loc.path(), search = loc.search(), hash = loc.hash();
29+
30+
let matchState = state => [state, state.url.exec(path, search, hash)];
31+
let matches = newStates.map(matchState).filter(([state, params]) => !!params);
32+
if (matches.length) {
33+
let [state, params] = matches[0];
34+
return transition.router.stateService.target(state, params, transition.options());
35+
}
36+
transition.router.urlRouter.sync();
37+
}
38+
39+
let state = transition.targetState().identifier();
40+
let params = transition.params();
41+
let options = transition.options();
42+
return transition.router.stateService.target(state, params, options);
43+
}
44+
45+
/**
46+
* Replace the placeholder state with the newly loaded states from the NgModule.
47+
*/
48+
function updateStateRegistry(newStates: StateDeclaration[]) {
49+
let registry = transition.router.stateRegistry;
50+
let placeholderState = transition.to();
51+
52+
registry.deregister(placeholderState);
53+
newStates.forEach(state => registry.register(state));
54+
return newStates.map(state => registry.get(state).$$state());
55+
}
56+
57+
return toState.lazyLoad(transition)
58+
.then(updateStateRegistry)
59+
.then(retryOriginalTransition)
60+
};
61+
62+
export const registerLazyLoadHook = (transitionService: TransitionService) =>
63+
transitionService.onBefore({ to: (state) => !!state.lazyLoad }, lazyLoadHook);

src/hooks/url.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {UrlRouter} from "../url/urlRouter";
33
import {StateService} from "../state/stateService";
44
import {Transition} from "../transition/transition";
55
import {TransitionHookFn} from "../transition/interface";
6+
import {TransitionService} from "../transition/transitionService";
67

78
/**
89
* A [[TransitionHookFn]] which updates the URL after a successful transition

src/ng2.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ export * from "./core";
88
import "./justjs";
99

1010
export * from "./ng2/interface";
11-
export * from "./ng2/routerModule";
11+
export * from "./ng2/lazyLoadNgModule";
1212
export * from "./ng2/providers";
1313
export * from "./ng2/location";
1414
export * from "./ng2/directives/directives";
1515
export * from "./ng2/statebuilders/views";
16+
export * from "./ng2/statebuilders/lazyLoadNgModuleResolvable";
17+
export * from "./ng2/uiRouterNgModule";
1618
export * from "./ng2/uiRouterConfig";
1719

src/ng2/directives/directives.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@ export * from "./uiSrefActive";
1919

2020
/**
2121
* References to the UI-Router directive classes, for use within a @Component's `directives:` property
22-
*
23-
* @example
24-
* ```js
25-
*
26-
* Component({
27-
* selector: 'my-cmp',
28-
* directives: [UIROUTER_DIRECTIVES],
29-
* template: '<a uiSref="foo">Foo</a>'
30-
* })
31-
* ```
22+
* @deprecated use [[UIRouterModule]]
3223
*/
3324
export let UIROUTER_DIRECTIVES = [UISref, AnchorUISref, UIView, UISrefActive, UISrefStatus];

src/ng2/interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @module ng2 */ /** */
22
import {StateDeclaration, _ViewDeclaration} from "../state/interface";
33
import {Transition} from "../transition/transition";
4-
import {Type} from "@angular/core";
4+
import {Type, OpaqueToken} from "@angular/core";
55
import {HookResult} from "../transition/interface";
66

77
/**
@@ -342,4 +342,4 @@ export interface Ng2Component {
342342
uiCanExit(): HookResult;
343343
}
344344

345-
export const NG2_INJECTOR_TOKEN = {};
345+
export const NG2_INJECTOR_TOKEN = new OpaqueToken("NgModule Injector");

src/ng2/lazyLoadNgModule.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {Transition} from "../transition/transition";
2+
import {NG2_INJECTOR_TOKEN, Ng2StateDeclaration} from "./interface";
3+
import {UIROUTER_STATES_TOKEN} from "./uiRouterNgModule";
4+
5+
import {NgModuleFactoryLoader, NgModuleRef, Injector, NgModuleFactory} from "@angular/core";
6+
import {unnestR} from "../common/common";
7+
8+
/**
9+
* Returns a function which lazy loads a nested module
10+
*
11+
* Use this function as a [[StateDeclaration.lazyLoad]] property to lazy load a state tree (an NgModule).
12+
*
13+
* @param path the path to the module source code.
14+
* @returns A function which takes a transition, then:
15+
*
16+
* - Gets the Injector (scoped properly for the destination state)
17+
* - Loads and creates the NgModule
18+
* - Finds the "replacement state" for the target state, and adds the new NgModule Injector to it (as a resolve)
19+
*
20+
* returns the new states array
21+
*/
22+
export function loadNgModule(path: string) {
23+
/** Get the parent NgModule Injector (from resolves) */
24+
const getNg2Injector = (transition: Transition) =>
25+
transition.injector().getAsync(NG2_INJECTOR_TOKEN);
26+
27+
/**
28+
* Lazy loads the NgModule using the NgModuleFactoryLoader
29+
*
30+
* Use the parent NgModule's Injector to:
31+
* - Find the correct NgModuleFactoryLoader
32+
* - Load the new NgModuleFactory from the path string (async)
33+
* - Create the new NgModule
34+
*/
35+
const createNg2Module = (path: string, ng2Injector: Injector) =>
36+
ng2Injector.get(NgModuleFactoryLoader).load(path)
37+
.then((factory: NgModuleFactory<any>) => factory.create(ng2Injector));
38+
39+
/**
40+
* Apply the Lazy Loaded NgModule's Injector to the newly loaded state tree.
41+
*
42+
* Lazy loading uses a placeholder state which is removed (and replaced) after the module is loaded.
43+
* The NgModule should include a state with the same name as the placeholder.
44+
*
45+
* Find the *newly loaded state* with the same name as the *placeholder state*.
46+
* The NgModule's Injector (and ComponentFactoryResolver) will be added to that state.
47+
* The Injector/Factory are used when creating Components for the `replacement` state and all its children.
48+
*/
49+
function applyNgModuleToNewStates(transition: Transition, ng2Module: NgModuleRef<any>): Ng2StateDeclaration[] {
50+
var targetName = transition.to().name;
51+
let newStates: Ng2StateDeclaration[] = ng2Module.injector.get(UIROUTER_STATES_TOKEN).reduce(unnestR, []);
52+
let replacementState = newStates.find(state => state.name === targetName);
53+
54+
if (!replacementState) {
55+
throw new Error(`The module that was loaded from ${path} should have a state named '${targetName}'` +
56+
`, but it only had: ${(newStates || []).map(s=>s.name).join(', ')}`);
57+
}
58+
59+
// Add the injector as a resolve.
60+
replacementState['_ngModuleInjector'] = ng2Module.injector;
61+
62+
return newStates;
63+
}
64+
65+
return (transition: Transition) => getNg2Injector(transition)
66+
.then((injector: Injector) => createNg2Module(path, injector))
67+
.then((moduleRef: NgModuleRef<any>) => applyNgModuleToNewStates(transition, moduleRef))
68+
}

src/ng2/providers.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,27 @@ import {UIRouterLocation} from "./location";
6464
import {services} from "../common/coreservices";
6565
import {ProviderLike} from "../state/interface";
6666
import {Resolvable} from "../resolve/resolvable";
67+
import {ngModuleResolvablesBuilder} from "./statebuilders/lazyLoadNgModuleResolvable";
6768

6869
let uiRouterFactory = (routerConfig: UIRouterConfig, location: UIRouterLocation, injector: Injector) => {
6970
services.$injector.get = injector.get.bind(injector);
70-
let router = new UIRouter();
7171

7272
location.init();
7373

74+
75+
// ----------------- Create router -----------------
76+
// Create a new ng2 UIRouter and configure it for ng2
77+
let router = new UIRouter();
78+
let registry = router.stateRegistry;
79+
80+
// ----------------- Configure for ng2 -------------
81+
// Apply ng2 ui-view handling code
7482
router.viewService.viewConfigFactory("ng2", (path: PathNode[], config: Ng2ViewDeclaration) => new Ng2ViewConfig(path, config));
75-
router.stateRegistry.decorator('views', ng2ViewsBuilder);
83+
registry.decorator('views', ng2ViewsBuilder);
84+
85+
// Apply statebuilder decorator for ng2 NgModule registration
86+
registry.stateQueue.flush(router.stateService);
87+
registry.decorator('resolvables', ngModuleResolvablesBuilder);
7688

7789
router.stateRegistry.stateQueue.autoFlush(router.stateService);
7890

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** @module ng2 */ /** */
2+
import {State} from "../../state/stateObject";
3+
import {NG2_INJECTOR_TOKEN} from "../interface";
4+
import {Resolvable} from "../../resolve/resolvable";
5+
6+
/**
7+
* This is a [[StateBuilder.builder]] function which enables lazy Ng2Module support.
8+
*
9+
* See [[loadNgModule]]
10+
*
11+
* After lazy loading an NgModule, any Components from that module should be created using the NgModule's Injecjtor.
12+
* The NgModule's ComponentFactory only exists inside that Injector.
13+
*
14+
* After lazy loading an NgModule, it is stored on the root state of the lazy loaded state tree.
15+
* When instantiating Component, the parent Component's Injector is merged with the NgModule injector.
16+
*/
17+
export function ngModuleResolvablesBuilder(state: State, parentFn: Function): Resolvable[] {
18+
let resolvables: Resolvable[] = parentFn(state);
19+
let injector = state.self['_ngModuleInjector'];
20+
return !injector ? resolvables : resolvables.concat(Resolvable.fromData(NG2_INJECTOR_TOKEN, injector));
21+
}

src/ng2/routerModule.ts renamed to src/ng2/uiRouterNgModule.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import {NgModule, NgModuleMetadataType, OpaqueToken} from "@angular/core";
33
import {UIROUTER_DIRECTIVES} from "./directives/directives";
44
import {UIROUTER_PROVIDERS} from "./providers";
55
import {UIView} from "./directives/uiView";
6-
import {uniqR} from "../common/common";
6+
import {uniqR, flattenR} from "../common/common";
77

88
@NgModule({
99
declarations: [UIROUTER_DIRECTIVES],
1010
exports: [UIROUTER_DIRECTIVES],
1111
entryComponents: [UIView],
1212
providers: [UIROUTER_PROVIDERS]
1313
})
14-
export class _UIRouterModule {}
14+
export class UIRouterRootModule {}
1515

1616
/**
1717
* A module declaration lteral, including UI-Router states.
@@ -23,7 +23,7 @@ export interface UIRouterModuleMetadata extends NgModuleMetadataType {
2323
states?: Ng2StateDeclaration[]
2424
}
2525

26-
export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouterStates");
26+
export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouter States");
2727

2828
/**
2929
* Declares a NgModule with UI-Router states
@@ -51,17 +51,19 @@ export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouterStates");
5151
*/
5252
export function UIRouterModule(moduleMetaData: UIRouterModuleMetadata) {
5353
let states = moduleMetaData.states || [];
54+
var statesProvider = { provide: UIROUTER_STATES_TOKEN, useValue: states, multi: true };
5455

5556
// Get the component classes for all views for all states in the module
56-
let components = states.map(state => state.views || { $default: state })
57+
let routedComponents = states.reduce(flattenR, [])
58+
.map(state => state.views || { $default: state })
5759
.map(viewObj => Object.keys(viewObj).map(key => viewObj[key].component))
5860
.reduce((acc, arr) => acc.concat(arr), [])
5961
.filter(x => typeof x === 'function' && x !== UIView);
6062

61-
moduleMetaData.imports = <any[]> (moduleMetaData.imports || []).concat(_UIRouterModule).reduce(uniqR, []);
62-
moduleMetaData.declarations = <any[]> (moduleMetaData.declarations || []).concat(components).reduce(uniqR, []);
63-
moduleMetaData.entryComponents = <any[]> (moduleMetaData.entryComponents || []).concat(components).reduce(uniqR, []);
64-
moduleMetaData.providers = (moduleMetaData.providers || []).concat({ provide: UIROUTER_STATES_TOKEN, useValue: states });
63+
moduleMetaData.imports = <any[]> (moduleMetaData.imports || []).concat(UIRouterRootModule).reduce(uniqR, []);
64+
moduleMetaData.declarations = <any[]> (moduleMetaData.declarations || []).concat(routedComponents).reduce(uniqR, []);
65+
moduleMetaData.entryComponents = <any[]> (moduleMetaData.entryComponents || []).concat(routedComponents).reduce(uniqR, []);
66+
moduleMetaData.providers = (moduleMetaData.providers || []).concat(statesProvider);
6567

6668
return function(moduleClass) {
6769
return NgModule(moduleMetaData)(moduleClass);

src/resolve/resolvable.ts

+3
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,7 @@ export class Resolvable implements ResolvableLiteral {
167167
clone(): Resolvable {
168168
return new Resolvable(this);
169169
}
170+
171+
static fromData = (token: any, data: any) =>
172+
new Resolvable(token, () => data, null, null, data);
170173
}

src/state/interface.ts

+9
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,15 @@ export interface StateDeclaration {
520520
*/
521521
onExit?: TransitionStateHookFn;
522522

523+
/**
524+
* A function that lazy loads a state tree.
525+
526+
527+
*
528+
* @param transition
529+
*/
530+
lazyLoad?: (transition: Transition) => Promise<StateDeclaration[]>;
531+
523532
/**
524533
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
525534
*/

src/state/stateMatcher.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import {isString} from "../common/predicates";
33
import {StateOrName} from "./interface";
44
import {State} from "./stateObject";
5+
import {Glob} from "../common/glob";
6+
import {values} from "../common/common";
57

68
export class StateMatcher {
79
constructor (private _states: { [key: string]: State }) { }
@@ -22,6 +24,17 @@ export class StateMatcher {
2224

2325
if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) {
2426
return state;
27+
} else if (isStr) {
28+
let matches = values(this._states)
29+
.filter(state => !!state.lazyLoad)
30+
.map(state => ({ state, glob: new Glob(state.name + ".**")}))
31+
.filter(({state, glob}) => glob.matches(name))
32+
.map(({state, glob}) => state);
33+
34+
if (matches.length > 1) {
35+
console.log(`stateMatcher.find: Found multiple matches for ${name} using glob: `, matches.map(match => match.name));
36+
}
37+
return matches[0];
2538
}
2639
return undefined;
2740
}

src/state/stateObject.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export class State {
4444
public onExit: TransitionStateHookFn;
4545
public onRetain: TransitionStateHookFn;
4646
public onEnter: TransitionStateHookFn;
47+
public lazyLoad: (transition: Transition) => Promise<StateDeclaration[]>;
4748

4849
redirectTo: (
4950
string |

src/transition/transitionService.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {registerLoadEnteringViews, registerActivateViews} from "../hooks/views";
1717
import {registerUpdateUrl} from "../hooks/url";
1818
import {registerRedirectToHook} from "../hooks/redirectTo";
1919
import {registerOnExitHook, registerOnRetainHook, registerOnEnterHook} from "../hooks/onEnterExitRetain";
20+
import {registerLazyLoadHook} from "../hooks/lazyLoadStates";
2021

2122
/**
2223
* The default [[Transition]] options.
@@ -50,12 +51,13 @@ export class TransitionService implements IHookRegistry {
5051
public $view: ViewService;
5152

5253
/**
53-
* This object has hook de-registration functions.
54+
* This object has hook de-registration functions for the built-in hooks.
5455
* This can be used by third parties libraries that wish to customize the behaviors
5556
*
5657
* @hidden
5758
*/
5859
_deregisterHookFns: {
60+
redirectTo: Function;
5961
onExit: Function;
6062
onRetain: Function;
6163
onEnter: Function;
@@ -64,7 +66,7 @@ export class TransitionService implements IHookRegistry {
6466
loadViews: Function;
6567
activateViews: Function;
6668
updateUrl: Function;
67-
redirectTo: Function;
69+
lazyLoad: Function;
6870
};
6971

7072
constructor(private _router: UIRouter) {
@@ -96,6 +98,9 @@ export class TransitionService implements IHookRegistry {
9698

9799
// After globals.current is updated at priority: 10000
98100
fns.updateUrl = registerUpdateUrl(this);
101+
102+
// Lazy load state trees
103+
fns.lazyLoad = registerLazyLoadHook(this);
99104
}
100105

101106
/** @inheritdoc */

0 commit comments

Comments
 (0)