Skip to content

Commit

Permalink
refactor(router): use real EnvironmentInjector in the `RouterOutlet…
Browse files Browse the repository at this point in the history
…` class

Currently, the `RouterOutlet` class uses a custom injector implementation to provide route-specific tokens (such as `ActivatedRoute`). This commit switches Router logic to use real `EnvironmentInjector` instead.

This commit partially addresses issue angular#54864 and there will be one more PR with extra changes to defer logic.
  • Loading branch information
AndrewKushnir committed Mar 18, 2024
1 parent ebd0709 commit 86e6809
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 102 deletions.
1 change: 1 addition & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,5 +323,6 @@ export {depsTracker as ɵdepsTracker, USE_RUNTIME_DEPS_TRACKER_FOR_JIT as ɵUSE_
export {generateStandaloneInDeclarationsError as ɵgenerateStandaloneInDeclarationsError} from './render3/jit/module';
export {getAsyncClassMetadataFn as ɵgetAsyncClassMetadataFn} from './render3/metadata';
export {InputFlags as ɵɵInputFlags} from './render3/interfaces/input_flags';
export {createEnvironmentInjectorWithScope as ɵcreateEnvironmentInjectorWithScope} from './render3/ng_module_ref';

// clang-format on
174 changes: 72 additions & 102 deletions packages/router/src/directives/router_outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {
ChangeDetectorRef,
ComponentRef,
Directive,
EnvironmentInjector,
EventEmitter,
inject,
Injectable,
InjectionToken,
Injector,
Input,
OnDestroy,
OnInit,
Output,
reflectComponentType,
SimpleChanges,
ViewContainerRef,
ɵRuntimeError as RuntimeError,
} from '@angular/core';
import {ChangeDetectorRef, ComponentRef, Directive, EnvironmentInjector, EventEmitter, inject, Injectable, InjectionToken, Input, OnDestroy, OnInit, Output, reflectComponentType, SimpleChanges, ViewContainerRef, ɵcreateEnvironmentInjectorWithScope as createEnvironmentInjectorWithScope, ɵRuntimeError as RuntimeError,} from '@angular/core';
import {combineLatest, of, Subscription} from 'rxjs';
import {switchMap} from 'rxjs/operators';

Expand Down Expand Up @@ -56,7 +38,7 @@ export interface RouterOutletContract {
isActivated: boolean;

/** The instance of the activated component or `null` if the outlet is not activated. */
component: Object | null;
component: Object|null;

/**
* The `Data` of the `ActivatedRoute` snapshot.
Expand All @@ -66,15 +48,15 @@ export interface RouterOutletContract {
/**
* The `ActivatedRoute` for the outlet or `null` if the outlet is not activated.
*/
activatedRoute: ActivatedRoute | null;
activatedRoute: ActivatedRoute|null;

/**
* Called by the `Router` when the outlet should activate (create a component).
*/
activateWith(
activatedRoute: ActivatedRoute,
environmentInjector: EnvironmentInjector | null,
): void;
activatedRoute: ActivatedRoute,
environmentInjector: EnvironmentInjector|null,
): void;

/**
* A request to destroy the currently activated component.
Expand Down Expand Up @@ -187,12 +169,12 @@ export interface RouterOutletContract {
standalone: true,
})
export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
private activated: ComponentRef<any> | null = null;
private activated: ComponentRef<any>|null = null;
/** @internal */
get activatedComponentRef(): ComponentRef<any> | null {
get activatedComponentRef(): ComponentRef<any>|null {
return this.activated;
}
private _activatedRoute: ActivatedRoute | null = null;
private _activatedRoute: ActivatedRoute|null = null;
/**
* The name of the outlet
*
Expand Down Expand Up @@ -290,17 +272,17 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
get component(): Object {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
return this.activated.instance;
}

get activatedRoute(): ActivatedRoute {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
return this._activatedRoute as ActivatedRoute;
}
Expand All @@ -318,8 +300,8 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
detach(): ComponentRef<any> {
if (!this.activated)
throw new RuntimeError(
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
RuntimeErrorCode.OUTLET_NOT_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated',
);
this.location.detach();
const cmp = this.activated;
Expand Down Expand Up @@ -350,26 +332,36 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
}
}

activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector | null) {
activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector|null) {
if (this.isActivated) {
throw new RuntimeError(
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot activate an already activated outlet',
RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot activate an already activated outlet',
);
}
this._activatedRoute = activatedRoute;
const location = this.location;
const snapshot = activatedRoute.snapshot;
const component = snapshot.component!;
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
const injector = new OutletInjector(activatedRoute, childContexts, location.injector);
const providers = [
{provide: ActivatedRoute, useValue: activatedRoute},
{provide: ChildrenOutletContexts, useValue: childContexts},
];

// TODO: add a comment on why we do not have `any` scope here.
const scopes = new Set<any>(['environment']);
this.activated = location.createComponent(component, {
index: location.length,
injector,
environmentInjector: environmentInjector ?? this.environmentInjector,
injector: location.injector,
environmentInjector: createEnvironmentInjectorWithScope(
providers,
environmentInjector ?? this.environmentInjector,
scopes,
),
});

// Calling `markForCheck` to make sure we will run the change detection when the
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
this.changeDetector.markForCheck();
Expand All @@ -378,26 +370,6 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
}
}

class OutletInjector implements Injector {
constructor(
private route: ActivatedRoute,
private childContexts: ChildrenOutletContexts,
private parent: Injector,
) {}

get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
return this.route;
}

if (token === ChildrenOutletContexts) {
return this.childContexts;
}

return this.parent.get(token, notFoundValue);
}
}

export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');

/**
Expand Down Expand Up @@ -430,48 +402,46 @@ export class RoutedComponentInputBinder {

private subscribeToRouteData(outlet: RouterOutlet) {
const {activatedRoute} = outlet;
const dataSubscription = combineLatest([
activatedRoute.queryParams,
activatedRoute.params,
activatedRoute.data,
])
.pipe(
switchMap(([queryParams, params, data], index) => {
data = {...queryParams, ...params, ...data};
// Get the first result from the data subscription synchronously so it's available to
// the component as soon as possible (and doesn't require a second change detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
}),
)
.subscribe((data) => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (
!outlet.isActivated ||
!outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute ||
activatedRoute.component === null
) {
this.unsubscribeFromRouteData(outlet);
return;
}

const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}

for (const {templateName} of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});
const dataSubscription =
combineLatest([
activatedRoute.queryParams,
activatedRoute.params,
activatedRoute.data,
])
.pipe(
switchMap(([queryParams, params, data], index) => {
data = {...queryParams, ...params, ...data};
// Get the first result from the data subscription synchronously so it's available
// to the component as soon as possible (and doesn't require a second change
// detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
}),
)
.subscribe((data) => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (!outlet.isActivated || !outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute || activatedRoute.component === null) {
this.unsubscribeFromRouteData(outlet);
return;
}

const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}

for (const {templateName} of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});

this.outletDataSubscriptions.set(outlet, dataSubscription);
}
Expand Down

0 comments on commit 86e6809

Please sign in to comment.