From 2fb0ebad2b8c3cb1c2c608d2d780dbb3704f7fde Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 5 Nov 2025 15:46:42 -0800 Subject: [PATCH 1/2] feat(router): Execute RunGuardsAndResolvers function in injection context Allows more sophisticated checks based on information available in DI (e.g. the router state). Use-cases have been described in #53944 / https://github.com/angular/angular/issues/31843#issuecomment-1890955590 resolves #53944 --- packages/router/src/navigation_transition.ts | 7 +- packages/router/src/utils/preactivation.ts | 64 ++++++++++++++++--- .../router/test/integration/guards.spec.ts | 35 ++++++++++ packages/router/test/router.spec.ts | 8 ++- 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/packages/router/src/navigation_transition.ts b/packages/router/src/navigation_transition.ts index ef1ee78c6d2c..adf59ffcc3c9 100644 --- a/packages/router/src/navigation_transition.ts +++ b/packages/router/src/navigation_transition.ts @@ -623,7 +623,12 @@ export class NavigationTransitions { this.currentTransition = overallTransitionState = { ...t, - guards: getAllRouteGuards(t.targetSnapshot!, t.currentSnapshot, this.rootContexts), + guards: getAllRouteGuards( + t.targetSnapshot!, + t.currentSnapshot, + this.rootContexts, + this.environmentInjector, + ), }; return overallTransitionState; }), diff --git a/packages/router/src/utils/preactivation.ts b/packages/router/src/utils/preactivation.ts index e02ca5263c87..022cdce4c711 100644 --- a/packages/router/src/utils/preactivation.ts +++ b/packages/router/src/utils/preactivation.ts @@ -6,9 +6,16 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injector, ProviderToken, ɵisInjectable as isInjectable} from '@angular/core'; - +import { + Injector, + ProviderToken, + ɵisInjectable as isInjectable, + EnvironmentInjector, + runInInjectionContext, +} from '@angular/core'; import {RunGuardsAndResolvers} from '../models'; +import {getClosestRouteInjector} from './config'; + import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; import { ActivatedRouteSnapshot, @@ -42,11 +49,18 @@ export function getAllRouteGuards( future: RouterStateSnapshot, curr: RouterStateSnapshot, parentContexts: ChildrenOutletContexts, + environmentInjector: EnvironmentInjector, ): Checks { const futureRoot = future._root; const currRoot = curr ? curr._root : null; - return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); + return getChildRouteGuards( + futureRoot, + currRoot, + parentContexts, + [futureRoot.value], + environmentInjector, + ); } export function getCanActivateChild( @@ -80,6 +94,7 @@ function getChildRouteGuards( currNode: TreeNode | null, contexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], + environmentInjector: EnvironmentInjector, checks: Checks = { canDeactivateChecks: [], canActivateChecks: [], @@ -89,7 +104,14 @@ function getChildRouteGuards( // Process the children of the future route futureNode.children.forEach((c) => { - getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks); + getRouteGuards( + c, + prevChildren[c.value.outlet], + contexts, + futurePath.concat([c.value]), + environmentInjector, + checks, + ); delete prevChildren[c.value.outlet]; }); @@ -106,6 +128,7 @@ function getRouteGuards( currNode: TreeNode, parentContexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], + environmentInjector: EnvironmentInjector, checks: Checks = { canDeactivateChecks: [], canActivateChecks: [], @@ -121,6 +144,7 @@ function getRouteGuards( curr, future, future.routeConfig!.runGuardsAndResolvers, + environmentInjector, ); if (shouldRun) { checks.canActivateChecks.push(new CanActivate(futurePath)); @@ -137,12 +161,20 @@ function getRouteGuards( currNode, context ? context.children : null, futurePath, + environmentInjector, checks, ); // if we have a componentless route, we recurse but keep the same outlet map. } else { - getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks); + getChildRouteGuards( + futureNode, + currNode, + parentContexts, + futurePath, + environmentInjector, + checks, + ); } if (shouldRun && context && context.outlet && context.outlet.isActivated) { @@ -156,11 +188,25 @@ function getRouteGuards( checks.canActivateChecks.push(new CanActivate(futurePath)); // If we have a component, we need to go through an outlet. if (future.component) { - getChildRouteGuards(futureNode, null, context ? context.children : null, futurePath, checks); + getChildRouteGuards( + futureNode, + null, + context ? context.children : null, + futurePath, + environmentInjector, + checks, + ); // if we have a componentless route, we recurse but keep the same outlet map. } else { - getChildRouteGuards(futureNode, null, parentContexts, futurePath, checks); + getChildRouteGuards( + futureNode, + null, + parentContexts, + futurePath, + environmentInjector, + checks, + ); } } @@ -171,9 +217,11 @@ function shouldRunGuardsAndResolvers( curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, mode: RunGuardsAndResolvers | undefined, + environmentInjector: EnvironmentInjector, ): boolean { if (typeof mode === 'function') { - return mode(curr, future); + const injector = getClosestRouteInjector(future) ?? environmentInjector; + return runInInjectionContext(injector, () => mode(curr, future)); } switch (mode) { case 'pathParamsChange': diff --git a/packages/router/test/integration/guards.spec.ts b/packages/router/test/integration/guards.spec.ts index 716db0b12217..565cc8aee70e 100644 --- a/packages/router/test/integration/guards.spec.ts +++ b/packages/router/test/integration/guards.spec.ts @@ -2395,5 +2395,40 @@ export function guardsIntegrationSuite() { await router.navigateByUrl(''); expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']); }); + + it('should run in injection context', async () => { + @Injectable({providedIn: 'root'}) + class MyService { + canRun = false; + } + + let resolveCount = 0; + const routes = [ + { + path: 'a', + children: [], + resolve: { + x: () => ++resolveCount, + }, + runGuardsAndResolvers: () => inject(MyService).canRun, + }, + ]; + const router = TestBed.inject(Router); + router.resetConfig(routes); + const service = TestBed.inject(MyService); + + await router.navigateByUrl('/a'); + expect(router.url).toEqual('/a'); + // Always run on activation + expect(resolveCount).toBe(1); + + service.canRun = false; + await router.navigateByUrl('/a?q=1'); + expect(resolveCount).toBe(1); + + service.canRun = true; + await router.navigateByUrl('/a?q=2'); + expect(resolveCount).toBe(2); + }); }); } diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index fd221fc04cd8..612b72b39d94 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -207,6 +207,7 @@ describe('Router', () => { futureState, empty, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), + TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; @@ -267,6 +268,7 @@ describe('Router', () => { futureState, empty, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), + TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; @@ -325,6 +327,7 @@ describe('Router', () => { futureState, currentState, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), + TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; @@ -401,6 +404,7 @@ describe('Router', () => { futureState, currentState, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), + TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; @@ -894,7 +898,7 @@ function checkResolveData( // Since we only test the guards and their resolve data function, we don't need to provide // a full navigation transition object with all properties set. of({ - guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)), + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector), } as NavigationTransition) .pipe(resolveDataOperator('emptyOnly', injector)) .subscribe(check, (e) => { @@ -911,7 +915,7 @@ function checkGuards( // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. of({ - guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)), + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector), } as NavigationTransition) .pipe(checkGuardsOperator(injector)) .subscribe({ From 22664ce3040a0cd602d4059d3a3a88fb2fb5a74b Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 5 Nov 2025 16:10:21 -0800 Subject: [PATCH 2/2] refactor(router): Store route injector on ActivatedRoute instance This eliminates the need to pass around the EnvironmentInjector everywhere we need the injection context for a route. --- .../router/bundle.golden_symbols.json | 1 - packages/router/src/navigation_transition.ts | 19 +++---- packages/router/src/operators/check_guards.ts | 30 +++-------- packages/router/src/operators/resolve_data.ts | 13 ++--- packages/router/src/recognize.ts | 3 ++ packages/router/src/recognize_rxjs.ts | 3 ++ packages/router/src/router_outlet_context.ts | 3 +- packages/router/src/router_state.ts | 19 +++++-- .../router/src/statemanager/state_manager.ts | 4 +- packages/router/src/utils/config.ts | 37 ------------- packages/router/src/utils/preactivation.ts | 54 +++---------------- .../router/test/create_router_state.spec.ts | 2 +- packages/router/test/helpers.ts | 14 ++--- packages/router/test/router.spec.ts | 22 ++++---- 14 files changed, 68 insertions(+), 156 deletions(-) diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index d46a700bb42f..7131d7b755f8 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -633,7 +633,6 @@ "getCanActivateChild", "getChildRouteGuards", "getClosestRElement", - "getClosestRouteInjector", "getClosureSafeProperty", "getComponentDef", "getComponentId", diff --git a/packages/router/src/navigation_transition.ts b/packages/router/src/navigation_transition.ts index adf59ffcc3c9..d2b15a4cbc2b 100644 --- a/packages/router/src/navigation_transition.ts +++ b/packages/router/src/navigation_transition.ts @@ -80,7 +80,6 @@ import {UrlHandlingStrategy} from './url_handling_strategy'; import {UrlSerializer, UrlTree} from './url_tree'; import {Checks, getAllRouteGuards} from './utils/preactivation'; import {CREATE_VIEW_TRANSITION} from './utils/view_transition'; -import {getClosestRouteInjector} from './utils/config'; import {abortSignalToObservable} from './utils/abort_signal_to_observable'; /** @@ -572,7 +571,10 @@ export class NavigationTransitions { restoredState, ); this.events.next(navStart); - const targetSnapshot = createEmptyState(this.rootComponentType).snapshot; + const targetSnapshot = createEmptyState( + this.rootComponentType, + this.environmentInjector, + ).snapshot; this.currentTransition = overallTransitionState = { ...t, @@ -623,17 +625,12 @@ export class NavigationTransitions { this.currentTransition = overallTransitionState = { ...t, - guards: getAllRouteGuards( - t.targetSnapshot!, - t.currentSnapshot, - this.rootContexts, - this.environmentInjector, - ), + guards: getAllRouteGuards(t.targetSnapshot!, t.currentSnapshot, this.rootContexts), }; return overallTransitionState; }), - checkGuards(this.environmentInjector, (evt: Event) => this.events.next(evt)), + checkGuards((evt: Event) => this.events.next(evt)), switchMap((t) => { overallTransitionState.guardsResult = t.guardsResult; @@ -674,7 +671,7 @@ export class NavigationTransitions { let dataResolved = false; return of(t).pipe( - resolveData(this.paramsInheritanceStrategy, this.environmentInjector), + resolveData(this.paramsInheritanceStrategy), tap({ next: () => { dataResolved = true; @@ -708,7 +705,7 @@ export class NavigationTransitions { if (route.routeConfig?._loadedComponent) { route.component = route.routeConfig?._loadedComponent; } else if (route.routeConfig?.loadComponent) { - const injector = getClosestRouteInjector(route) ?? this.environmentInjector; + const injector = route._environmentInjector; loaders.push( this.configLoader .loadComponent(injector, route.routeConfig) diff --git a/packages/router/src/operators/check_guards.ts b/packages/router/src/operators/check_guards.ts index 706e3f521d27..579a5db2e31f 100644 --- a/packages/router/src/operators/check_guards.ts +++ b/packages/router/src/operators/check_guards.ts @@ -34,7 +34,6 @@ import type {NavigationTransition} from '../navigation_transition'; import type {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state'; import {UrlSegment, UrlSerializer} from '../url_tree'; import {wrapIntoObservable} from '../utils/collection'; -import {getClosestRouteInjector} from '../utils/config'; import { CanActivate, CanDeactivate, @@ -54,7 +53,6 @@ import {prioritizedGuardValue} from './prioritized_guard_value'; import {takeUntilAbort} from '../utils/abort_signal_to_observable'; export function checkGuards( - injector: EnvironmentInjector, forwardEvent?: (evt: Event) => void, ): MonoTypeOperatorFunction { return mergeMap((t) => { @@ -67,15 +65,10 @@ export function checkGuards( return of({...t, guardsResult: true}); } - return runCanDeactivateChecks( - canDeactivateChecks, - targetSnapshot!, - currentSnapshot, - injector, - ).pipe( + return runCanDeactivateChecks(canDeactivateChecks, targetSnapshot!, currentSnapshot).pipe( mergeMap((canDeactivate) => { return canDeactivate && isBoolean(canDeactivate) - ? runCanActivateChecks(targetSnapshot!, canActivateChecks, injector, forwardEvent) + ? runCanActivateChecks(targetSnapshot!, canActivateChecks, forwardEvent) : of(canDeactivate); }), map((guardsResult) => ({...t, guardsResult})), @@ -87,12 +80,9 @@ function runCanDeactivateChecks( checks: CanDeactivate[], futureRSS: RouterStateSnapshot, currRSS: RouterStateSnapshot, - injector: EnvironmentInjector, ) { return from(checks).pipe( - mergeMap((check) => - runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector), - ), + mergeMap((check) => runCanDeactivate(check.component, check.route, currRSS, futureRSS)), first((result) => { return result !== true; }, true), @@ -102,7 +92,6 @@ function runCanDeactivateChecks( function runCanActivateChecks( futureSnapshot: RouterStateSnapshot, checks: CanActivate[], - injector: EnvironmentInjector, forwardEvent?: (evt: Event) => void, ) { return from(checks).pipe( @@ -110,8 +99,8 @@ function runCanActivateChecks( return concat( fireChildActivationStart(check.route.parent, forwardEvent), fireActivationStart(check.route, forwardEvent), - runCanActivateChild(futureSnapshot, check.path, injector), - runCanActivate(futureSnapshot, check.route, injector), + runCanActivateChild(futureSnapshot, check.path), + runCanActivate(futureSnapshot, check.route), ); }), first((result) => { @@ -159,14 +148,13 @@ function fireChildActivationStart( function runCanActivate( futureRSS: RouterStateSnapshot, futureARS: ActivatedRouteSnapshot, - injector: EnvironmentInjector, ): Observable { const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of(true); const canActivateObservables = canActivate.map((canActivate) => { return defer(() => { - const closestInjector = getClosestRouteInjector(futureARS) ?? injector; + const closestInjector = futureARS._environmentInjector; const guard = getTokenOrFunctionIdentity( canActivate as ProviderToken, closestInjector, @@ -185,7 +173,6 @@ function runCanActivate( function runCanActivateChild( futureRSS: RouterStateSnapshot, path: ActivatedRouteSnapshot[], - injector: EnvironmentInjector, ): Observable { const futureARS = path[path.length - 1]; @@ -199,7 +186,7 @@ function runCanActivateChild( return defer(() => { const guardsMapped = d.guards.map( (canActivateChild: CanActivateChildFn | ProviderToken) => { - const closestInjector = getClosestRouteInjector(d.node) ?? injector; + const closestInjector = d.node._environmentInjector; const guard = getTokenOrFunctionIdentity<{canActivateChild: CanActivateChildFn}>( canActivateChild, closestInjector, @@ -223,12 +210,11 @@ function runCanDeactivate( currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot, futureRSS: RouterStateSnapshot, - injector: EnvironmentInjector, ): Observable { const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null; if (!canDeactivate || canDeactivate.length === 0) return of(true); const canDeactivateObservables = canDeactivate.map((c: any) => { - const closestInjector = getClosestRouteInjector(currARS) ?? injector; + const closestInjector = currARS._environmentInjector; const guard = getTokenOrFunctionIdentity(c, closestInjector); const guardVal = isCanDeactivate(guard) ? guard.canDeactivate(component, currARS, currRSS, futureRSS) diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index 2c1bcd20856e..d8666db6911d 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -20,7 +20,6 @@ import { } from '../router_state'; import {RouteTitleKey} from '../shared'; import {getDataKeys, wrapIntoObservable} from '../utils/collection'; -import {getClosestRouteInjector} from '../utils/config'; import {getTokenOrFunctionIdentity} from '../utils/preactivation'; import {isEmptyError} from '../utils/type_guards'; import {redirectingNavigationError} from '../navigation_canceling_error'; @@ -28,7 +27,6 @@ import {DefaultUrlSerializer} from '../url_tree'; export function resolveData( paramsInheritanceStrategy: 'emptyOnly' | 'always', - injector: EnvironmentInjector, ): MonoTypeOperatorFunction { return mergeMap((t) => { const { @@ -57,7 +55,7 @@ export function resolveData( return from(routesNeedingDataUpdates).pipe( concatMap((route) => { if (routesWithResolversToRun.has(route)) { - return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector); + return runResolve(route, targetSnapshot!, paramsInheritanceStrategy); } else { route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve; return of(void 0); @@ -82,7 +80,6 @@ function runResolve( futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, paramsInheritanceStrategy: 'emptyOnly' | 'always', - injector: EnvironmentInjector, ) { const config = futureARS.routeConfig; const resolve = futureARS._resolve; @@ -91,7 +88,7 @@ function runResolve( } return defer(() => { futureARS.data = getInherited(futureARS, futureARS.parent, paramsInheritanceStrategy).resolve; - return resolveNode(resolve, futureARS, futureRSS, injector).pipe( + return resolveNode(resolve, futureARS, futureRSS).pipe( map((resolvedData: any) => { futureARS._resolvedData = resolvedData; futureARS.data = {...futureARS.data, ...resolvedData}; @@ -105,7 +102,6 @@ function resolveNode( resolve: ResolveData, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, - injector: EnvironmentInjector, ): Observable { const keys = getDataKeys(resolve); if (keys.length === 0) { @@ -114,7 +110,7 @@ function resolveNode( const data: {[k: string | symbol]: any} = {}; return from(keys).pipe( mergeMap((key) => - getResolver(resolve[key], futureARS, futureRSS, injector).pipe( + getResolver(resolve[key], futureARS, futureRSS).pipe( first(), tap((value: any) => { if (value instanceof RedirectCommand) { @@ -134,9 +130,8 @@ function getResolver( injectionToken: ProviderToken | Function, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, - injector: EnvironmentInjector, ): Observable { - const closestInjector = getClosestRouteInjector(futureARS) ?? injector; + const closestInjector = futureARS._environmentInjector; const resolver = getTokenOrFunctionIdentity(injectionToken, closestInjector); const resolverValue = resolver.resolve ? resolver.resolve(futureARS, futureRSS) diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 1cec67301d28..b99cba30fa30 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -128,6 +128,7 @@ export class Recognizer { this.rootComponentType, null, {}, + this.injector, ); try { const children = await this.processSegmentGroup( @@ -357,6 +358,7 @@ export class Recognizer { route.component ?? route._loadedComponent ?? null, route, getResolve(route), + injector, ); const inherited = getInherited(currentSnapshot, parentRoute, this.paramsInheritanceStrategy); currentSnapshot.params = Object.freeze(inherited.params); @@ -425,6 +427,7 @@ export class Recognizer { route.component ?? route._loadedComponent ?? null, route, getResolve(route), + injector, ); const inherited = getInherited(snapshot, parentRoute, this.paramsInheritanceStrategy); snapshot.params = Object.freeze(inherited.params); diff --git a/packages/router/src/recognize_rxjs.ts b/packages/router/src/recognize_rxjs.ts index 0ddc0c233ef3..e1220e136e1e 100644 --- a/packages/router/src/recognize_rxjs.ts +++ b/packages/router/src/recognize_rxjs.ts @@ -147,6 +147,7 @@ export class Recognizer { this.rootComponentType, null, {}, + this.injector, ); return this.processSegmentGroup( this.injector, @@ -385,6 +386,7 @@ export class Recognizer { route.component ?? route._loadedComponent ?? null, route, getResolve(route), + injector, ); const inherited = getInherited(currentSnapshot, parentRoute, this.paramsInheritanceStrategy); currentSnapshot.params = Object.freeze(inherited.params); @@ -452,6 +454,7 @@ export class Recognizer { route.component ?? route._loadedComponent ?? null, route, getResolve(route), + injector, ); const inherited = getInherited(snapshot, parentRoute, this.paramsInheritanceStrategy); snapshot.params = Object.freeze(inherited.params); diff --git a/packages/router/src/router_outlet_context.ts b/packages/router/src/router_outlet_context.ts index 24cb45561568..64b1ead82935 100644 --- a/packages/router/src/router_outlet_context.ts +++ b/packages/router/src/router_outlet_context.ts @@ -10,7 +10,6 @@ import {ComponentRef, EnvironmentInjector, Injectable} from '@angular/core'; import type {RouterOutletContract} from './directives/router_outlet'; import {ActivatedRoute} from './router_state'; -import {getClosestRouteInjector} from './utils/config'; /** * Store contextual information about a `RouterOutlet` @@ -23,7 +22,7 @@ export class OutletContext { children: ChildrenOutletContexts; attachRef: ComponentRef | null = null; get injector(): EnvironmentInjector { - return getClosestRouteInjector(this.route?.snapshot) ?? this.rootInjector; + return this.route?.snapshot._environmentInjector ?? this.rootInjector; } constructor(private readonly rootInjector: EnvironmentInjector) { diff --git a/packages/router/src/router_state.ts b/packages/router/src/router_state.ts index 868d52b89d1c..5d744cbc0292 100644 --- a/packages/router/src/router_state.ts +++ b/packages/router/src/router_state.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Type} from '@angular/core'; +import {EnvironmentInjector, Type} from '@angular/core'; import {BehaviorSubject, Observable, of} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -63,8 +63,11 @@ export class RouterState extends Tree { } } -export function createEmptyState(rootComponent: Type | null): RouterState { - const snapshot = createEmptyStateSnapshot(rootComponent); +export function createEmptyState( + rootComponent: Type | null, + injector: EnvironmentInjector, +): RouterState { + const snapshot = createEmptyStateSnapshot(rootComponent, injector); const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]); const emptyParams = new BehaviorSubject({}); const emptyData = new BehaviorSubject({}); @@ -84,7 +87,10 @@ export function createEmptyState(rootComponent: Type | null): RouterState { return new RouterState(new TreeNode(activated, []), snapshot); } -export function createEmptyStateSnapshot(rootComponent: Type | null): RouterStateSnapshot { +export function createEmptyStateSnapshot( + rootComponent: Type | null, + injector: EnvironmentInjector, +): RouterStateSnapshot { const emptyParams = {}; const emptyData = {}; const emptyQueryParams = {}; @@ -99,6 +105,7 @@ export function createEmptyStateSnapshot(rootComponent: Type | null): Route rootComponent, null, {}, + injector, ); return new RouterStateSnapshot('', new TreeNode(activated, [])); } @@ -330,6 +337,8 @@ export class ActivatedRouteSnapshot { _paramMap?: ParamMap; /** @internal */ _queryParamMap?: ParamMap; + /** @internal */ + readonly _environmentInjector: EnvironmentInjector; /** The resolved route title */ get title(): string | undefined { @@ -374,9 +383,11 @@ export class ActivatedRouteSnapshot { public component: Type | null, routeConfig: Route | null, resolve: ResolveData, + environmentInjector: EnvironmentInjector, ) { this.routeConfig = routeConfig; this._resolve = resolve; + this._environmentInjector = environmentInjector; } /** The root of the router state */ diff --git a/packages/router/src/statemanager/state_manager.ts b/packages/router/src/statemanager/state_manager.ts index 0d7fc76569de..335a2768a05d 100644 --- a/packages/router/src/statemanager/state_manager.ts +++ b/packages/router/src/statemanager/state_manager.ts @@ -7,7 +7,7 @@ */ import {Location} from '@angular/common'; -import {inject, Injectable} from '@angular/core'; +import {EnvironmentInjector, inject, Injectable} from '@angular/core'; import {SubscriptionLike} from 'rxjs'; import { @@ -104,7 +104,7 @@ export abstract class StateManager { } } - protected routerState = createEmptyState(null); + protected routerState = createEmptyState(null, inject(EnvironmentInjector)); /** Returns the current RouterState. */ getRouterState(): RouterState { diff --git a/packages/router/src/utils/config.ts b/packages/router/src/utils/config.ts index 4555a86704e1..5c27269d7b14 100644 --- a/packages/router/src/utils/config.ts +++ b/packages/router/src/utils/config.ts @@ -239,40 +239,3 @@ export function sortByMatchingOutlets(routes: Routes, outletName: string): Route sortedConfig.push(...routes.filter((r) => getOutlet(r) !== outletName)); return sortedConfig; } - -/** - * Gets the first injector in the snapshot's parent tree. - * - * If the `Route` has a static list of providers, the returned injector will be the one created from - * those. If it does not exist, the returned injector may come from the parents, which may be from a - * loaded config or their static providers. - * - * Returns `null` if there is neither this nor any parents have a stored injector. - * - * Generally used for retrieving the injector to use for getting tokens for guards/resolvers and - * also used for getting the correct injector to use for creating components. - */ -export function getClosestRouteInjector( - snapshot: ActivatedRouteSnapshot | undefined, -): EnvironmentInjector | null { - if (!snapshot) return null; - - // If the current route has its own injector, which is created from the static providers on the - // route itself, we should use that. Otherwise, we start at the parent since we do not want to - // include the lazy loaded injector from this route. - if (snapshot.routeConfig?._injector) { - return snapshot.routeConfig._injector; - } - - for (let s = snapshot.parent; s; s = s.parent) { - const route = s.routeConfig; - // Note that the order here is important. `_loadedInjector` stored on the route with - // `loadChildren: () => NgModule` so it applies to child routes with priority. The `_injector` - // is created from the static providers on that parent route, so it applies to the children as - // well, but only if there is no lazy loaded NgModuleRef injector. - if (route?._loadedInjector) return route._loadedInjector; - if (route?._injector) return route._injector; - } - - return null; -} diff --git a/packages/router/src/utils/preactivation.ts b/packages/router/src/utils/preactivation.ts index 022cdce4c711..fc9a35e2f65c 100644 --- a/packages/router/src/utils/preactivation.ts +++ b/packages/router/src/utils/preactivation.ts @@ -14,7 +14,6 @@ import { runInInjectionContext, } from '@angular/core'; import {RunGuardsAndResolvers} from '../models'; -import {getClosestRouteInjector} from './config'; import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; import { @@ -49,18 +48,11 @@ export function getAllRouteGuards( future: RouterStateSnapshot, curr: RouterStateSnapshot, parentContexts: ChildrenOutletContexts, - environmentInjector: EnvironmentInjector, ): Checks { const futureRoot = future._root; const currRoot = curr ? curr._root : null; - return getChildRouteGuards( - futureRoot, - currRoot, - parentContexts, - [futureRoot.value], - environmentInjector, - ); + return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); } export function getCanActivateChild( @@ -94,7 +86,6 @@ function getChildRouteGuards( currNode: TreeNode | null, contexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], - environmentInjector: EnvironmentInjector, checks: Checks = { canDeactivateChecks: [], canActivateChecks: [], @@ -104,14 +95,7 @@ function getChildRouteGuards( // Process the children of the future route futureNode.children.forEach((c) => { - getRouteGuards( - c, - prevChildren[c.value.outlet], - contexts, - futurePath.concat([c.value]), - environmentInjector, - checks, - ); + getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks); delete prevChildren[c.value.outlet]; }); @@ -128,7 +112,6 @@ function getRouteGuards( currNode: TreeNode, parentContexts: ChildrenOutletContexts | null, futurePath: ActivatedRouteSnapshot[], - environmentInjector: EnvironmentInjector, checks: Checks = { canDeactivateChecks: [], canActivateChecks: [], @@ -144,7 +127,6 @@ function getRouteGuards( curr, future, future.routeConfig!.runGuardsAndResolvers, - environmentInjector, ); if (shouldRun) { checks.canActivateChecks.push(new CanActivate(futurePath)); @@ -161,20 +143,12 @@ function getRouteGuards( currNode, context ? context.children : null, futurePath, - environmentInjector, checks, ); // if we have a componentless route, we recurse but keep the same outlet map. } else { - getChildRouteGuards( - futureNode, - currNode, - parentContexts, - futurePath, - environmentInjector, - checks, - ); + getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks); } if (shouldRun && context && context.outlet && context.outlet.isActivated) { @@ -188,25 +162,11 @@ function getRouteGuards( checks.canActivateChecks.push(new CanActivate(futurePath)); // If we have a component, we need to go through an outlet. if (future.component) { - getChildRouteGuards( - futureNode, - null, - context ? context.children : null, - futurePath, - environmentInjector, - checks, - ); + getChildRouteGuards(futureNode, null, context ? context.children : null, futurePath, checks); // if we have a componentless route, we recurse but keep the same outlet map. } else { - getChildRouteGuards( - futureNode, - null, - parentContexts, - futurePath, - environmentInjector, - checks, - ); + getChildRouteGuards(futureNode, null, parentContexts, futurePath, checks); } } @@ -217,11 +177,9 @@ function shouldRunGuardsAndResolvers( curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, mode: RunGuardsAndResolvers | undefined, - environmentInjector: EnvironmentInjector, ): boolean { if (typeof mode === 'function') { - const injector = getClosestRouteInjector(future) ?? environmentInjector; - return runInInjectionContext(injector, () => mode(curr, future)); + return runInInjectionContext(future._environmentInjector, () => mode(curr, future)); } switch (mode) { case 'pathParamsChange': diff --git a/packages/router/test/create_router_state.spec.ts b/packages/router/test/create_router_state.spec.ts index b4ef2940e3bf..7f8ffc37f03c 100644 --- a/packages/router/test/create_router_state.spec.ts +++ b/packages/router/test/create_router_state.spec.ts @@ -32,7 +32,7 @@ describe('create router state', () => { reuseStrategy = new DefaultRouteReuseStrategy(); }); - const emptyState = () => createEmptyState(RootComponent); + const emptyState = () => createEmptyState(RootComponent, TestBed.inject(EnvironmentInjector)); it('should create new state', async () => { const state = createRouterState( diff --git a/packages/router/test/helpers.ts b/packages/router/test/helpers.ts index 989546a2a562..1a3c2a600e16 100644 --- a/packages/router/test/helpers.ts +++ b/packages/router/test/helpers.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Type} from '@angular/core'; +import {EnvironmentInjector, Type} from '@angular/core'; import {Data, ResolveData, Route} from '../src/models'; import {ActivatedRouteSnapshot} from '../src/router_state'; -import {Params} from '../src/shared'; +import {Params, PRIMARY_OUTLET} from '../src/shared'; import {UrlSegment, UrlTree} from '../src/url_tree'; +import {TestBed} from '@angular/core/testing'; export class Logger { logs: string[] = []; @@ -39,13 +40,14 @@ export function createActivatedRouteSnapshot(args: ARSArgs): ActivatedRouteSnaps return new (ActivatedRouteSnapshot as any)( args.url || [], args.params || {}, - args.queryParams || null, + args.queryParams || {}, args.fragment || null, - args.data || null, - args.outlet || null, - args.component, + args.data || {}, + args.outlet || PRIMARY_OUTLET, + args.component as any, args.routeConfig || {}, args.resolve || {}, + TestBed.inject(EnvironmentInjector), ); } diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 612b72b39d94..f85863757c75 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -179,7 +179,7 @@ describe('Router', () => { beforeEach(() => { const _logger: Logger = TestBed.inject(Logger); - empty = createEmptyStateSnapshot(null); + empty = createEmptyStateSnapshot(null, TestBed.inject(EnvironmentInjector)); logger = _logger; events = []; }); @@ -207,13 +207,12 @@ describe('Router', () => { futureState, empty, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), - TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; of(testTransition) .pipe( - checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + checkGuardsOperator((evt) => { events.push(evt); }), ) @@ -268,13 +267,12 @@ describe('Router', () => { futureState, empty, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), - TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; of(testTransition) .pipe( - checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + checkGuardsOperator((evt) => { events.push(evt); }), ) @@ -327,13 +325,12 @@ describe('Router', () => { futureState, currentState, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), - TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; of(testTransition) .pipe( - checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + checkGuardsOperator((evt) => { events.push(evt); }), ) @@ -404,13 +401,12 @@ describe('Router', () => { futureState, currentState, new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)), - TestBed.inject(EnvironmentInjector), ), } as NavigationTransition; of(testTransition) .pipe( - checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + checkGuardsOperator((evt) => { events.push(evt); }), ) @@ -898,9 +894,9 @@ function checkResolveData( // Since we only test the guards and their resolve data function, we don't need to provide // a full navigation transition object with all properties set. of({ - guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector), + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)), } as NavigationTransition) - .pipe(resolveDataOperator('emptyOnly', injector)) + .pipe(resolveDataOperator('emptyOnly')) .subscribe(check, (e) => { throw e; }); @@ -915,9 +911,9 @@ function checkGuards( // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. of({ - guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector), + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)), } as NavigationTransition) - .pipe(checkGuardsOperator(injector)) + .pipe(checkGuardsOperator()) .subscribe({ next(t) { if (t.guardsResult === null) throw new Error('Guard result expected');