Skip to content

Commit e381ea4

Browse files
authored
feat: Extending lazy-load functionality to include loading standalone components for specific routes. (#995)
* Updated Ng2StateDeclaration and Ng2ViewDeclaration to use generics and added the loadComponent property. * Updated Lazy load implementeation, replaced deprecated API with newest changes to angular framework. * Implemented load component functionality in the lazy load section. * Updated implementation of the UIView component. replaced deprecated API with newest angular API. inputs signals now work for view bound components. * Updated the lazyLoadBuilder to load components. * Updated standalone example. * Fixed bug applying input bindings in uiView component. * Created a few new test cases for uiView for new features in components inputs. * Updated documentation for loadComponent functionality * Added test suite for loadComponent functionality. * Addition to the test for lazy.child in v19-standalone test project. * updating test to be clear is not a standalone component.
1 parent 48fbfd0 commit e381ea4

File tree

11 files changed

+300
-75
lines changed

11 files changed

+300
-75
lines changed

src/directives/uiView.ts

+13-21
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
22
Component,
3-
ComponentFactory,
4-
ComponentFactoryResolver,
3+
ComponentMirror,
54
ComponentRef,
65
Inject,
76
Injector,
87
Input,
98
OnDestroy,
109
OnInit,
10+
reflectComponentType,
11+
Type,
1112
ViewChild,
1213
ViewContainerRef,
1314
} from '@angular/core';
@@ -58,8 +59,8 @@ interface InputMapping {
5859
*
5960
* @internal
6061
*/
61-
const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
62-
return factory.inputs.map((input) => ({ prop: input.propName, token: input.templateName }));
62+
function ng2ComponentInputs<T>(mirror: ComponentMirror<T>): InputMapping[] {
63+
return mirror.inputs.map((input) => ({ prop: input.templateName, token: input.templateName }));
6364
};
6465

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

295296
// Create the component
296-
const compFactoryResolver = componentInjector.get(ComponentFactoryResolver);
297-
const compFactory = compFactoryResolver.resolveComponentFactory(componentClass);
298-
this._componentRef = this._componentTarget.createComponent(compFactory, undefined, componentInjector);
299-
297+
this._componentRef = this._componentTarget.createComponent(componentClass, { injector: componentInjector });
300298
// Wire resolves to @Input()s
301-
this._applyInputBindings(compFactory, this._componentRef.instance, context, componentClass);
299+
this._applyInputBindings(componentClass, this._componentRef, context);
302300
}
303301

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

330-
return Injector.create(newProviders, mergedParentInjector);
328+
return Injector.create({ providers: newProviders, parent: mergedParentInjector });
331329
}
332330

333331
/**
@@ -336,25 +334,19 @@ export class UIView implements OnInit, OnDestroy {
336334
* Finds component inputs which match resolves (by name) and sets the input value
337335
* to the resolve data.
338336
*/
339-
private _applyInputBindings(factory: ComponentFactory<any>, component: any, context: ResolveContext, componentClass) {
337+
private _applyInputBindings<T>(component: Type<T>, componentRef: ComponentRef<T>, context: ResolveContext): void {
340338
const bindings = this._uiViewData.config.viewDecl['bindings'] || {};
341339
const explicitBoundProps = Object.keys(bindings);
342-
343-
// Returns the actual component property for a renamed an input renamed using `@Input('foo') _foo`.
344-
// return the `_foo` property
345-
const renamedInputProp = (prop: string) => {
346-
const input = factory.inputs.find((i) => i.templateName === prop);
347-
return (input && input.propName) || prop;
348-
};
340+
const mirror = reflectComponentType(component);
349341

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

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

359351
const addResolvable = (tuple: InputMapping) => ({
360352
prop: tuple.prop,
@@ -368,7 +360,7 @@ export class UIView implements OnInit, OnDestroy {
368360
.map(addResolvable)
369361
.filter((tuple) => tuple.resolvable && tuple.resolvable.resolved)
370362
.forEach((tuple) => {
371-
component[tuple.prop] = injector.get(tuple.resolvable.token);
363+
componentRef.setInput(tuple.prop, injector.get(tuple.resolvable.token));
372364
});
373365
}
374366
}

src/interface.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { StateDeclaration, _ViewDeclaration, Transition, HookResult } from '@uirouter/core';
22
import { Component, Type } from '@angular/core';
3-
import { ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';
3+
import { ComponentTypeCallback, ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';
44

55
/**
66
* The StateDeclaration object is used to define a state or nested state.
@@ -25,7 +25,7 @@ import { ModuleTypeCallback } from './lazyLoad/lazyLoadNgModule';
2525
* }
2626
* ```
2727
*/
28-
export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaration {
28+
export interface Ng2StateDeclaration<T = unknown> extends StateDeclaration, Ng2ViewDeclaration<T> {
2929
/**
3030
* An optional object used to define multiple named views.
3131
*
@@ -152,10 +152,28 @@ export interface Ng2StateDeclaration extends StateDeclaration, Ng2ViewDeclaratio
152152
* }
153153
* ```
154154
*/
155-
loadChildren?: ModuleTypeCallback;
155+
loadChildren?: ModuleTypeCallback<T>;
156+
157+
/**
158+
* A function used to lazy load a `Component`.
159+
*
160+
* When the state is activate the `loadComponent` property should lazy load a standalone `Component`
161+
* and use it to render the view of the state
162+
*
163+
* ### Example:
164+
* ```ts
165+
* var homeState = {
166+
* name: 'home',
167+
* url: '/home',
168+
* loadComponent: () => import('./home/home.component')
169+
* .then(result => result.HomeComponent)
170+
* }
171+
* ```
172+
*/
173+
loadComponent?: ComponentTypeCallback<T>;
156174
}
157175

158-
export interface Ng2ViewDeclaration extends _ViewDeclaration {
176+
export interface Ng2ViewDeclaration<T = unknown> extends _ViewDeclaration {
159177
/**
160178
* The `Component` class to use for this view.
161179
*
@@ -238,7 +256,7 @@ export interface Ng2ViewDeclaration extends _ViewDeclaration {
238256
* }
239257
* ```
240258
*/
241-
component?: Type<any>;
259+
component?: Type<T>;
242260

243261
/**
244262
* An object which maps `resolve` keys to [[component]] `bindings`.

src/lazyLoad/lazyLoadNgModule.ts

+94-44
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { NgModuleRef, Injector, NgModuleFactory, Type, Compiler } from '@angular/core';
1+
import { NgModuleRef, Injector, Type, createNgModule, InjectionToken, isStandalone } from '@angular/core';
22
import {
33
Transition,
44
LazyLoadResult,
55
UIRouter,
66
Resolvable,
77
NATIVE_INJECTOR_TOKEN,
8-
isString,
98
unnestR,
109
inArray,
1110
StateObject,
@@ -15,6 +14,7 @@ import {
1514
import { UIROUTER_MODULE_TOKEN, UIROUTER_ROOT_MODULE } from '../injectionTokens';
1615
import { RootModule, StatesModule } from '../uiRouterNgModule';
1716
import { applyModuleConfig } from '../uiRouterConfig';
17+
import { Ng2StateDeclaration } from '../interface';
1818

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

3131
/**
3232
* Returns a function which lazy loads a nested module
@@ -36,29 +36,16 @@ export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
3636
* It could also be used manually as a [[StateDeclaration.lazyLoad]] property to lazy load an `NgModule` and its state(s).
3737
*
3838
* #### Example:
39-
* Using `import()` and named export of `HomeModule`
40-
* ```js
41-
* declare var System;
39+
* ```ts
4240
* var futureState = {
4341
* name: 'home.**',
4442
* url: '/home',
4543
* lazyLoad: loadNgModule(() => import('./home/home.module').then(result => result.HomeModule))
4644
* }
4745
* ```
4846
*
49-
* #### Example:
50-
* Using a path (string) to the module
51-
* ```js
52-
* var futureState = {
53-
* name: 'home.**',
54-
* url: '/home',
55-
* lazyLoad: loadNgModule('./home/home.module#HomeModule')
56-
* }
57-
* ```
58-
*
5947
*
60-
* @param moduleToLoad a path (string) to the NgModule to load.
61-
* Or a function which loads the NgModule code which should
48+
* @param moduleToLoad function which loads the NgModule code which should
6249
* return a reference to the `NgModule` class being loaded (or a `Promise` for it).
6350
*
6451
* @returns A function which takes a transition, which:
@@ -67,17 +54,15 @@ export type ModuleTypeCallback = () => Type<any> | Promise<Type<any>>;
6754
* - Finds the "replacement state" for the target state, and adds the new NgModule Injector to it (as a resolve)
6855
* - Returns the new states array
6956
*/
70-
export function loadNgModule(
71-
moduleToLoad: ModuleTypeCallback
57+
export function loadNgModule<T>(
58+
moduleToLoad: ModuleTypeCallback<T>
7259
): (transition: Transition, stateObject: StateDeclaration) => Promise<LazyLoadResult> {
7360
return (transition: Transition, stateObject: StateDeclaration) => {
74-
const ng2Injector = transition.injector().get(NATIVE_INJECTOR_TOKEN);
75-
76-
const createModule = (factory: NgModuleFactory<any>) => factory.create(ng2Injector);
7761

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

80-
return loadModuleFactory(moduleToLoad, ng2Injector).then(createModule).then(applyModule);
64+
return loadModuleFactory(moduleToLoad, ng2Injector)
65+
.then(moduleRef => applyNgModule(moduleRef, ng2Injector, stateObject));
8166
};
8267
}
8368

@@ -90,22 +75,18 @@ export function loadNgModule(
9075
*
9176
* @internal
9277
*/
93-
export function loadModuleFactory(
94-
moduleToLoad: ModuleTypeCallback,
78+
export function loadModuleFactory<T>(
79+
moduleToLoad: ModuleTypeCallback<T>,
9580
ng2Injector: Injector
96-
): Promise<NgModuleFactory<any>> {
97-
const compiler: Compiler = ng2Injector.get(Compiler);
98-
99-
const unwrapEsModuleDefault = (x) => (x && x.__esModule && x['default'] ? x['default'] : x);
81+
): Promise<NgModuleRef<T>> {
10082

10183
return Promise.resolve(moduleToLoad())
102-
.then(unwrapEsModuleDefault)
103-
.then((t: NgModuleFactory<any> | Type<any>) => {
104-
if (t instanceof NgModuleFactory) {
105-
return t;
106-
}
107-
return compiler.compileModuleAsync(t);
108-
});
84+
.then(_unwrapEsModuleDefault)
85+
.then((t: Type<T>) => createNgModule(t, ng2Injector));
86+
}
87+
88+
function _unwrapEsModuleDefault(x) {
89+
return x && x.__esModule && x['default'] ? x['default'] : x;
10990
}
11091

11192
/**
@@ -122,9 +103,8 @@ export function loadModuleFactory(
122103
*
123104
* @internal
124105
*/
125-
export function applyNgModule(
126-
transition: Transition,
127-
ng2Module: NgModuleRef<any>,
106+
export function applyNgModule<T>(
107+
ng2Module: NgModuleRef<T>,
128108
parentInjector: Injector,
129109
lazyLoadState: StateDeclaration
130110
): LazyLoadResult {
@@ -192,8 +172,78 @@ export function applyNgModule(
192172
*
193173
* @internal
194174
*/
195-
export function multiProviderParentChildDelta(parent: Injector, child: Injector, token: any) {
196-
const childVals: RootModule[] = child.get(token, []);
197-
const parentVals: RootModule[] = parent.get(token, []);
175+
export function multiProviderParentChildDelta<T>(parent: Injector, child: Injector, token: InjectionToken<T>): RootModule[] {
176+
const childVals: RootModule[] = child.get<RootModule[]>(token, []);
177+
const parentVals: RootModule[] = parent.get<RootModule[]>(token, []);
198178
return childVals.filter((val) => parentVals.indexOf(val) === -1);
199179
}
180+
181+
/**
182+
* A function that returns a Component, or a promise for a Component
183+
*
184+
* #### Example:
185+
* ```ts
186+
* export function loadFooComponent() {
187+
* return import('../foo/foo.component').then(result => result.FooComponent);
188+
* }
189+
* ```
190+
*/
191+
export type ComponentTypeCallback<T> = ModuleTypeCallback<T>;
192+
193+
/**
194+
* Returns a function which lazy loads a standalone component for the target state
195+
*
196+
* #### Example:
197+
* ```ts
198+
* var futureComponentState = {
199+
* name: 'home',
200+
* url: '/home',
201+
* lazyLoad: loadComponent(() => import('./home.component').then(result => result.HomeComponent))
202+
* }
203+
* ```
204+
*
205+
* @param callback function which loads the Component code which should
206+
* return a reference to the `Component` class being loaded (or a `Promise` for it).
207+
*
208+
* @returns A function which takes a transition, stateObject, and:
209+
* - Loads a standalone component
210+
* - replaces the component configuration of the stateObject.
211+
* - Returns the new states array
212+
*/
213+
export function loadComponent<T>(
214+
callback: ComponentTypeCallback<T>
215+
): (transition: Transition, stateObject: Ng2StateDeclaration) => Promise<LazyLoadResult> {
216+
return (transition: Transition, stateObject: Ng2StateDeclaration) => {
217+
218+
return Promise.resolve(callback())
219+
.then(_unwrapEsModuleDefault)
220+
.then((component: Type<T>) => applyComponent(component, transition, stateObject))
221+
}
222+
}
223+
224+
/**
225+
* Apply the lazy-loaded component to the stateObject.
226+
*
227+
* @internal
228+
* @param component reference to the component class
229+
* @param transition Transition object reference
230+
* @param stateObject target state configuration object
231+
*
232+
* @returns the new states array
233+
*/
234+
export function applyComponent<T>(
235+
component: Type<T>,
236+
transition: Transition,
237+
stateObject: Ng2StateDeclaration
238+
): LazyLoadResult {
239+
240+
if (!isStandalone(component)) throw new Error("Is not a standalone component.");
241+
242+
const registry = transition.router.stateRegistry;
243+
const current = stateObject.component;
244+
stateObject.component = component || current;
245+
const removed = registry.deregister(stateObject).map(child => child.self);
246+
const children = removed.filter(i => i.name != stateObject.name);
247+
248+
return { states: [stateObject, ...children] }
249+
}

src/statebuilders/lazyLoad.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LazyLoadResult, Transition, StateDeclaration } from '@uirouter/core'; // has or is using
22
import { BuilderFunction, StateObject } from '@uirouter/core';
3-
import { loadNgModule } from '../lazyLoad/lazyLoadNgModule';
3+
import { loadComponent, loadNgModule } from '../lazyLoad/lazyLoadNgModule';
44

55
/**
66
* This is a [[StateBuilder.builder]] function for ngModule lazy loading in Angular.
@@ -46,6 +46,7 @@ import { loadNgModule } from '../lazyLoad/lazyLoadNgModule';
4646
*
4747
*/
4848
export function ng2LazyLoadBuilder(state: StateObject, parent: BuilderFunction) {
49+
const loadComponentFn = state['loadComponent'];
4950
const loadNgModuleFn = state['loadChildren'];
50-
return loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
51+
return loadComponentFn ? loadComponent(loadComponentFn) : loadNgModuleFn ? loadNgModule(loadNgModuleFn) : state.lazyLoad;
5152
}

test-angular-versions/v19-standalone/cypress/e2e/sample_app.cy.js

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe('Angular app', () => {
5656
cy.get('a').contains('home').should('not.have.class', 'active');
5757
cy.get('a').contains('lazy.child').should('have.class', 'active');
5858
cy.get('#default').contains('lazy.child works');
59+
cy.get('#lazy-child-provided').contains('provided value');
5960
});
6061

6162
it('targets named views', () => {

0 commit comments

Comments
 (0)