Skip to content

Commit

Permalink
feat(lazyLoad): Allow loadChildren: sugar on a state definition
Browse files Browse the repository at this point in the history
feat(lazyLoad): automatically unwrap default export (`__esModule`) (allows easier default export of NgModule)
fix(lazyLoad): Update lazyLoad for ui-router-core 3.1

Instead of:
```js
import { loadNgModule } from "ui-router-ng2";
var futureState = {
  name: 'lazy.state',
  url: '/lazy',
  lazyLoad: loadNgModule('./lazy/lazy.module')
}
```

Instead of:
```js
import { loadNgModule } from "ui-router-ng2";
var futureState = {
  name: 'lazy.state',
  url: '/lazy',
  lazyLoad: loadNgModule(() =>
      System.import('./lazy/lazy.module').then(result => result.LazyModule))
}
```

Switch to:
```js
var futureState = {
  name: 'lazy.state.**',
  url: '/lazy',
  loadChildren: './lazy/lazy.module#LazyModule'
}
```

---

This change is an incremental step towards seamless AoT and lazy load
support within the angular-cli.

Currently the cli only supports lazy loading of angular router routes.
We expect the AoT compiler and cli (@ngtools/webpack) will be updated to
support AoT lazy loading by other routers (such as ui-router) soon.
  • Loading branch information
christopherthielen committed Jan 21, 2017
1 parent 82a52e5 commit 2a4b174
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"dependencies": {
"ui-router-core": "=3.1.0"
"ui-router-core": "=3.1.1"
},
"peerDependencies": {
"@angular/common": "^2.0.0",
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/map';

export * from "./interface";
export * from "./lazyLoadNgModule";
export * from "./rx";
export * from "./providers";
export * from "./location/uiRouterLocation";
export * from "./directives/directives";
export * from "./statebuilders/views";
export * from "./uiRouterNgModule";
export * from "./uiRouterConfig";
export * from "./directives/directives";
export * from "./location/uiRouterLocation";
export * from "./statebuilders/views";
export * from "./lazyLoad/lazyLoadNgModule";
export * from "./rx";
91 changes: 80 additions & 11 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @ng2api @module state */ /** */
import {StateDeclaration, _ViewDeclaration} from "ui-router-core";
import {Transition} from "ui-router-core";
import {Type, OpaqueToken} from "@angular/core";
import {HookResult} from "ui-router-core";
/** @ng2api @module state */
/** */

import { StateDeclaration, _ViewDeclaration, Transition, HookResult } from "ui-router-core";
import { Type } from "@angular/core";
import { NgModuleToLoad } from "./lazyLoad/lazyLoadNgModule";

/**
* The StateDeclaration object is used to define a state or nested state.
Expand All @@ -12,16 +13,18 @@ import {HookResult} from "ui-router-core";
* ```js
* import {FoldersComponent} from "./folders";
*
* export function getAllFolders(FolderService) {
* return FolderService.list();
* }
*
* // StateDeclaration object
* var foldersState = {
* export let foldersState = {
* name: 'folders',
* url: '/folders',
* component: FoldersComponent,
* resolve: {
* allfolders: function(FolderService) {
* return FolderService.list();
* }
* }
* resolve: [
* { token: 'allfolders', deps: [FolderService], resolveFn: getAllFolders }
* ]
* }
* ```
*/
Expand Down Expand Up @@ -130,6 +133,72 @@ export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaratio
* the `views` object.
*/
views?: { [key: string]: Ng2ViewDeclaration; };

/**
* A string or function used to lazy load an `NgModule`
*
* The `loadChildren` property should be added to a Future State (a lazy loaded state whose name ends in `.**`).
* The Future State is a placeholder for a tree of states that will be lazy loaded in the future.
*
* When the future state is activated, the `loadChildren` property will lazy load an `NgModule`
* which contains the fully loaded states.
* The `NgModule` should contain the fully loaded states which will be registered.
* The fully loaded states will replace the temporary future states once lazy loading is complete.
*
* ---
*
* When `loadChildren` is a string, it should be a relative path to the module code that will be lazy loaded.
* It should follow the semantics of the Angular Router's `loadChildren` property.
* The string will be split in half on the hash character (`#`).
* The first half is the path to the module.
* The last half is the named export of the `NgModule` inside the ES6 module.
*
* #### Example:
*
* home.module.ts
*
* ```
* @NgModule({... })
* export class HomeModule {};
* ```
*
* ```js
* var futureState = {
* name: 'home.**',
* url: '/home',
* loadChildren: './home/home.module#HomeModule')
* }
* ```
*
*
* As a function, it should return a promise for the `NgModule`
*
* #### Example:
* ```js
* var futureState = {
* name: 'home.**',
* url: '/home',
* loadChildren: () => System.import('./home/home.module')
* .then(result => result.HomeModule);
* }
* ```
*
* #### Example:
* This shows the load function being exported for compatibility with the AoT compiler.
* ```js
* export function loadHomeModule() {
* return System.import('./home/home.module')
* .then(result => result.HomeModule);
* }
*
* var futureState = {
* name: 'home.**',
* url: '/home',
* loadChildren: loadHomeModule
* }
* ```
*/
loadChildren?: NgModuleToLoad;
}

export interface Ng2ViewDeclaration extends _ViewDeclaration {
Expand Down
89 changes: 64 additions & 25 deletions src/lazyLoadNgModule.ts → src/lazyLoad/lazyLoadNgModule.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
/** @ng2api @module core */ /** */
import {NgModuleFactoryLoader, NgModuleRef, Injector, NgModuleFactory, Type, Compiler} from "@angular/core";
import {Transition, LazyLoadResult, UIRouter, Resolvable, NATIVE_INJECTOR_TOKEN, isString} from "ui-router-core";
import {RootModule, StatesModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN} from "./uiRouterNgModule";
import {applyModuleConfig} from "./uiRouterConfig";
/** @ng2api @module core */
/** */
import { NgModuleRef, Injector, NgModuleFactory, Type, Compiler, NgModuleFactoryLoader } from "@angular/core";
import { Transition, LazyLoadResult, UIRouter, Resolvable, NATIVE_INJECTOR_TOKEN, isString } from "ui-router-core";
import { RootModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN } from "../uiRouterNgModule";
import { applyModuleConfig } from "../uiRouterConfig";

/**
* A function that returns an NgModule, or a promise for an NgModule
*
* #### Example:
* ```js
* export function loadFooModule() {
* return System.import('../foo/foo.module').then(result => result.FooModule);
* }
* ```
*/
export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
/**
* A string or a function which lazy loads a module
*
* If a string, should conform to the Angular Router `loadChildren` string.
* #### Example:
* ```
* var ngModuleToLoad = './foo/foo.module#FooModule'
* ```
*
* For functions, see: [[ModuleTypeCallback]]
*/
export type NgModuleToLoad = string | ModuleTypeCallback;

/**
* Returns a function which lazy loads a nested module
*
* Use this function as a [[StateDeclaration.lazyLoad]] property to lazy load an NgModule and its state.
* This is primarily used by the [[ng2LazyLoadBuilder]] when processing [[Ng2StateDeclaration.loadChildren]].
*
* Example using `System.import()`:
* It could also be used manually as a [[StateDeclaration.lazyLoad]] property to lazy load an `NgModule` and its state(s).
*
* #### Example:
* Using `System.import()` and named export of `HomeModule`
* ```js
* {
* name: 'home',
* declare var System;
* var futureState = {
* name: 'home.**',
* url: '/home',
* lazyLoad: loadNgModule(() => System.import('./home.module').then(result => result.HomeModule))
* lazyLoad: loadNgModule(() => System.import('./home/home.module').then(result => result.HomeModule))
* }
* ```
*
* Example using `NgModuleFactoryLoader`:
* #### Example:
* Using a path (string) to the module
* ```js
* {
* name: 'home',
* var futureState = {
* name: 'home.**',
* url: '/home',
* lazyLoad: loadNgModule('./home.module')
* lazyLoad: loadNgModule('./home/home.module#HomeModule')
* }
* ```
*
* @param moduleToLoad
* If a string, it should be the path to the NgModule code, which will then be loaded by the `NgModuleFactoryLoader`.
* If a function, the function should load the NgModule code and return a reference to the `NgModule` class being loaded.
*
* @param moduleToLoad a path (string) to the NgModule to load.
* Or a function which loads the NgModule code which should
* return a reference to the `NgModule` class being loaded (or a `Promise` for it).
*
* @returns A function which takes a transition, which:
* - Gets the Injector (scoped properly for the destination state)
Expand Down Expand Up @@ -76,10 +104,13 @@ export function loadModuleFactory(moduleToLoad: NgModuleToLoad, ng2Injector: Inj

const compiler: Compiler = ng2Injector.get(Compiler);
const offlineMode = compiler instanceof Compiler;
const loadChildrenPromise = Promise.resolve(moduleToLoad());

const unwrapEsModuleDefault = x =>
x && x.__esModule && x['default'] ? x['default'] : x;
const compileAsync = (moduleType: Type<any>) =>
compiler.compileModuleAsync(moduleType);

const loadChildrenPromise = Promise.resolve(moduleToLoad()).then(unwrapEsModuleDefault);
return offlineMode ? loadChildrenPromise : loadChildrenPromise.then(compileAsync);
}

Expand All @@ -101,23 +132,31 @@ export function applyNgModule(transition: Transition, ng2Module: NgModuleRef<any
let injector = ng2Module.injector;
let parentInjector = <Injector> ng2Module.injector['parent'];
let uiRouter: UIRouter = injector.get(UIRouter);
let registry = uiRouter.stateRegistry;

let originalName = transition.to().name;
let originalState = uiRouter.stateRegistry.get(originalName);
let originalState = registry.get(originalName);
// Check if it's a future state (ends with .**)
let isFuture = /^(.*)\.\*\*$/.exec(originalName);
// Final name (without the .**)
let replacementName = isFuture && isFuture[1];

let newRootModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_ROOT_MODULE);

if (newRootModules.length) {
console.log(newRootModules);
throw new Error('Lazy loaded modules should not contain a UIRouterModule.forRoot() module');
}

let newModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_MODULE_TOKEN);
newModules.forEach(module => applyModuleConfig(uiRouter, injector, module));
let newChildModules: RootModule[] = multiProviderParentChildDelta(parentInjector, injector, UIROUTER_MODULE_TOKEN);
newChildModules.forEach(module => applyModuleConfig(uiRouter, injector, module));

let replacementState = uiRouter.stateRegistry.get(originalName);
if (replacementState === originalState) {
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. That NgModule should also have a UIRouterModule.forChild() state named '${originalName}' to replace the Future State, but it did not.`);
let replacementState = registry.get(replacementName);
if (!replacementState || replacementState === originalState) {
throw new Error(`The Future State named '${originalName}' lazy loaded an NgModule. ` +
`The lazy loaded NgModule must have a state named '${replacementName}' ` +
`which replaces the (placeholder) '${originalName}' Future State. ` +
`Add a '${replacementName}' state to the lazy loaded NgModule ` +
`using UIRouterModule.forChild({ states: CHILD_STATES }).`);
}

// Supply the newly loaded states with the Injector from the lazy loaded NgModule
Expand Down
2 changes: 1 addition & 1 deletion src/location/locationService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @module ng2 */
/** */
import { UIRouter } from "ui-router-core";
import { BaseLocationServices } from "ui-router-core/lib/vanilla";
import { BaseLocationServices } from "ui-router-core/lib/vanilla/baseLocationService";
import { parseUrl } from "ui-router-core/lib/vanilla/utils";
import { PlatformLocation, LocationStrategy } from "@angular/common";

Expand Down
2 changes: 2 additions & 0 deletions src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { RootModule, StatesModule, UIROUTER_ROOT_MODULE, UIROUTER_MODULE_TOKEN }
import { UIRouterRx } from "./rx";
import { servicesPlugin } from "ui-router-core/lib/vanilla";
import { ServicesPlugin } from "ui-router-core/lib/vanilla/interface";
import { ng2LazyLoadBuilder } from "./statebuilders/lazyLoad";

/**
* This is a factory function for a UIRouter instance
Expand Down Expand Up @@ -142,6 +143,7 @@ export function uiRouterFactory(location: UIRouterLocation, injector: Injector)
// Apply statebuilder decorator for ng2 NgModule registration
let registry = router.stateRegistry;
registry.decorator('views', ng2ViewsBuilder);
registry.decorator('lazyLoad', ng2LazyLoadBuilder);

// Prep the tree of NgModule by placing the root NgModule's Injector on the root state.
let ng2InjectorResolvable = Resolvable.fromData(NATIVE_INJECTOR_TOKEN, injector);
Expand Down
53 changes: 53 additions & 0 deletions src/statebuilders/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** @module ng2 */
/** */
import { LazyLoadResult, Transition, StateDeclaration } from "ui-router-core"; // has or is using
import { BuilderFunction, State } from "ui-router-core";
import { loadNgModule } from "../lazyLoad/lazyLoadNgModule";

/**
* This is a [[StateBuilder.builder]] function for ngModule lazy loading in angular2.
*
* When the [[StateBuilder]] builds a [[State]] object from a raw [[StateDeclaration]], this builder
* decorates the `lazyLoad` property for states that have a [[Ng2StateDeclaration.ngModule]] declaration.
*
* If the state has a [[Ng2StateDeclaration.ngModule]], it will create a `lazyLoad` function
* that in turn calls `loadNgModule(loadNgModuleFn)`.
*
* #### Example:
* A state that has a `ngModule`
* ```js
* var decl = {
* ngModule: () => System.import('./childModule.ts')
* }
* ```
* would build a state with a `lazyLoad` function like:
* ```js
* import { loadNgModule } from "ui-router-ng2";
* var decl = {
* lazyLoad: loadNgModule(() => System.import('./childModule.ts')
* }
* ```
*
* If the state has both a `ngModule:` *and* a `lazyLoad`, then the `lazyLoad` is run first.
*
* #### Example:
* ```js
* var decl = {
* lazyLoad: () => System.import('third-party-library'),
* ngModule: () => System.import('./childModule.ts')
* }
* ```
* would build a state with a `lazyLoad` function like:
* ```js
* import { loadNgModule } from "ui-router-ng2";
* var decl = {
* lazyLoad: () => System.import('third-party-library')
* .then(() => loadNgModule(() => System.import('./childModule.ts'))
* }
* ```
*
*/
export function ng2LazyLoadBuilder(state: State, parent: BuilderFunction) {
let loadNgModuleFn = state['loadChildren'];
return loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
}
12 changes: 6 additions & 6 deletions src/uiRouterNgModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import { _UIROUTER_INSTANCE_PROVIDERS, _UIROUTER_SERVICE_PROVIDERS } from "./pro

export function makeRootProviders(module: StatesModule): Provider[] {
return [
{ provide: UIROUTER_ROOT_MODULE, useValue: module, multi: true},
{ provide: UIROUTER_ROOT_MODULE, useValue: module, multi: true},
{ provide: UIROUTER_MODULE_TOKEN, useValue: module, multi: true },
{ provide: UIROUTER_STATES, useValue: module.states || [], multi: true },
{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: module.states || [], multi: true },
];
}

export function makeChildProviders(module: StatesModule): Provider[] {
return [
{ provide: UIROUTER_MODULE_TOKEN, useValue: module, multi: true },
{ provide: UIROUTER_STATES, useValue: module.states || [], multi: true },
{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: module.states || [], multi: true },
];
}
Expand Down Expand Up @@ -221,8 +223,6 @@ export interface StatesModule {
configClass?: Type<any>;
}

/** @hidden */
export const UIROUTER_ROOT_MODULE = new OpaqueToken("UIRouter Root Module");

/** @hidden */
export const UIROUTER_MODULE_TOKEN = new OpaqueToken("UIRouter Module");
/** @hidden */ export const UIROUTER_ROOT_MODULE = new OpaqueToken("UIRouter Root Module");
/** @hidden */ export const UIROUTER_MODULE_TOKEN = new OpaqueToken("UIRouter Module");
/** @hidden */ export const UIROUTER_STATES = new OpaqueToken("UIRouter States");

0 comments on commit 2a4b174

Please sign in to comment.