diff --git a/src/hooks/lazyLoadStates.ts b/src/hooks/lazyLoadStates.ts
new file mode 100644
index 000000000..06c67e446
--- /dev/null
+++ b/src/hooks/lazyLoadStates.ts
@@ -0,0 +1,63 @@
+import {Transition} from "../transition/transition";
+import {TransitionService} from "../transition/transitionService";
+import {TransitionHookFn} from "../transition/interface";
+import {StateDeclaration} from "../state/interface";
+import {State} from "../state/stateObject";
+import {services} from "../common/coreservices";
+
+/**
+ * A [[TransitionHookFn]] that lazy loads a state tree.
+ *
+ * When transitioning to a state "abc" which has a `lazyLoad` function defined:
+ * - Invoke the `lazyLoad` function
+ * - The function should return a promise for an array of lazy loaded [[StateDeclaration]]s
+ * - Wait for the promise to resolve
+ * - Deregister the original state "abc"
+ * - The original state definition is a placeholder for the lazy loaded states
+ * - Register the new states
+ * - Retry the transition
+ *
+ * See [[StateDeclaration.lazyLoad]]
+ */
+const lazyLoadHook: TransitionHookFn = (transition: Transition) => {
+ var toState = transition.to();
+
+ function retryOriginalTransition(newStates: State[]) {
+ if (transition.options().source === 'url') {
+ let loc = services.location;
+ let path = loc.path(), search = loc.search(), hash = loc.hash();
+
+ let matchState = state => [state, state.url.exec(path, search, hash)];
+ let matches = newStates.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();
+ }
+
+ let state = transition.targetState().identifier();
+ let params = transition.params();
+ let options = transition.options();
+ return transition.router.stateService.target(state, params, options);
+ }
+
+ /**
+ * Replace the placeholder state with the newly loaded states from the NgModule.
+ */
+ function updateStateRegistry(newStates: StateDeclaration[]) {
+ let registry = transition.router.stateRegistry;
+ let placeholderState = transition.to();
+
+ registry.deregister(placeholderState);
+ newStates.forEach(state => registry.register(state));
+ return newStates.map(state => registry.get(state).$$state());
+ }
+
+ return toState.lazyLoad(transition)
+ .then(updateStateRegistry)
+ .then(retryOriginalTransition)
+};
+
+export const registerLazyLoadHook = (transitionService: TransitionService) =>
+ transitionService.onBefore({ to: (state) => !!state.lazyLoad }, lazyLoadHook);
diff --git a/src/hooks/url.ts b/src/hooks/url.ts
index b68a30320..3cd25b316 100644
--- a/src/hooks/url.ts
+++ b/src/hooks/url.ts
@@ -3,6 +3,7 @@ import {UrlRouter} from "../url/urlRouter";
import {StateService} from "../state/stateService";
import {Transition} from "../transition/transition";
import {TransitionHookFn} from "../transition/interface";
+import {TransitionService} from "../transition/transitionService";
/**
* A [[TransitionHookFn]] which updates the URL after a successful transition
diff --git a/src/ng2.ts b/src/ng2.ts
index c449704a9..69fdcd44b 100644
--- a/src/ng2.ts
+++ b/src/ng2.ts
@@ -8,10 +8,12 @@ export * from "./core";
import "./justjs";
export * from "./ng2/interface";
-export * from "./ng2/routerModule";
+export * from "./ng2/lazyLoadNgModule";
export * from "./ng2/providers";
export * from "./ng2/location";
export * from "./ng2/directives/directives";
export * from "./ng2/statebuilders/views";
+export * from "./ng2/statebuilders/lazyLoadNgModuleResolvable";
+export * from "./ng2/uiRouterNgModule";
export * from "./ng2/uiRouterConfig";
diff --git a/src/ng2/directives/directives.ts b/src/ng2/directives/directives.ts
index b1225b446..7744c3bdd 100644
--- a/src/ng2/directives/directives.ts
+++ b/src/ng2/directives/directives.ts
@@ -19,15 +19,6 @@ export * from "./uiSrefActive";
/**
* References to the UI-Router directive classes, for use within a @Component's `directives:` property
- *
- * @example
- * ```js
- *
- * Component({
- * selector: 'my-cmp',
- * directives: [UIROUTER_DIRECTIVES],
- * template: 'Foo'
- * })
- * ```
+ * @deprecated use [[UIRouterModule]]
*/
export let UIROUTER_DIRECTIVES = [UISref, AnchorUISref, UIView, UISrefActive, UISrefStatus];
diff --git a/src/ng2/interface.ts b/src/ng2/interface.ts
index 3192d7ad7..30d20fcd9 100644
--- a/src/ng2/interface.ts
+++ b/src/ng2/interface.ts
@@ -1,7 +1,7 @@
/** @module ng2 */ /** */
import {StateDeclaration, _ViewDeclaration} from "../state/interface";
import {Transition} from "../transition/transition";
-import {Type} from "@angular/core";
+import {Type, OpaqueToken} from "@angular/core";
import {HookResult} from "../transition/interface";
/**
@@ -342,4 +342,4 @@ export interface Ng2Component {
uiCanExit(): HookResult;
}
-export const NG2_INJECTOR_TOKEN = {};
+export const NG2_INJECTOR_TOKEN = new OpaqueToken("NgModule Injector");
diff --git a/src/ng2/lazyLoadNgModule.ts b/src/ng2/lazyLoadNgModule.ts
new file mode 100644
index 000000000..f315aa78f
--- /dev/null
+++ b/src/ng2/lazyLoadNgModule.ts
@@ -0,0 +1,68 @@
+import {Transition} from "../transition/transition";
+import {NG2_INJECTOR_TOKEN, Ng2StateDeclaration} from "./interface";
+import {UIROUTER_STATES_TOKEN} from "./uiRouterNgModule";
+
+import {NgModuleFactoryLoader, NgModuleRef, Injector, NgModuleFactory} from "@angular/core";
+import {unnestR} from "../common/common";
+
+/**
+ * Returns a function which lazy loads a nested module
+ *
+ * Use this function as a [[StateDeclaration.lazyLoad]] property to lazy load a state tree (an NgModule).
+ *
+ * @param path the path to the module source code.
+ * @returns A function which takes a transition, then:
+ *
+ * - Gets the Injector (scoped properly for the destination state)
+ * - Loads and creates the NgModule
+ * - Finds the "replacement state" for the target state, and adds the new NgModule Injector to it (as a resolve)
+ *
+ * returns the new states array
+ */
+export function loadNgModule(path: string) {
+ /** Get the parent NgModule Injector (from resolves) */
+ const getNg2Injector = (transition: Transition) =>
+ transition.injector().getAsync(NG2_INJECTOR_TOKEN);
+
+ /**
+ * Lazy loads the NgModule using the NgModuleFactoryLoader
+ *
+ * Use the parent NgModule's Injector to:
+ * - Find the correct NgModuleFactoryLoader
+ * - Load the new NgModuleFactory from the path string (async)
+ * - Create the new NgModule
+ */
+ const createNg2Module = (path: string, ng2Injector: Injector) =>
+ ng2Injector.get(NgModuleFactoryLoader).load(path)
+ .then((factory: NgModuleFactory) => factory.create(ng2Injector));
+
+ /**
+ * Apply the Lazy Loaded NgModule's Injector to the newly loaded state tree.
+ *
+ * Lazy loading uses a placeholder state which is removed (and replaced) after the module is loaded.
+ * The NgModule should include a state with the same name as the placeholder.
+ *
+ * Find the *newly loaded state* with the same name as the *placeholder state*.
+ * The NgModule's Injector (and ComponentFactoryResolver) will be added to that state.
+ * The Injector/Factory are used when creating Components for the `replacement` state and all its children.
+ */
+ function applyNgModuleToNewStates(transition: Transition, ng2Module: NgModuleRef): Ng2StateDeclaration[] {
+ var targetName = transition.to().name;
+ let newStates: Ng2StateDeclaration[] = ng2Module.injector.get(UIROUTER_STATES_TOKEN).reduce(unnestR, []);
+ let replacementState = newStates.find(state => state.name === targetName);
+
+ if (!replacementState) {
+ throw new Error(`The module that was loaded from ${path} should have a state named '${targetName}'` +
+ `, but it only had: ${(newStates || []).map(s=>s.name).join(', ')}`);
+ }
+
+ // Add the injector as a resolve.
+ replacementState['_ngModuleInjector'] = ng2Module.injector;
+
+ return newStates;
+ }
+
+ return (transition: Transition) => getNg2Injector(transition)
+ .then((injector: Injector) => createNg2Module(path, injector))
+ .then((moduleRef: NgModuleRef) => applyNgModuleToNewStates(transition, moduleRef))
+}
diff --git a/src/ng2/providers.ts b/src/ng2/providers.ts
index 20c7b2bee..733070ccf 100644
--- a/src/ng2/providers.ts
+++ b/src/ng2/providers.ts
@@ -64,15 +64,27 @@ import {UIRouterLocation} from "./location";
import {services} from "../common/coreservices";
import {ProviderLike} from "../state/interface";
import {Resolvable} from "../resolve/resolvable";
+import {ngModuleResolvablesBuilder} from "./statebuilders/lazyLoadNgModuleResolvable";
let uiRouterFactory = (routerConfig: UIRouterConfig, location: UIRouterLocation, injector: Injector) => {
services.$injector.get = injector.get.bind(injector);
- let router = new UIRouter();
location.init();
+
+ // ----------------- Create router -----------------
+ // Create a new ng2 UIRouter and configure it for ng2
+ let router = new UIRouter();
+ let registry = router.stateRegistry;
+
+ // ----------------- Configure for ng2 -------------
+ // Apply ng2 ui-view handling code
router.viewService.viewConfigFactory("ng2", (path: PathNode[], config: Ng2ViewDeclaration) => new Ng2ViewConfig(path, config));
- router.stateRegistry.decorator('views', ng2ViewsBuilder);
+ registry.decorator('views', ng2ViewsBuilder);
+
+ // Apply statebuilder decorator for ng2 NgModule registration
+ registry.stateQueue.flush(router.stateService);
+ registry.decorator('resolvables', ngModuleResolvablesBuilder);
router.stateRegistry.stateQueue.autoFlush(router.stateService);
diff --git a/src/ng2/statebuilders/lazyLoadNgModuleResolvable.ts b/src/ng2/statebuilders/lazyLoadNgModuleResolvable.ts
new file mode 100644
index 000000000..3066551fa
--- /dev/null
+++ b/src/ng2/statebuilders/lazyLoadNgModuleResolvable.ts
@@ -0,0 +1,21 @@
+/** @module ng2 */ /** */
+import {State} from "../../state/stateObject";
+import {NG2_INJECTOR_TOKEN} from "../interface";
+import {Resolvable} from "../../resolve/resolvable";
+
+/**
+ * This is a [[StateBuilder.builder]] function which enables lazy Ng2Module support.
+ *
+ * See [[loadNgModule]]
+ *
+ * After lazy loading an NgModule, any Components from that module should be created using the NgModule's Injecjtor.
+ * The NgModule's ComponentFactory only exists inside that Injector.
+ *
+ * After lazy loading an NgModule, it is stored on the root state of the lazy loaded state tree.
+ * When instantiating Component, the parent Component's Injector is merged with the NgModule injector.
+ */
+export function ngModuleResolvablesBuilder(state: State, parentFn: Function): Resolvable[] {
+ let resolvables: Resolvable[] = parentFn(state);
+ let injector = state.self['_ngModuleInjector'];
+ return !injector ? resolvables : resolvables.concat(Resolvable.fromData(NG2_INJECTOR_TOKEN, injector));
+}
diff --git a/src/ng2/routerModule.ts b/src/ng2/uiRouterNgModule.ts
similarity index 72%
rename from src/ng2/routerModule.ts
rename to src/ng2/uiRouterNgModule.ts
index d1d8830bc..de2efd5f5 100644
--- a/src/ng2/routerModule.ts
+++ b/src/ng2/uiRouterNgModule.ts
@@ -3,7 +3,7 @@ import {NgModule, NgModuleMetadataType, OpaqueToken} from "@angular/core";
import {UIROUTER_DIRECTIVES} from "./directives/directives";
import {UIROUTER_PROVIDERS} from "./providers";
import {UIView} from "./directives/uiView";
-import {uniqR} from "../common/common";
+import {uniqR, flattenR} from "../common/common";
@NgModule({
declarations: [UIROUTER_DIRECTIVES],
@@ -11,7 +11,7 @@ import {uniqR} from "../common/common";
entryComponents: [UIView],
providers: [UIROUTER_PROVIDERS]
})
-export class _UIRouterModule {}
+export class UIRouterRootModule {}
/**
* A module declaration lteral, including UI-Router states.
@@ -23,7 +23,7 @@ export interface UIRouterModuleMetadata extends NgModuleMetadataType {
states?: Ng2StateDeclaration[]
}
-export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouterStates");
+export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouter States");
/**
* Declares a NgModule with UI-Router states
@@ -51,17 +51,19 @@ export const UIROUTER_STATES_TOKEN = new OpaqueToken("UIRouterStates");
*/
export function UIRouterModule(moduleMetaData: UIRouterModuleMetadata) {
let states = moduleMetaData.states || [];
+ var statesProvider = { provide: UIROUTER_STATES_TOKEN, useValue: states, multi: true };
// Get the component classes for all views for all states in the module
- let components = states.map(state => state.views || { $default: state })
+ let routedComponents = states.reduce(flattenR, [])
+ .map(state => state.views || { $default: state })
.map(viewObj => Object.keys(viewObj).map(key => viewObj[key].component))
.reduce((acc, arr) => acc.concat(arr), [])
.filter(x => typeof x === 'function' && x !== UIView);
- moduleMetaData.imports = (moduleMetaData.imports || []).concat(_UIRouterModule).reduce(uniqR, []);
- moduleMetaData.declarations = (moduleMetaData.declarations || []).concat(components).reduce(uniqR, []);
- moduleMetaData.entryComponents = (moduleMetaData.entryComponents || []).concat(components).reduce(uniqR, []);
- moduleMetaData.providers = (moduleMetaData.providers || []).concat({ provide: UIROUTER_STATES_TOKEN, useValue: states });
+ moduleMetaData.imports = (moduleMetaData.imports || []).concat(UIRouterRootModule).reduce(uniqR, []);
+ moduleMetaData.declarations = (moduleMetaData.declarations || []).concat(routedComponents).reduce(uniqR, []);
+ moduleMetaData.entryComponents = (moduleMetaData.entryComponents || []).concat(routedComponents).reduce(uniqR, []);
+ moduleMetaData.providers = (moduleMetaData.providers || []).concat(statesProvider);
return function(moduleClass) {
return NgModule(moduleMetaData)(moduleClass);
diff --git a/src/resolve/resolvable.ts b/src/resolve/resolvable.ts
index 2fc521d21..ed61a6845 100644
--- a/src/resolve/resolvable.ts
+++ b/src/resolve/resolvable.ts
@@ -167,4 +167,7 @@ export class Resolvable implements ResolvableLiteral {
clone(): Resolvable {
return new Resolvable(this);
}
+
+ static fromData = (token: any, data: any) =>
+ new Resolvable(token, () => data, null, null, data);
}
diff --git a/src/state/interface.ts b/src/state/interface.ts
index 365967210..a891194da 100644
--- a/src/state/interface.ts
+++ b/src/state/interface.ts
@@ -520,6 +520,15 @@ export interface StateDeclaration {
*/
onExit?: TransitionStateHookFn;
+ /**
+ * A function that lazy loads a state tree.
+
+
+ *
+ * @param transition
+ */
+ lazyLoad?: (transition: Transition) => Promise;
+
/**
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
*/
diff --git a/src/state/stateMatcher.ts b/src/state/stateMatcher.ts
index ccdc0208f..2c801c8bc 100644
--- a/src/state/stateMatcher.ts
+++ b/src/state/stateMatcher.ts
@@ -2,6 +2,8 @@
import {isString} from "../common/predicates";
import {StateOrName} from "./interface";
import {State} from "./stateObject";
+import {Glob} from "../common/glob";
+import {values} from "../common/common";
export class StateMatcher {
constructor (private _states: { [key: string]: State }) { }
@@ -22,6 +24,17 @@ export class StateMatcher {
if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) {
return state;
+ } else if (isStr) {
+ let matches = values(this._states)
+ .filter(state => !!state.lazyLoad)
+ .map(state => ({ state, glob: new Glob(state.name + ".**")}))
+ .filter(({state, glob}) => glob.matches(name))
+ .map(({state, glob}) => state);
+
+ if (matches.length > 1) {
+ console.log(`stateMatcher.find: Found multiple matches for ${name} using glob: `, matches.map(match => match.name));
+ }
+ return matches[0];
}
return undefined;
}
diff --git a/src/state/stateObject.ts b/src/state/stateObject.ts
index cddfc7ccd..553a4ccda 100644
--- a/src/state/stateObject.ts
+++ b/src/state/stateObject.ts
@@ -44,6 +44,7 @@ export class State {
public onExit: TransitionStateHookFn;
public onRetain: TransitionStateHookFn;
public onEnter: TransitionStateHookFn;
+ public lazyLoad: (transition: Transition) => Promise;
redirectTo: (
string |
diff --git a/src/transition/transitionService.ts b/src/transition/transitionService.ts
index 01b1502ff..dd644fc6e 100644
--- a/src/transition/transitionService.ts
+++ b/src/transition/transitionService.ts
@@ -17,6 +17,7 @@ import {registerLoadEnteringViews, registerActivateViews} from "../hooks/views";
import {registerUpdateUrl} from "../hooks/url";
import {registerRedirectToHook} from "../hooks/redirectTo";
import {registerOnExitHook, registerOnRetainHook, registerOnEnterHook} from "../hooks/onEnterExitRetain";
+import {registerLazyLoadHook} from "../hooks/lazyLoadStates";
/**
* The default [[Transition]] options.
@@ -50,12 +51,13 @@ export class TransitionService implements IHookRegistry {
public $view: ViewService;
/**
- * This object has hook de-registration functions.
+ * This object has hook de-registration functions for the built-in hooks.
* This can be used by third parties libraries that wish to customize the behaviors
*
* @hidden
*/
_deregisterHookFns: {
+ redirectTo: Function;
onExit: Function;
onRetain: Function;
onEnter: Function;
@@ -64,7 +66,7 @@ export class TransitionService implements IHookRegistry {
loadViews: Function;
activateViews: Function;
updateUrl: Function;
- redirectTo: Function;
+ lazyLoad: Function;
};
constructor(private _router: UIRouter) {
@@ -96,6 +98,9 @@ export class TransitionService implements IHookRegistry {
// After globals.current is updated at priority: 10000
fns.updateUrl = registerUpdateUrl(this);
+
+ // Lazy load state trees
+ fns.lazyLoad = registerLazyLoadHook(this);
}
/** @inheritdoc */