Skip to content

Commit ec50da4

Browse files
feat(futureState): States with a .** name suffix (i.e., foo.**) are considered future states
- instead of states with a `lazyLoad` fn feat(lazyLoad): Created `StateService.lazyLoad` method to imperatively lazy load a state Closes #8 feat(lazyLoad): Exported/exposed the `lazyLoadState` function - This can be used to manually trigger lazy loading of states. feat(lazyLoad): the `lazyLoad` hook can be used to lazy load anything (component code, etc) - Previously, `lazyLoad` was only used to load future states. - Now, `lazyLoad` can be used to load anything. - Previously, `lazyLoad` would forcibly de-register the future state. - Now, `lazyLoad` does not deregister the future state. - Now, the future state is deregistered when a normal state of the same name (without the .**) is registered. Closes #4 BREAKING CHANGE: Previously, a state with a `lazyLoad` function was considered a future state. Now, a state whose name ends with `.**` (i.e., a glob pattern which matches all children) is a future state. ### All future states should be given a name that ends in `.**`. Change your future states from: ``` { name: 'future', url: '/future', lazyLoad: () => ... } ``` to: ``` { name: 'future.**', url: '/future', lazyLoad: () => ... } ```
1 parent 459ae05 commit ec50da4

10 files changed

+462
-130
lines changed

src/hooks/lazyLoad.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/** @module hooks */ /** */
2+
import {Transition} from "../transition/transition";
3+
import {TransitionService} from "../transition/transitionService";
4+
import {TransitionHookFn} from "../transition/interface";
5+
import {StateDeclaration, LazyLoadResult} from "../state/interface";
6+
import {State} from "../state/stateObject";
7+
import {services} from "../common/coreservices";
8+
9+
/**
10+
* A [[TransitionHookFn]] that performs lazy loading
11+
*
12+
* When entering a state "abc" which has a `lazyLoad` function defined:
13+
* - Invoke the `lazyLoad` function (unless it is already in process)
14+
* - Flag the hook function as "in process"
15+
* - The function should return a promise (that resolves when lazy loading is complete)
16+
* - Wait for the promise to settle
17+
* - If the promise resolves to a [[LazyLoadResult]], then register those states
18+
* - Flag the hook function as "not in process"
19+
* - If the hook was successful
20+
* - Remove the `lazyLoad` function from the state declaration
21+
* - If all the hooks were successful
22+
* - Retry the transition (by returning a TargetState)
23+
*
24+
* ```
25+
* .state('abc', {
26+
* component: 'fooComponent',
27+
* lazyLoad: () => System.import('./fooComponent')
28+
* });
29+
* ```
30+
*
31+
* See [[StateDeclaration.lazyLoad]]
32+
*/
33+
const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
34+
const transitionSource = (trans: Transition) =>
35+
trans.redirectedFrom() ? transitionSource(trans.redirectedFrom()) : trans.options().source;
36+
37+
function retryOriginalTransition() {
38+
if (transitionSource(transition) === 'url') {
39+
let loc = services.location, path = loc.path(), search = loc.search(), hash = loc.hash();
40+
41+
let matchState = state =>
42+
[state, state.url && state.url.exec(path, search, hash)];
43+
44+
let matches = transition.router.stateRegistry.get()
45+
.map(s => s.$$state())
46+
.map(matchState)
47+
.filter(([state, params]) => !!params);
48+
49+
if (matches.length) {
50+
let [state, params] = matches[0];
51+
return transition.router.stateService.target(state, params, transition.options());
52+
}
53+
54+
transition.router.urlRouter.sync();
55+
return;
56+
}
57+
58+
// The original transition was not triggered via url sync
59+
// The lazy state should be loaded now, so re-try the original transition
60+
let orig = transition.targetState();
61+
return transition.router.stateService.target(orig.identifier(), orig.params(), orig.options());
62+
}
63+
64+
let promises = transition.entering()
65+
.filter(state => !!state.lazyLoad)
66+
.map(state => lazyLoadState(transition, state));
67+
68+
return services.$q.all(promises).then(retryOriginalTransition);
69+
};
70+
71+
export const registerLazyLoadHook = (transitionService: TransitionService) =>
72+
transitionService.onBefore({ entering: (state) => !!state.lazyLoad }, lazyLoadHook);
73+
74+
75+
/**
76+
* Invokes a state's lazy load function
77+
*
78+
* @param transition a Transition context
79+
* @param state the state to lazy load
80+
* @returns A promise for the lazy load result
81+
*/
82+
export function lazyLoadState(transition: Transition, state: StateDeclaration): Promise<LazyLoadResult> {
83+
let lazyLoadFn = state.lazyLoad;
84+
85+
// Store/get the lazy load promise on/from the hookfn so it doesn't get re-invoked
86+
let promise = lazyLoadFn['_promise'];
87+
if (!promise) {
88+
const success = (result) => {
89+
delete state.lazyLoad;
90+
delete state.$$state().lazyLoad;
91+
delete lazyLoadFn['_promise'];
92+
return result;
93+
};
94+
95+
const error = (err) => {
96+
delete lazyLoadFn['_promise'];
97+
return services.$q.reject(err);
98+
};
99+
100+
promise = lazyLoadFn['_promise'] =
101+
services.$q.when(lazyLoadFn(transition, state))
102+
.then(updateStateRegistry)
103+
.then(success, error);
104+
}
105+
106+
/** Register any lazy loaded state definitions */
107+
function updateStateRegistry(result: LazyLoadResult) {
108+
if (result && Array.isArray(result.states)) {
109+
result.states.forEach(state => transition.router.stateRegistry.register(state));
110+
}
111+
return result;
112+
}
113+
114+
return promise;
115+
}

src/hooks/lazyLoadStates.ts

-76
This file was deleted.

src/state/interface.ts

+74-33
Original file line numberDiff line numberDiff line change
@@ -504,66 +504,106 @@ export interface StateDeclaration {
504504
onExit?: TransitionStateHookFn;
505505

506506
/**
507-
* A function which lazy loads the state definition (and child state definitions)
507+
* A function used to lazy load code
508508
*
509-
* A state which has a `lazyLoad` function is treated as a **temporary
510-
* placeholder** for a state definition that will be lazy loaded some time
511-
* in the future.
512-
* These temporary placeholder states are called "**Future States**".
509+
* The `lazyLoad` function is invoked before the state is activated.
510+
* The transition waits while the code is loading.
513511
*
512+
* The function should load the code that is required to activate the state.
513+
* For example, it may load a component class, or some service code.
514+
* The function must retur a promise which resolves when loading is complete.
514515
*
515-
* #### `lazyLoad`:
516+
* For example, this code lazy loads a service before the `abc` state is activated:
516517
*
517-
* A future state's `lazyLoad` function should return a Promise to lazy load the
518-
* code for one or more lazy loaded [[StateDeclaration]] objects.
518+
* ```
519+
* .state('abc', {
520+
* lazyLoad: (transition, state) => System.import('./abcService')
521+
* }
522+
* ```
519523
*
520-
* If the promise resolves to an object with a `states: []` array,
521-
* the lazy loaded states will be registered with the [[StateRegistry]].
522-
* Generally, of the lazy loaded states should have the same name as the future state;
523-
* then it will **replace the future state placeholder** in the registry.
524+
* The `abcService` file is imported and loaded
525+
* (it is assumed that the `abcService` file knows how to register itself as a service).
524526
*
525-
* In any case, when the promise successfully resolves, the placeholder Future State will be deregistered.
527+
* #### Lifecycle
526528
*
527-
* #### `url`
529+
* - The `lazyLoad` function is invoked if a transition is going to enter the state.
530+
* - The function is invoked before the transition starts (using an `onBefore` transition hook).
531+
* - The function is only invoked once; while the `lazyLoad` function is loading code, it will not be invoked again.
532+
* For example, if the user double clicks a ui-sref, `lazyLoad` is only invoked once even though there were two transition attempts.
533+
* Instead, the existing lazy load promise is re-used.
534+
* - When the promise resolves successfully, the `lazyLoad` property is deleted from the state declaration.
535+
* - If the promise resolves to a [[LazyLoadResult]] which has an array of `states`, those states are registered.
536+
* - The original transition is retried (this time without the `lazyLoad` property present).
528537
*
529-
* A future state's `url` property acts as a wildcard.
538+
* - If the `lazyLoad` function fails, then the transition also fails.
539+
* The failed transition (and the `lazyLoad` function) could potentially be retried by the user.
530540
*
531-
* UI-Router matches all paths that begin with the `url`.
532-
* It effectively appends `.*` to the internal regular expression.
541+
* ### Lazy loading state definitions (Future States)
533542
*
534-
* #### `name`
543+
* State definitions can also be lazy loaded.
544+
* This might be desirable when building large, multi-module applications.
535545
*
536-
* A future state's `name` property acts as a wildcard.
546+
* To lazy load state definitions, a Future State should be registered as a placeholder.
547+
* When the state definitions are lazy loaded, the Future State is deregistered.
537548
*
538-
* It matches any state name that starts with the `name`.
539-
* UI-Router effectively matches the future state using a `.**` [[Glob]] appended to the `name`.
549+
* A future state can act as a placeholder for a single state, or for an entire module of states and substates.
550+
* A future state should have:
540551
*
541-
* @example
542-
* #### states.js
552+
* - A `name` which ends in `.**`.
553+
* A future state's `name` property acts as a wildcard [[Glob]].
554+
* It matches any state name that starts with the `name` (including child states that are not yet loaded).
555+
* - A `url` prefix.
556+
* A future state's `url` property acts as a wildcard.
557+
* UI-Router matches all paths that begin with the `url`.
558+
* It effectively appends `.*` to the internal regular expression.
559+
* When the prefix matches, the future state will begin loading.
560+
* - A `lazyLoad` function.
561+
* This function should should return a Promise to lazy load the code for one or more [[StateDeclaration]] objects.
562+
* It should return a [[LazyLoadResult]].
563+
* Generally, one of the lazy loaded states should have the same name as the future state.
564+
* The new state will then **replace the future state placeholder** in the registry.
565+
*
566+
* ### Additional resources
567+
*
568+
* For in depth information on lazy loading and Future States, see the [Lazy Loading Guide](https://ui-router.github.io/guides/lazyload).
569+
*
570+
* #### Example: states.js
543571
* ```js
544572
*
545573
* // This child state is a lazy loaded future state
546574
* // The `lazyLoad` function loads the final state definition
547575
* {
548-
* name: 'parent.child',
549-
* url: '/child',
550-
* lazyLoad: () => System.import('./child.state.js')
576+
* name: 'parent.**',
577+
* url: '/parent',
578+
* lazyLoad: () => System.import('./lazy.states.js')
551579
* }
552580
* ```
553581
*
554-
* #### child.state.js
582+
* #### Example: lazy.states.js
555583
*
556584
* This file is lazy loaded. It exports an array of states.
557585
*
558586
* ```js
559587
* import {ChildComponent} from "./child.component.js";
588+
* import {ParentComponent} from "./parent.component.js";
560589
*
561-
* let childState = {
590+
* // This fully defined state replaces the future state
591+
* let parentState = {
562592
* // the name should match the future state
593+
* name: 'parent',
594+
* url: '/parent/:parentId',
595+
* component: ParentComponent,
596+
* resolve: {
597+
* parentData: ($transition$, ParentService) =>
598+
* ParentService.get($transition$.params().parentId)
599+
* }
600+
* }
601+
*
602+
* let childState = {
563603
* name: 'parent.child',
564604
* url: '/child/:childId',
565605
* params: {
566-
* id: "default"
606+
* childId: "default"
567607
* },
568608
* resolve: {
569609
* childData: ($transition$, ChildService) =>
@@ -572,19 +612,20 @@ export interface StateDeclaration {
572612
* };
573613
*
574614
* // This array of states will be registered by the lazyLoad hook
575-
* let result = {
576-
* states: [ childState ]
615+
* let lazyLoadResults = {
616+
* states: [ parentState, childState ]
577617
* };
578618
*
579-
* export default result;
619+
* export default lazyLoadResults;
580620
* ```
581621
*
582622
* @param transition the [[Transition]] that is activating the future state
623+
* @param state the [[StateDeclaration]] that the `lazyLoad` function is declared on
583624
* @return a Promise to load the states.
584625
* Optionally, if the promise resolves to a [[LazyLoadResult]],
585626
* the states will be registered with the [[StateRegistry]].
586627
*/
587-
lazyLoad?: (transition: Transition) => Promise<LazyLoadResult>;
628+
lazyLoad?: (transition: Transition, state: StateDeclaration) => Promise<LazyLoadResult>;
588629

589630
/**
590631
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]

src/state/stateBuilder.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ interface Builders {
4040

4141

4242
function nameBuilder(state: State) {
43-
if (state.lazyLoad)
44-
state.name = state.self.name + ".**";
4543
return state.name;
4644
}
4745

@@ -61,7 +59,9 @@ const getUrlBuilder = ($urlMatcherFactoryProvider: UrlMatcherFactory, root: () =
6159
function urlBuilder(state: State) {
6260
let stateDec: StateDeclaration = <any> state;
6361

64-
if (stateDec && stateDec.url && stateDec.lazyLoad) {
62+
// For future states, i.e., states whose name ends with `.**`,
63+
// match anything that starts with the url prefix
64+
if (stateDec && stateDec.url && stateDec.name && stateDec.name.match(/\.\*\*$/)) {
6565
stateDec.url += "{remainder:any}"; // match any path (.*)
6666
}
6767

0 commit comments

Comments
 (0)