Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending lazy-load functionality to include loading standalone components for specific routes. #995

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 13 additions & 21 deletions src/directives/uiView.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
Component,
ComponentFactory,
ComponentFactoryResolver,
ComponentMirror,
ComponentRef,
Inject,
Injector,
Input,
OnDestroy,
OnInit,
reflectComponentType,
Type,
ViewChild,
ViewContainerRef,
} from '@angular/core';
Expand Down Expand Up @@ -58,8 +59,8 @@ interface InputMapping {
*
* @internal
*/
const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
return factory.inputs.map((input) => ({ prop: input.propName, token: input.templateName }));
function ng2ComponentInputs<T>(mirror: ComponentMirror<T>): InputMapping[] {
return mirror.inputs.map((input) => ({ prop: input.templateName, token: input.templateName }));
};

/**
Expand Down Expand Up @@ -293,12 +294,9 @@ export class UIView implements OnInit, OnDestroy {
const componentClass = config.viewDecl.component;

// Create the component
const compFactoryResolver = componentInjector.get(ComponentFactoryResolver);
const compFactory = compFactoryResolver.resolveComponentFactory(componentClass);
this._componentRef = this._componentTarget.createComponent(compFactory, undefined, componentInjector);

this._componentRef = this._componentTarget.createComponent(componentClass, { injector: componentInjector });
// Wire resolves to @Input()s
this._applyInputBindings(compFactory, this._componentRef.instance, context, componentClass);
this._applyInputBindings(componentClass, this._componentRef, context);
}

/**
Expand Down Expand Up @@ -327,7 +325,7 @@ export class UIView implements OnInit, OnDestroy {
const moduleInjector = context.getResolvable(NATIVE_INJECTOR_TOKEN).data;
const mergedParentInjector = new MergeInjector(moduleInjector, parentComponentInjector);

return Injector.create(newProviders, mergedParentInjector);
return Injector.create({ providers: newProviders, parent: mergedParentInjector });
}

/**
Expand All @@ -336,25 +334,19 @@ export class UIView implements OnInit, OnDestroy {
* Finds component inputs which match resolves (by name) and sets the input value
* to the resolve data.
*/
private _applyInputBindings(factory: ComponentFactory<any>, component: any, context: ResolveContext, componentClass) {
private _applyInputBindings<T>(component: Type<T>, componentRef: ComponentRef<T>, context: ResolveContext): void {
const bindings = this._uiViewData.config.viewDecl['bindings'] || {};
const explicitBoundProps = Object.keys(bindings);

// Returns the actual component property for a renamed an input renamed using `@Input('foo') _foo`.
// return the `_foo` property
const renamedInputProp = (prop: string) => {
const input = factory.inputs.find((i) => i.templateName === prop);
return (input && input.propName) || prop;
};
const mirror = reflectComponentType(component);

// Supply resolve data to component as specified in the state's `bindings: {}`
const explicitInputTuples = explicitBoundProps.reduce(
(acc, key) => acc.concat([{ prop: renamedInputProp(key), token: bindings[key] }]),
(acc, key) => acc.concat([{ prop: key, token: bindings[key] }]),
[]
);

// Supply resolve data to matching @Input('prop') or inputs: ['prop']
const implicitInputTuples = ng2ComponentInputs(factory).filter((tuple) => !inArray(explicitBoundProps, tuple.prop));
const implicitInputTuples = ng2ComponentInputs(mirror).filter((tuple) => !inArray(explicitBoundProps, tuple.prop));

const addResolvable = (tuple: InputMapping) => ({
prop: tuple.prop,
Expand All @@ -368,7 +360,7 @@ export class UIView implements OnInit, OnDestroy {
.map(addResolvable)
.filter((tuple) => tuple.resolvable && tuple.resolvable.resolved)
.forEach((tuple) => {
component[tuple.prop] = injector.get(tuple.resolvable.token);
componentRef.setInput(tuple.prop, injector.get(tuple.resolvable.token));
});
}
}
28 changes: 23 additions & 5 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StateDeclaration, _ViewDeclaration, Transition, HookResult } from '@uirouter/core';
import { Component, Type } from '@angular/core';
import { ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';
import { ComponentTypeCallback, ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';

/**
* The StateDeclaration object is used to define a state or nested state.
Expand All @@ -25,7 +25,7 @@ import { ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';
* }
* ```
*/
export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaration {
export interface Ng2StateDeclaration<T = unknown> extends StateDeclaration, Ng2ViewDeclaration<T> {
/**
* An optional object used to define multiple named views.
*
Expand Down Expand Up @@ -152,10 +152,28 @@ export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaratio
* }
* ```
*/
loadChildren?: ModuleTypeCallback;
loadChildren?: ModuleTypeCallback<T>;

/**
* A function used to lazy load a `Component`.
*
* When the state is activate the `loadComponent` property should lazy load a standalone `Component`
* and use it to render the view of the state
*
* ### Example:
* ```ts
* var homeState = {
* name: 'home',
* url: '/home',
* loadComponent: () => import('./home/home.component')
* .then(result => result.HomeComponent)
* }
* ```
*/
loadComponent?: ComponentTypeCallback<T>;
}

export interface Ng2ViewDeclaration extends _ViewDeclaration {
export interface Ng2ViewDeclaration<T = unknown> extends _ViewDeclaration {
/**
* The `Component` class to use for this view.
*
Expand Down Expand Up @@ -238,7 +256,7 @@ export interface Ng2ViewDeclaration extends _ViewDeclaration {
* }
* ```
*/
component?: Type<any>;
component?: Type<T>;

/**
* An object which maps `resolve` keys to [[component]] `bindings`.
Expand Down
138 changes: 94 additions & 44 deletions src/lazyLoad/lazyLoadNgModule.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { NgModuleRef, Injector, NgModuleFactory, Type, Compiler } from '@angular/core';
import { NgModuleRef, Injector, Type, createNgModule, InjectionToken, isStandalone } from '@angular/core';
import {
Transition,
LazyLoadResult,
UIRouter,
Resolvable,
NATIVE_INJECTOR_TOKEN,
isString,
unnestR,
inArray,
StateObject,
Expand All @@ -15,6 +14,7 @@ import {
import { UIROUTER_MODULE_TOKEN, UIROUTER_ROOT_MODULE } from '../injectionTokens';
import { RootModule, StatesModule } from '../uiRouterNgModule';
import { applyModuleConfig } from '../uiRouterConfig';
import { Ng2StateDeclaration } from '../interface';

/**
* A function that returns an NgModule, or a promise for an NgModule
Expand All @@ -26,7 +26,7 @@ import { applyModuleConfig } from '../uiRouterConfig';
* }
* ```
*/
export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
export type ModuleTypeCallback<T = unknown> = () => Type<T> | Promise<Type<T>>;

/**
* Returns a function which lazy loads a nested module
Expand All @@ -36,29 +36,16 @@ export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
* It could also be used manually as a [[StateDeclaration.lazyLoad]] property to lazy load an `NgModule` and its state(s).
*
* #### Example:
* Using `import()` and named export of `HomeModule`
* ```js
* declare var System;
* ```ts
* var futureState = {
* name: 'home.**',
* url: '/home',
* lazyLoad: loadNgModule(() => import('./home/home.module').then(result => result.HomeModule))
* }
* ```
*
* #### Example:
* Using a path (string) to the module
* ```js
* var futureState = {
* name: 'home.**',
* url: '/home',
* lazyLoad: loadNgModule('./home/home.module#HomeModule')
* }
* ```
*
*
* @param moduleToLoad a path (string) to the NgModule to load.
* Or a function which loads the NgModule code which should
* @param moduleToLoad 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:
Expand All @@ -67,17 +54,15 @@ export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
* - 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(
moduleToLoad: ModuleTypeCallback
export function loadNgModule<T>(
moduleToLoad: ModuleTypeCallback<T>
): (transition: Transition, stateObject: StateDeclaration) => Promise<LazyLoadResult> {
return (transition: Transition, stateObject: StateDeclaration) => {
const ng2Injector = transition.injector().get(NATIVE_INJECTOR_TOKEN);

const createModule = (factory: NgModuleFactory<any>) => factory.create(ng2Injector);

const applyModule = (moduleRef: NgModuleRef<any>) => applyNgModule(transition, moduleRef, ng2Injector, stateObject);
const ng2Injector = transition.injector().get(NATIVE_INJECTOR_TOKEN);

return loadModuleFactory(moduleToLoad, ng2Injector).then(createModule).then(applyModule);
return loadModuleFactory(moduleToLoad, ng2Injector)
.then(moduleRef => applyNgModule(moduleRef, ng2Injector, stateObject));
};
}

Expand All @@ -90,22 +75,18 @@ export function loadNgModule(
*
* @internal
*/
export function loadModuleFactory(
moduleToLoad: ModuleTypeCallback,
export function loadModuleFactory<T>(
moduleToLoad: ModuleTypeCallback<T>,
ng2Injector: Injector
): Promise<NgModuleFactory<any>> {
const compiler: Compiler = ng2Injector.get(Compiler);

const unwrapEsModuleDefault = (x) => (x && x.__esModule && x['default'] ? x['default'] : x);
): Promise<NgModuleRef<T>> {

return Promise.resolve(moduleToLoad())
.then(unwrapEsModuleDefault)
.then((t: NgModuleFactory<any> | Type<any>) => {
if (t instanceof NgModuleFactory) {
return t;
}
return compiler.compileModuleAsync(t);
});
.then(_unwrapEsModuleDefault)
.then((t: Type<T>) => createNgModule(t, ng2Injector));
}

function _unwrapEsModuleDefault(x) {
return x && x.__esModule && x['default'] ? x['default'] : x;
}

/**
Expand All @@ -122,9 +103,8 @@ export function loadModuleFactory(
*
* @internal
*/
export function applyNgModule(
transition: Transition,
ng2Module: NgModuleRef<any>,
export function applyNgModule<T>(
ng2Module: NgModuleRef<T>,
parentInjector: Injector,
lazyLoadState: StateDeclaration
): LazyLoadResult {
Expand Down Expand Up @@ -192,8 +172,78 @@ export function applyNgModule(
*
* @internal
*/
export function multiProviderParentChildDelta(parent: Injector, child: Injector, token: any) {
const childVals: RootModule[] = child.get(token, []);
const parentVals: RootModule[] = parent.get(token, []);
export function multiProviderParentChildDelta<T>(parent: Injector, child: Injector, token: InjectionToken<T>): RootModule[] {
const childVals: RootModule[] = child.get<RootModule[]>(token, []);
const parentVals: RootModule[] = parent.get<RootModule[]>(token, []);
return childVals.filter((val) => parentVals.indexOf(val) === -1);
}

/**
* A function that returns a Component, or a promise for a Component
*
* #### Example:
* ```ts
* export function loadFooComponent() {
* return import('../foo/foo.component').then(result => result.FooComponent);
* }
* ```
*/
export type ComponentTypeCallback<T> = ModuleTypeCallback<T>;

/**
* Returns a function which lazy loads a standalone component for the target state
*
* #### Example:
* ```ts
* var futureComponentState = {
* name: 'home',
* url: '/home',
* lazyLoad: loadComponent(() => import('./home.component').then(result => result.HomeComponent))
* }
* ```
*
* @param callback function which loads the Component code which should
* return a reference to the `Component` class being loaded (or a `Promise` for it).
*
* @returns A function which takes a transition, stateObject, and:
* - Loads a standalone component
* - replaces the component configuration of the stateObject.
* - Returns the new states array
*/
export function loadComponent<T>(
callback: ComponentTypeCallback<T>
): (transition: Transition, stateObject: Ng2StateDeclaration) => Promise<LazyLoadResult> {
return (transition: Transition, stateObject: Ng2StateDeclaration) => {

return Promise.resolve(callback())
.then(_unwrapEsModuleDefault)
.then((component: Type<T>) => applyComponent(component, transition, stateObject))
}
}

/**
* Apply the lazy-loaded component to the stateObject.
*
* @internal
* @param component reference to the component class
* @param transition Transition object reference
* @param stateObject target state configuration object
*
* @returns the new states array
*/
export function applyComponent<T>(
component: Type<T>,
transition: Transition,
stateObject: Ng2StateDeclaration
): LazyLoadResult {

if (!isStandalone(component)) throw new Error("Is not a standalone component.");

const registry = transition.router.stateRegistry;
const current = stateObject.component;
stateObject.component = component || current;
const removed = registry.deregister(stateObject).map(child => child.self);
const children = removed.filter(i => i.name != stateObject.name);

return { states: [stateObject, ...children] }
}
5 changes: 3 additions & 2 deletions src/statebuilders/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LazyLoadResult, Transition, StateDeclaration } from '@uirouter/core'; // has or is using
import { BuilderFunction, StateObject } from '@uirouter/core';
import { loadNgModule } from '../lazyLoad/lazyLoadNgModule';
import { loadComponent, loadNgModule } from '../lazyLoad/lazyLoadNgModule';

/**
* This is a [[StateBuilder.builder]] function for ngModule lazy loading in Angular.
Expand Down Expand Up @@ -46,6 +46,7 @@ import { loadNgModule } from '../lazyLoad/lazyLoadNgModule';
*
*/
export function ng2LazyLoadBuilder(state: StateObject, parent: BuilderFunction) {
const loadComponentFn = state['loadComponent'];
const loadNgModuleFn = state['loadChildren'];
return loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
return loadComponentFn ? loadComponent(loadComponentFn) : loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('Angular app', () => {
cy.get('a').contains('home').should('not.have.class', 'active');
cy.get('a').contains('lazy.child').should('have.class', 'active');
cy.get('#default').contains('lazy.child works');
cy.get('#lazy-child-provided').contains('provided value');
});

it('targets named views', () => {
Expand Down
Loading
Loading