Skip to content

Commit c25d749

Browse files
atscottkirjs
authored andcommitted
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 / #31843 (comment) resolves #53944
1 parent 1a094dd commit c25d749

File tree

4 files changed

+103
-11
lines changed

4 files changed

+103
-11
lines changed

packages/router/src/navigation_transition.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,12 @@ export class NavigationTransitions {
623623

624624
this.currentTransition = overallTransitionState = {
625625
...t,
626-
guards: getAllRouteGuards(t.targetSnapshot!, t.currentSnapshot, this.rootContexts),
626+
guards: getAllRouteGuards(
627+
t.targetSnapshot!,
628+
t.currentSnapshot,
629+
this.rootContexts,
630+
this.environmentInjector,
631+
),
627632
};
628633
return overallTransitionState;
629634
}),

packages/router/src/utils/preactivation.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Injector, ProviderToken, ɵisInjectable as isInjectable} from '@angular/core';
10-
9+
import {
10+
Injector,
11+
ProviderToken,
12+
ɵisInjectable as isInjectable,
13+
EnvironmentInjector,
14+
runInInjectionContext,
15+
} from '@angular/core';
1116
import {RunGuardsAndResolvers} from '../models';
17+
import {getClosestRouteInjector} from './config';
18+
1219
import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context';
1320
import {
1421
ActivatedRouteSnapshot,
@@ -42,11 +49,18 @@ export function getAllRouteGuards(
4249
future: RouterStateSnapshot,
4350
curr: RouterStateSnapshot,
4451
parentContexts: ChildrenOutletContexts,
52+
environmentInjector: EnvironmentInjector,
4553
): Checks {
4654
const futureRoot = future._root;
4755
const currRoot = curr ? curr._root : null;
4856

49-
return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]);
57+
return getChildRouteGuards(
58+
futureRoot,
59+
currRoot,
60+
parentContexts,
61+
[futureRoot.value],
62+
environmentInjector,
63+
);
5064
}
5165

5266
export function getCanActivateChild(
@@ -80,6 +94,7 @@ function getChildRouteGuards(
8094
currNode: TreeNode<ActivatedRouteSnapshot> | null,
8195
contexts: ChildrenOutletContexts | null,
8296
futurePath: ActivatedRouteSnapshot[],
97+
environmentInjector: EnvironmentInjector,
8398
checks: Checks = {
8499
canDeactivateChecks: [],
85100
canActivateChecks: [],
@@ -89,7 +104,14 @@ function getChildRouteGuards(
89104

90105
// Process the children of the future route
91106
futureNode.children.forEach((c) => {
92-
getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks);
107+
getRouteGuards(
108+
c,
109+
prevChildren[c.value.outlet],
110+
contexts,
111+
futurePath.concat([c.value]),
112+
environmentInjector,
113+
checks,
114+
);
93115
delete prevChildren[c.value.outlet];
94116
});
95117

@@ -106,6 +128,7 @@ function getRouteGuards(
106128
currNode: TreeNode<ActivatedRouteSnapshot>,
107129
parentContexts: ChildrenOutletContexts | null,
108130
futurePath: ActivatedRouteSnapshot[],
131+
environmentInjector: EnvironmentInjector,
109132
checks: Checks = {
110133
canDeactivateChecks: [],
111134
canActivateChecks: [],
@@ -121,6 +144,7 @@ function getRouteGuards(
121144
curr,
122145
future,
123146
future.routeConfig!.runGuardsAndResolvers,
147+
environmentInjector,
124148
);
125149
if (shouldRun) {
126150
checks.canActivateChecks.push(new CanActivate(futurePath));
@@ -137,12 +161,20 @@ function getRouteGuards(
137161
currNode,
138162
context ? context.children : null,
139163
futurePath,
164+
environmentInjector,
140165
checks,
141166
);
142167

143168
// if we have a componentless route, we recurse but keep the same outlet map.
144169
} else {
145-
getChildRouteGuards(futureNode, currNode, parentContexts, futurePath, checks);
170+
getChildRouteGuards(
171+
futureNode,
172+
currNode,
173+
parentContexts,
174+
futurePath,
175+
environmentInjector,
176+
checks,
177+
);
146178
}
147179

148180
if (shouldRun && context && context.outlet && context.outlet.isActivated) {
@@ -156,11 +188,25 @@ function getRouteGuards(
156188
checks.canActivateChecks.push(new CanActivate(futurePath));
157189
// If we have a component, we need to go through an outlet.
158190
if (future.component) {
159-
getChildRouteGuards(futureNode, null, context ? context.children : null, futurePath, checks);
191+
getChildRouteGuards(
192+
futureNode,
193+
null,
194+
context ? context.children : null,
195+
futurePath,
196+
environmentInjector,
197+
checks,
198+
);
160199

161200
// if we have a componentless route, we recurse but keep the same outlet map.
162201
} else {
163-
getChildRouteGuards(futureNode, null, parentContexts, futurePath, checks);
202+
getChildRouteGuards(
203+
futureNode,
204+
null,
205+
parentContexts,
206+
futurePath,
207+
environmentInjector,
208+
checks,
209+
);
164210
}
165211
}
166212

@@ -171,9 +217,11 @@ function shouldRunGuardsAndResolvers(
171217
curr: ActivatedRouteSnapshot,
172218
future: ActivatedRouteSnapshot,
173219
mode: RunGuardsAndResolvers | undefined,
220+
environmentInjector: EnvironmentInjector,
174221
): boolean {
175222
if (typeof mode === 'function') {
176-
return mode(curr, future);
223+
const injector = getClosestRouteInjector(future) ?? environmentInjector;
224+
return runInInjectionContext(injector, () => mode(curr, future));
177225
}
178226
switch (mode) {
179227
case 'pathParamsChange':

packages/router/test/integration/guards.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,5 +2395,40 @@ export function guardsIntegrationSuite() {
23952395
await router.navigateByUrl('');
23962396
expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']);
23972397
});
2398+
2399+
it('should run in injection context', async () => {
2400+
@Injectable({providedIn: 'root'})
2401+
class MyService {
2402+
canRun = false;
2403+
}
2404+
2405+
let resolveCount = 0;
2406+
const routes = [
2407+
{
2408+
path: 'a',
2409+
children: [],
2410+
resolve: {
2411+
x: () => ++resolveCount,
2412+
},
2413+
runGuardsAndResolvers: () => inject(MyService).canRun,
2414+
},
2415+
];
2416+
const router = TestBed.inject(Router);
2417+
router.resetConfig(routes);
2418+
const service = TestBed.inject(MyService);
2419+
2420+
await router.navigateByUrl('/a');
2421+
expect(router.url).toEqual('/a');
2422+
// Always run on activation
2423+
expect(resolveCount).toBe(1);
2424+
2425+
service.canRun = false;
2426+
await router.navigateByUrl('/a?q=1');
2427+
expect(resolveCount).toBe(1);
2428+
2429+
service.canRun = true;
2430+
await router.navigateByUrl('/a?q=2');
2431+
expect(resolveCount).toBe(2);
2432+
});
23982433
});
23992434
}

packages/router/test/router.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ describe('Router', () => {
207207
futureState,
208208
empty,
209209
new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)),
210+
TestBed.inject(EnvironmentInjector),
210211
),
211212
} as NavigationTransition;
212213

@@ -267,6 +268,7 @@ describe('Router', () => {
267268
futureState,
268269
empty,
269270
new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)),
271+
TestBed.inject(EnvironmentInjector),
270272
),
271273
} as NavigationTransition;
272274

@@ -325,6 +327,7 @@ describe('Router', () => {
325327
futureState,
326328
currentState,
327329
new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)),
330+
TestBed.inject(EnvironmentInjector),
328331
),
329332
} as NavigationTransition;
330333

@@ -401,6 +404,7 @@ describe('Router', () => {
401404
futureState,
402405
currentState,
403406
new ChildrenOutletContexts(TestBed.inject(EnvironmentInjector)),
407+
TestBed.inject(EnvironmentInjector),
404408
),
405409
} as NavigationTransition;
406410

@@ -894,7 +898,7 @@ function checkResolveData(
894898
// Since we only test the guards and their resolve data function, we don't need to provide
895899
// a full navigation transition object with all properties set.
896900
of({
897-
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)),
901+
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector),
898902
} as NavigationTransition)
899903
.pipe(resolveDataOperator('emptyOnly', injector))
900904
.subscribe(check, (e) => {
@@ -911,7 +915,7 @@ function checkGuards(
911915
// Since we only test the guards, we don't need to provide a full navigation
912916
// transition object with all properties set.
913917
of({
914-
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector)),
918+
guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts(injector), injector),
915919
} as NavigationTransition)
916920
.pipe(checkGuardsOperator(injector))
917921
.subscribe({

0 commit comments

Comments
 (0)