Skip to content

Commit

Permalink
feat(futureState): States with a .** name suffix (i.e., foo.**) a…
Browse files Browse the repository at this point in the history
…re 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: () => ... }
```
  • Loading branch information
christopherthielen committed Nov 29, 2016
1 parent 459ae05 commit ec50da4
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 130 deletions.
115 changes: 115 additions & 0 deletions src/hooks/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/** @module hooks */ /** */
import {Transition} from "../transition/transition";
import {TransitionService} from "../transition/transitionService";
import {TransitionHookFn} from "../transition/interface";
import {StateDeclaration, LazyLoadResult} from "../state/interface";
import {State} from "../state/stateObject";
import {services} from "../common/coreservices";

/**
* A [[TransitionHookFn]] that performs lazy loading
*
* When entering a state "abc" which has a `lazyLoad` function defined:
* - Invoke the `lazyLoad` function (unless it is already in process)
* - Flag the hook function as "in process"
* - The function should return a promise (that resolves when lazy loading is complete)
* - Wait for the promise to settle
* - If the promise resolves to a [[LazyLoadResult]], then register those states
* - Flag the hook function as "not in process"
* - If the hook was successful
* - Remove the `lazyLoad` function from the state declaration
* - If all the hooks were successful
* - Retry the transition (by returning a TargetState)
*
* ```
* .state('abc', {
* component: 'fooComponent',
* lazyLoad: () => System.import('./fooComponent')
* });
* ```
*
* See [[StateDeclaration.lazyLoad]]
*/
const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
const transitionSource = (trans: Transition) =>
trans.redirectedFrom() ? transitionSource(trans.redirectedFrom()) : trans.options().source;

function retryOriginalTransition() {
if (transitionSource(transition) === 'url') {
let loc = services.location, path = loc.path(), search = loc.search(), hash = loc.hash();

let matchState = state =>
[state, state.url && state.url.exec(path, search, hash)];

let matches = transition.router.stateRegistry.get()
.map(s => s.$$state())
.map(matchState)
.filter(([state, params]) => !!params);

if (matches.length) {
let [state, params] = matches[0];
return transition.router.stateService.target(state, params, transition.options());
}

transition.router.urlRouter.sync();
return;
}

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

let promises = transition.entering()
.filter(state => !!state.lazyLoad)
.map(state => lazyLoadState(transition, state));

return services.$q.all(promises).then(retryOriginalTransition);
};

export const registerLazyLoadHook = (transitionService: TransitionService) =>
transitionService.onBefore({ entering: (state) => !!state.lazyLoad }, lazyLoadHook);


/**
* Invokes a state's lazy load function
*
* @param transition a Transition context
* @param state the state to lazy load
* @returns A promise for the lazy load result
*/
export function lazyLoadState(transition: Transition, state: StateDeclaration): Promise<LazyLoadResult> {
let lazyLoadFn = state.lazyLoad;

// Store/get the lazy load promise on/from the hookfn so it doesn't get re-invoked
let promise = lazyLoadFn['_promise'];
if (!promise) {
const success = (result) => {
delete state.lazyLoad;
delete state.$$state().lazyLoad;
delete lazyLoadFn['_promise'];
return result;
};

const error = (err) => {
delete lazyLoadFn['_promise'];
return services.$q.reject(err);
};

promise = lazyLoadFn['_promise'] =
services.$q.when(lazyLoadFn(transition, state))
.then(updateStateRegistry)
.then(success, error);
}

/** Register any lazy loaded state definitions */
function updateStateRegistry(result: LazyLoadResult) {
if (result && Array.isArray(result.states)) {
result.states.forEach(state => transition.router.stateRegistry.register(state));
}
return result;
}

return promise;
}
76 changes: 0 additions & 76 deletions src/hooks/lazyLoadStates.ts

This file was deleted.

107 changes: 74 additions & 33 deletions src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,66 +504,106 @@ export interface StateDeclaration {
onExit?: TransitionStateHookFn;

/**
* A function which lazy loads the state definition (and child state definitions)
* A function used to lazy load code
*
* A state which has a `lazyLoad` function is treated as a **temporary
* placeholder** for a state definition that will be lazy loaded some time
* in the future.
* These temporary placeholder states are called "**Future States**".
* The `lazyLoad` function is invoked before the state is activated.
* The transition waits while the code is loading.
*
* The function should load the code that is required to activate the state.
* For example, it may load a component class, or some service code.
* The function must retur a promise which resolves when loading is complete.
*
* #### `lazyLoad`:
* For example, this code lazy loads a service before the `abc` state is activated:
*
* A future state's `lazyLoad` function should return a Promise to lazy load the
* code for one or more lazy loaded [[StateDeclaration]] objects.
* ```
* .state('abc', {
* lazyLoad: (transition, state) => System.import('./abcService')
* }
* ```
*
* If the promise resolves to an object with a `states: []` array,
* the lazy loaded states will be registered with the [[StateRegistry]].
* Generally, of the lazy loaded states should have the same name as the future state;
* then it will **replace the future state placeholder** in the registry.
* The `abcService` file is imported and loaded
* (it is assumed that the `abcService` file knows how to register itself as a service).
*
* In any case, when the promise successfully resolves, the placeholder Future State will be deregistered.
* #### Lifecycle
*
* #### `url`
* - The `lazyLoad` function is invoked if a transition is going to enter the state.
* - The function is invoked before the transition starts (using an `onBefore` transition hook).
* - The function is only invoked once; while the `lazyLoad` function is loading code, it will not be invoked again.
* For example, if the user double clicks a ui-sref, `lazyLoad` is only invoked once even though there were two transition attempts.
* Instead, the existing lazy load promise is re-used.
* - When the promise resolves successfully, the `lazyLoad` property is deleted from the state declaration.
* - If the promise resolves to a [[LazyLoadResult]] which has an array of `states`, those states are registered.
* - The original transition is retried (this time without the `lazyLoad` property present).
*
* A future state's `url` property acts as a wildcard.
* - If the `lazyLoad` function fails, then the transition also fails.
* The failed transition (and the `lazyLoad` function) could potentially be retried by the user.
*
* UI-Router matches all paths that begin with the `url`.
* It effectively appends `.*` to the internal regular expression.
* ### Lazy loading state definitions (Future States)
*
* #### `name`
* State definitions can also be lazy loaded.
* This might be desirable when building large, multi-module applications.
*
* A future state's `name` property acts as a wildcard.
* To lazy load state definitions, a Future State should be registered as a placeholder.
* When the state definitions are lazy loaded, the Future State is deregistered.
*
* It matches any state name that starts with the `name`.
* UI-Router effectively matches the future state using a `.**` [[Glob]] appended to the `name`.
* A future state can act as a placeholder for a single state, or for an entire module of states and substates.
* A future state should have:
*
* @example
* #### states.js
* - A `name` which ends in `.**`.
* A future state's `name` property acts as a wildcard [[Glob]].
* It matches any state name that starts with the `name` (including child states that are not yet loaded).
* - A `url` prefix.
* A future state's `url` property acts as a wildcard.
* UI-Router matches all paths that begin with the `url`.
* It effectively appends `.*` to the internal regular expression.
* When the prefix matches, the future state will begin loading.
* - A `lazyLoad` function.
* This function should should return a Promise to lazy load the code for one or more [[StateDeclaration]] objects.
* It should return a [[LazyLoadResult]].
* Generally, one of the lazy loaded states should have the same name as the future state.
* The new state will then **replace the future state placeholder** in the registry.
*
* ### Additional resources
*
* For in depth information on lazy loading and Future States, see the [Lazy Loading Guide](https://ui-router.github.io/guides/lazyload).
*
* #### Example: states.js
* ```js
*
* // This child state is a lazy loaded future state
* // The `lazyLoad` function loads the final state definition
* {
* name: 'parent.child',
* url: '/child',
* lazyLoad: () => System.import('./child.state.js')
* name: 'parent.**',
* url: '/parent',
* lazyLoad: () => System.import('./lazy.states.js')
* }
* ```
*
* #### child.state.js
* #### Example: lazy.states.js
*
* This file is lazy loaded. It exports an array of states.
*
* ```js
* import {ChildComponent} from "./child.component.js";
* import {ParentComponent} from "./parent.component.js";
*
* let childState = {
* // This fully defined state replaces the future state
* let parentState = {
* // the name should match the future state
* name: 'parent',
* url: '/parent/:parentId',
* component: ParentComponent,
* resolve: {
* parentData: ($transition$, ParentService) =>
* ParentService.get($transition$.params().parentId)
* }
* }
*
* let childState = {
* name: 'parent.child',
* url: '/child/:childId',
* params: {
* id: "default"
* childId: "default"
* },
* resolve: {
* childData: ($transition$, ChildService) =>
Expand All @@ -572,19 +612,20 @@ export interface StateDeclaration {
* };
*
* // This array of states will be registered by the lazyLoad hook
* let result = {
* states: [ childState ]
* let lazyLoadResults = {
* states: [ parentState, childState ]
* };
*
* export default result;
* export default lazyLoadResults;
* ```
*
* @param transition the [[Transition]] that is activating the future state
* @param state the [[StateDeclaration]] that the `lazyLoad` function is declared on
* @return a Promise to load the states.
* Optionally, if the promise resolves to a [[LazyLoadResult]],
* the states will be registered with the [[StateRegistry]].
*/
lazyLoad?: (transition: Transition) => Promise<LazyLoadResult>;
lazyLoad?: (transition: Transition, state: StateDeclaration) => Promise<LazyLoadResult>;

/**
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
Expand Down
6 changes: 3 additions & 3 deletions src/state/stateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ interface Builders {


function nameBuilder(state: State) {
if (state.lazyLoad)
state.name = state.self.name + ".**";
return state.name;
}

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

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

Expand Down
Loading

0 comments on commit ec50da4

Please sign in to comment.