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

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
df25acb
Updated uiSref directive to be an standalone directive
lindolo25 Aug 30, 2024
e89800e
Updated uiSrefActive directive to be a standalone directive
lindolo25 Aug 30, 2024
ec0f518
Updated uiSrefStatus directive to be a standalone directive
lindolo25 Aug 30, 2024
97cc602
Updated uiView component to be a standalone component
lindolo25 Aug 30, 2024
4a8d1bb
Imported directives instead of declaring them in UIRouterModule
lindolo25 Aug 30, 2024
45d4103
Created a root provider function for UIRouter.
lindolo25 Aug 30, 2024
5e3de9b
Fixed bug in uiView component not loading the CommonModule.
lindolo25 Aug 30, 2024
2955b86
Updated downstream sample app for the tests.
lindolo25 Aug 30, 2024
11fb548
Updated UISrefActive to use hostDirectives to implement the UISrefSta…
lindolo25 Aug 30, 2024
f3e91c2
Added provideUiRouter to global exports.
lindolo25 Aug 30, 2024
22a86b1
added a standalone v18 test project.
lindolo25 Aug 30, 2024
6942ca9
Added standalone version for downstream projects.
lindolo25 Aug 30, 2024
810ea5b
Updated package minor version
lindolo25 Aug 30, 2024
749a695
Add documentation for provideUIRouter() function.
lindolo25 Oct 26, 2024
3f35ea3
Updated Ng2StateDeclaration and Ng2ViewDeclaration to use generics an…
lindolo25 Oct 16, 2024
19ed32e
Updated Lazy load implementeation, replaced deprecated API with newes…
lindolo25 Oct 16, 2024
9e73147
Implemented load component functionality in the lazy load section.
lindolo25 Oct 16, 2024
29c022c
Updated implementation of the UIView component. replaced deprecated A…
lindolo25 Oct 16, 2024
420b786
Updated the lazyLoadBuilder to load components.
lindolo25 Oct 16, 2024
fc27f1c
Updated standalone example.
lindolo25 Oct 16, 2024
a7e6a89
Fixed bug applying input bindings in uiView component.
lindolo25 Oct 26, 2024
d69484a
Created a few new test cases for uiView for new features in component…
lindolo25 Oct 26, 2024
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
3 changes: 2 additions & 1 deletion downstream_projects.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"packageDir": "./dist",
"projects": {
"sample-app-angular": "https://github.com/ui-router/sample-app-angular.git",
"sample-app-angular": "https://github.com/lindolo25/sample-app-angular.git",
"angular18": "./test-angular-versions/v18",
"angular18standalone": "./test-angular-versions/v18-standalone",
"typescript54": "./test-typescript-versions/typescript5.4"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@uirouter/angular",
"description": "State-based routing for Angular",
"version": "14.0.0",
"version": "14.1.0",
"scripts": {
"clean": "shx rm -rf lib lib-esm _bundles _doc dist",
"compile": "npm run clean && ngc",
Expand Down
6 changes: 5 additions & 1 deletion src/directives/uiSref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { ReplaySubject, Subscription } from 'rxjs';
* @internal
* # blah blah blah
*/
@Directive({ selector: 'a[uiSref]' })
@Directive({
selector: 'a[uiSref]',
standalone: true
})
export class AnchorUISref {
constructor(public _el: ElementRef, public _renderer: Renderer2) {}

Expand Down Expand Up @@ -78,6 +81,7 @@ export class AnchorUISref {
@Directive({
selector: '[uiSref]',
exportAs: 'uiSref',
standalone: true
})
export class UISref implements OnChanges {
/**
Expand Down
5 changes: 5 additions & 0 deletions src/directives/uiSrefActive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ import { Subscription } from 'rxjs';
*/
@Directive({
selector: '[uiSrefActive],[uiSrefActiveEq]',
hostDirectives: [{
directive: UISrefStatus,
outputs: ['uiSrefStatus']
}],
standalone: true
})
export class UISrefActive {
private _classes: string[] = [];
Expand Down
3 changes: 2 additions & 1 deletion src/directives/uiSrefStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,9 @@ function mergeSrefStatus(left: SrefStatus, right: SrefStatus): SrefStatus {
* This API is subject to change.
*/
@Directive({
selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]',
selector: '[uiSrefStatus]',
exportAs: 'uiSrefStatus',
standalone: true
})
export class UISrefStatus {
/** current statuses of the state/params the uiSref directive is linking to */
Expand Down
37 changes: 16 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 All @@ -33,6 +34,7 @@ import {
} from '@uirouter/core';
import { Ng2ViewConfig } from '../statebuilders/views';
import { MergeInjector } from '../mergeInjector';
import { CommonModule } from '@angular/common';

/** @hidden */
let id = 0;
Expand All @@ -57,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 @@ -110,6 +112,8 @@ const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
@Component({
selector: 'ui-view, [ui-view]',
exportAs: 'uiView',
standalone: true,
imports: [CommonModule],
template: `
<ng-template #componentTarget></ng-template>
<ng-content *ngIf="!_componentRef"></ng-content>
Expand Down Expand Up @@ -290,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 @@ -324,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 @@ -333,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 @@ -365,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));
});
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export * from './statebuilders/lazyLoad';
export * from './statebuilders/views';
export * from './uiRouterConfig';
export * from './uiRouterNgModule';
export * from './provideUiRouter';

export * from '@uirouter/core';
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
101 changes: 72 additions & 29 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 Down Expand Up @@ -67,17 +67,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 +88,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 +116,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 +185,58 @@ 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>;

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))
}
}

/**
* @internal
* @param component
* @param transition
* @param stateObject
*/
export function applyComponent<T>(
component: Type<T>,
transition: Transition,
stateObject: Ng2StateDeclaration
): LazyLoadResult {

if (!isStandalone(component)) throw _notStandaloneError();

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] }
}

function _notStandaloneError(): Error {
return new Error("Is not standalone.");
}
Loading
Loading