Skip to content

Commit 8ecb6c6

Browse files
feat(lazyLoad): Add state.lazyLoad hook to lazy load a tree of states
- Retry URL sync by matching state*.url - Make registration of new states (by the lazyload hook) optional - Reuse lazy load promise, if one is in progress - Refactor so ng1-to-ng2 UIRouter provider is implicit - When state has a .lazyLoad, decorate state.name with `.**` wildcard Closes #146 Closes #2739
1 parent d1dff31 commit 8ecb6c6

File tree

7 files changed

+347
-51
lines changed

7 files changed

+347
-51
lines changed

src/hooks/lazyLoadStates.ts

+27-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import {Transition} from "../transition/transition";
33
import {TransitionService} from "../transition/transitionService";
44
import {TransitionHookFn} from "../transition/interface";
5-
import {StateDeclaration} from "../state/interface";
5+
import {StateDeclaration, LazyLoadResult} from "../state/interface";
66
import {State} from "../state/stateObject";
77
import {services} from "../common/coreservices";
88

@@ -22,42 +22,49 @@ import {services} from "../common/coreservices";
2222
*/
2323
const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
2424
var toState = transition.to();
25+
let registry = transition.router.stateRegistry;
2526

26-
function retryOriginalTransition(newStates: State[]) {
27+
function retryOriginalTransition() {
2728
if (transition.options().source === 'url') {
28-
let loc = services.location;
29-
let path = loc.path(), search = loc.search(), hash = loc.hash();
29+
let loc = services.location, path = loc.path(), search = loc.search(), hash = loc.hash();
30+
31+
let matchState = state => [state, state.url && state.url.exec(path, search, hash)];
32+
let matches = registry.get().map(s => s.$$state()).map(matchState).filter(([state, params]) => !!params);
3033

31-
let matchState = state => [state, state.url.exec(path, search, hash)];
32-
let matches = newStates.map(matchState).filter(([state, params]) => !!params);
3334
if (matches.length) {
3435
let [state, params] = matches[0];
3536
return transition.router.stateService.target(state, params, transition.options());
3637
}
3738
transition.router.urlRouter.sync();
3839
}
3940

40-
let state = transition.targetState().identifier();
41-
let params = transition.params();
42-
let options = transition.options();
43-
return transition.router.stateService.target(state, params, options);
41+
// The original transition was not triggered via url sync
42+
// The lazy state should be loaded now, so re-try the original transition
43+
let orig = transition.targetState();
44+
return transition.router.stateService.target(orig.identifier(), orig.params(), orig.options());
4445
}
4546

4647
/**
4748
* Replace the placeholder state with the newly loaded states from the NgModule.
4849
*/
49-
function updateStateRegistry(newStates: StateDeclaration[]) {
50-
let registry = transition.router.stateRegistry;
51-
let placeholderState = transition.to();
50+
function updateStateRegistry(result: LazyLoadResult) {
51+
// deregister placeholder state
52+
registry.deregister(transition.$to());
53+
if (result && Array.isArray(result.states)) {
54+
result.states.forEach(state => registry.register(state));
55+
}
56+
}
5257

53-
registry.deregister(placeholderState);
54-
newStates.forEach(state => registry.register(state));
55-
return newStates.map(state => registry.get(state).$$state());
58+
let hook = toState.lazyLoad;
59+
// Store/get the lazy load promise on/from the hookfn so it doesn't get re-invoked
60+
let promise = hook['_promise'];
61+
if (!promise) {
62+
promise = hook['_promise'] = hook(transition).then(updateStateRegistry);
63+
const cleanup = () => delete hook['_promise'];
64+
promise.catch(cleanup, cleanup);
5665
}
57-
58-
return toState.lazyLoad(transition)
59-
.then(updateStateRegistry)
60-
.then(retryOriginalTransition)
66+
67+
return promise.then(retryOriginalTransition);
6168
};
6269

6370
export const registerLazyLoadHook = (transitionService: TransitionService) =>

src/ng2/lazyLoadNgModule.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {UIROUTER_STATES_TOKEN} from "./uiRouterNgModule";
55

66
import {NgModuleFactoryLoader, NgModuleRef, Injector, NgModuleFactory} from "@angular/core";
77
import {unnestR} from "../common/common";
8+
import {LazyLoadResult} from "../state/interface";
89

910
/**
1011
* Returns a function which lazy loads a nested module
@@ -20,7 +21,7 @@ import {unnestR} from "../common/common";
2021
*
2122
* returns the new states array
2223
*/
23-
export function loadNgModule(path: string) {
24+
export function loadNgModule(path: string): (transition: Transition) => Promise<LazyLoadResult> {
2425
/** Get the parent NgModule Injector (from resolves) */
2526
const getNg2Injector = (transition: Transition) =>
2627
transition.injector().getAsync(NG2_INJECTOR_TOKEN);
@@ -34,8 +35,8 @@ export function loadNgModule(path: string) {
3435
* - Create the new NgModule
3536
*/
3637
const createNg2Module = (path: string, ng2Injector: Injector) =>
37-
ng2Injector.get(NgModuleFactoryLoader).load(path)
38-
.then((factory: NgModuleFactory<any>) => factory.create(ng2Injector));
38+
ng2Injector.get(NgModuleFactoryLoader).load(path).then((factory: NgModuleFactory<any>) =>
39+
factory.create(ng2Injector));
3940

4041
/**
4142
* Apply the Lazy Loaded NgModule's Injector to the newly loaded state tree.
@@ -47,7 +48,7 @@ export function loadNgModule(path: string) {
4748
* The NgModule's Injector (and ComponentFactoryResolver) will be added to that state.
4849
* The Injector/Factory are used when creating Components for the `replacement` state and all its children.
4950
*/
50-
function applyNgModuleToNewStates(transition: Transition, ng2Module: NgModuleRef<any>): Ng2StateDeclaration[] {
51+
function applyNgModuleToNewStates(transition: Transition, ng2Module: NgModuleRef<any>): LazyLoadResult {
5152
var targetName = transition.to().name;
5253
let newStates: Ng2StateDeclaration[] = ng2Module.injector.get(UIROUTER_STATES_TOKEN).reduce(unnestR, []);
5354
let replacementState = newStates.find(state => state.name === targetName);
@@ -60,7 +61,8 @@ export function loadNgModule(path: string) {
6061
// Add the injector as a resolve.
6162
replacementState['_ngModuleInjector'] = ng2Module.injector;
6263

63-
return newStates;
64+
// Return states to be registered by the lazyLoadHook
65+
return { states: newStates };
6466
}
6567

6668
return (transition: Transition) => getNg2Injector(transition)

src/ng2/providers.ts

+3-19
Original file line numberDiff line numberDiff line change
@@ -70,29 +70,13 @@ import {UIROUTER_STATES_TOKEN} from "./uiRouterNgModule";
7070
import {UIRouterRx} from "./rx";
7171
import {LocationStrategy, HashLocationStrategy, PathLocationStrategy} from "@angular/common";
7272

73-
export const NG1_UIROUTER_TOKEN = new OpaqueToken("$uiRouter");
74-
7573
/**
7674
* This is a factory function for a UIRouter instance
7775
*
7876
* Creates a UIRouter instance and configures it for Angular 2, then invokes router bootstrap.
7977
* This function is used as an Angular 2 `useFactory` Provider.
8078
*/
81-
let uiRouterFactory = (injector: Injector) => {
82-
// ----------------- ng1-to-ng2 short circuit ------
83-
// Before creating a UIRouter instance, see if there is
84-
// already one created (from ng1-to-ng2 as NG1_UIROUTER_TOKEN)
85-
let $uiRouter = injector.get(NG1_UIROUTER_TOKEN, null);
86-
if ($uiRouter) return $uiRouter;
87-
88-
89-
// ----------------- Get DI dependencies -----------
90-
// Get the DI deps manually from the injector
91-
// (no UIRouterConfig is provided when in hybrid mode)
92-
let routerConfig: UIRouterConfig = injector.get(UIRouterConfig);
93-
let location: UIRouterLocation = injector.get(UIRouterLocation);
94-
95-
79+
let uiRouterFactory = (routerConfig: UIRouterConfig, location: UIRouterLocation, injector: Injector) => {
9680
// ----------------- Monkey Patches ----------------
9781
// Monkey patch the services.$injector to the ng2 Injector
9882
services.$injector.get = injector.get.bind(injector);
@@ -131,7 +115,7 @@ let uiRouterFactory = (injector: Injector) => {
131115
routerConfig.configure(router);
132116

133117
// Register the states from the root NgModule [[UIRouterModule]]
134-
let states = (injector.get(UIROUTER_STATES_TOKEN) || []).reduce(flattenR, []);
118+
let states = injector.get(UIROUTER_STATES_TOKEN, []).reduce(flattenR, []);
135119
states.forEach(state => registry.register(state));
136120

137121
// Start monitoring the URL
@@ -145,7 +129,7 @@ let uiRouterFactory = (injector: Injector) => {
145129
};
146130

147131
export const _UIROUTER_INSTANCE_PROVIDERS: Provider[] = [
148-
{ provide: UIRouter, useFactory: uiRouterFactory, deps: [Injector] },
132+
{ provide: UIRouter, useFactory: uiRouterFactory, deps: [UIRouterConfig, UIRouterLocation, Injector] },
149133
{ provide: UIRouterLocation, useClass: UIRouterLocation },
150134
];
151135

src/state/interface.ts

+86-5
Original file line numberDiff line numberDiff line change
@@ -521,20 +521,101 @@ export interface StateDeclaration {
521521
onExit?: TransitionStateHookFn;
522522

523523
/**
524-
* A function that lazy loads a state tree.
525-
526-
524+
* A function which lazy loads the state (and child states)
527525
*
528-
* @param transition
526+
* A state which has a `lazyLoad` function is treated as a **temporary
527+
* placeholder** for a state definition that will be lazy loaded some time
528+
* in the future.
529+
* These temporary placeholder states are called "**Future States**".
530+
*
531+
*
532+
* ### Future State placeholders
533+
*
534+
* #### `lazyLoad`:
535+
*
536+
* A future state's `lazyLoad` function should return a Promise for an array of
537+
* lazy loaded [[StateDeclaration]] objects.
538+
*
539+
* The lazy loaded states are registered with the [[StateRegistry]].
540+
* One of the lazy loaded states must have the same name as the future state;
541+
* it will **replace the future state placeholder** in the registry.
542+
*
543+
* #### `url`
544+
* A future state's `url` property acts as a wildcard.
545+
*
546+
* UI-Router matches all paths that begin with the `url`.
547+
* It effectively appends `.*` to the internal regular expression.
548+
*
549+
* #### `name`
550+
*
551+
* A future state's `name` property acts as a wildcard.
552+
*
553+
* It matches any state name that starts with the `name`.
554+
* UI-Router effectively matches the future state using a `.**` [[Glob]] appended to the `name`.
555+
*
556+
* ---
557+
*
558+
* Future state placeholders should only define `lazyLoad`, `name`, and `url`.
559+
* Any additional properties should only be defined on the state that will eventually be lazy loaded.
560+
*
561+
* @example
562+
* #### states.js
563+
* ```js
564+
*
565+
* // The parent state is not lazy loaded
566+
* {
567+
* name: 'parent',
568+
* url: '/parent',
569+
* component: ParentComponent
570+
* }
571+
*
572+
* // This child state is a lazy loaded future state
573+
* // The `lazyLoad` function loads the final state definition
574+
* {
575+
* name: 'parent.child',
576+
* url: '/child',
577+
* lazyLoad: () => System.import('./child.state.js')
578+
* }
579+
* ```
580+
*
581+
* #### child.state.js
582+
*
583+
* This file is lazy loaded. It exports an array of states.
584+
*
585+
* ```js
586+
* import {ChildComponent} from "./child.component.js";
587+
*
588+
* let childState = {
589+
* // the name should match the future state
590+
* name: 'parent.child',
591+
* url: '/child/:childId',
592+
* params: {
593+
* id: "default"
594+
* },
595+
* resolve: {
596+
* childData: ($transition$, ChildService) =>
597+
* ChildService.get($transition$.params().childId)
598+
* }
599+
* };
600+
*
601+
* // The future state's lazyLoad imports this array of states
602+
* export default [ childState ];
603+
* ```
604+
*
605+
* @param transition the [[Transition]] that is activating the future state
529606
*/
530-
lazyLoad?: (transition: Transition) => Promise<StateDeclaration[]>;
607+
lazyLoad?: (transition: Transition) => Promise<LazyLoadResult>;
531608

532609
/**
533610
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
534611
*/
535612
reloadOnSearch?: boolean;
536613
}
537614

615+
export interface LazyLoadResult {
616+
states?: StateDeclaration[];
617+
}
618+
538619
export interface HrefOptions {
539620
relative?: StateOrName;
540621
lossy?: boolean;

src/state/stateBuilder.ts

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type BuilderFunction = (state: State, parent?: BuilderFunction) => any;
2626
interface Builders {
2727
[key: string]: BuilderFunction[];
2828

29+
name: BuilderFunction[];
2930
parent: BuilderFunction[];
3031
data: BuilderFunction[];
3132
url: BuilderFunction[];
@@ -38,6 +39,12 @@ interface Builders {
3839
}
3940

4041

42+
function nameBuilder(state: State) {
43+
if (state.lazyLoad)
44+
state.name = state.self.name + ".**";
45+
return state.name;
46+
}
47+
4148
function selfBuilder(state: State) {
4249
state.self.$$state = () => state;
4350
return state.self;
@@ -53,6 +60,11 @@ function dataBuilder(state: State) {
5360
const getUrlBuilder = ($urlMatcherFactoryProvider: UrlMatcherFactory, root: () => State) =>
5461
function urlBuilder(state: State) {
5562
let stateDec: StateDeclaration = <any> state;
63+
64+
if (stateDec && stateDec.url && stateDec.lazyLoad) {
65+
stateDec.url += "{remainder:any}"; // match any path (.*)
66+
}
67+
5668
const parsed = parseUrl(stateDec.url), parent = state.parent;
5769
const url = !parsed ? stateDec.url : $urlMatcherFactoryProvider.compile(parsed.val, {
5870
params: state.params || {},
@@ -212,6 +224,7 @@ export class StateBuilder {
212224
}
213225

214226
this.builders = {
227+
name: [ nameBuilder ],
215228
self: [ selfBuilder ],
216229
parent: [ parentBuilder ],
217230
data: [ dataBuilder ],

src/state/stateMatcher.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ export class StateMatcher {
2626
return state;
2727
} else if (isStr) {
2828
let matches = values(this._states)
29-
.filter(state => !!state.lazyLoad)
30-
.map(state => ({ state, glob: new Glob(state.name + ".**")}))
29+
.map(state => ({ state, glob: new Glob(state.name)}))
3130
.filter(({state, glob}) => glob.matches(name))
3231
.map(({state, glob}) => state);
3332

0 commit comments

Comments
 (0)