Skip to content

Commit b51bab5

Browse files
committed
feat(router): Add partial ActivatedRouteSnapshot information to canMatch params
This commit adds partial `ActivatedRouteSnapshot` information as the third parameter of the `canMatch` guard. resolves #49309
1 parent 57e80a5 commit b51bab5

File tree

7 files changed

+124
-12
lines changed

7 files changed

+124
-12
lines changed

goldens/public-api/router/index.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ export type CanLoadFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<Gua
151151
// @public
152152
export interface CanMatch {
153153
// (undocumented)
154-
canMatch(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
154+
canMatch(route: Route, segments: UrlSegment[], currentSnapshot?: PartialMatchRouteSnapshot): MaybeAsync<GuardResult>;
155155
}
156156

157157
// @public
158-
export type CanMatchFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;
158+
export type CanMatchFn = (route: Route, segments: UrlSegment[], currentSnapshot?: PartialMatchRouteSnapshot) => MaybeAsync<GuardResult>;
159159

160160
// @public
161161
export class ChildActivationEnd {

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@
520520
"createOrReusePlatformInjector",
521521
"createPlatformInjector",
522522
"createPositionApplyingDoubleDots",
523+
"createPreMatchRouteSnapshot",
523524
"createProvidersConfig",
524525
"createResultByTNodeType",
525526
"createResultForNode",

packages/router/src/models.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,11 @@ export type CanDeactivateFn<T> = (
11511151
* @see [CanMatch](guide/routing/route-guards#canmatch)
11521152
*/
11531153
export interface CanMatch {
1154-
canMatch(route: Route, segments: UrlSegment[]): MaybeAsync<GuardResult>;
1154+
canMatch(
1155+
route: Route,
1156+
segments: UrlSegment[],
1157+
currentSnapshot?: PartialMatchRouteSnapshot,
1158+
): MaybeAsync<GuardResult>;
11551159
}
11561160

11571161
/**
@@ -1168,12 +1172,19 @@ export interface CanMatch {
11681172
*
11691173
* @param route The route configuration.
11701174
* @param segments The URL segments that have not been consumed by previous parent route evaluations.
1175+
* @param currentSnapshot The current route snapshot up to this point in the matching process. While this parameter is optional,
1176+
* it will always be defined when called by the Router. It is only optional for backwards compatibility with functions defined prior
1177+
* to the introduction of this parameter.
11711178
*
11721179
* @publicApi
11731180
* @see {@link Route}
11741181
* @see [CanMatch](guide/routing/route-guards#canmatch)
11751182
*/
1176-
export type CanMatchFn = (route: Route, segments: UrlSegment[]) => MaybeAsync<GuardResult>;
1183+
export type CanMatchFn = (
1184+
route: Route,
1185+
segments: UrlSegment[],
1186+
currentSnapshot?: PartialMatchRouteSnapshot,
1187+
) => MaybeAsync<GuardResult>;
11771188

11781189
/**
11791190
* A subset of the `ActivatedRouteSnapshot` interface that includes only the known data

packages/router/src/operators/check_guards.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
CanLoadFn,
2929
CanMatchFn,
3030
Route,
31+
PartialMatchRouteSnapshot,
3132
} from '../models';
3233
import {redirectingNavigationError} from '../navigation_canceling_error';
3334
import type {NavigationTransition} from '../navigation_transition';
@@ -266,6 +267,7 @@ export function runCanMatchGuards(
266267
route: Route,
267268
segments: UrlSegment[],
268269
urlSerializer: UrlSerializer,
270+
currentSnapshot: PartialMatchRouteSnapshot,
269271
abortSignal: AbortSignal,
270272
): Observable<GuardResult> {
271273
const canMatch = route.canMatch;
@@ -274,8 +276,10 @@ export function runCanMatchGuards(
274276
const canMatchObservables = canMatch.map((injectionToken) => {
275277
const guard = getTokenOrFunctionIdentity(injectionToken as ProviderToken<any>, injector);
276278
const guardVal = isCanMatch(guard)
277-
? guard.canMatch(route, segments)
278-
: runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments));
279+
? guard.canMatch(route, segments, currentSnapshot)
280+
: runInInjectionContext(injector, () =>
281+
(guard as CanMatchFn)(route, segments, currentSnapshot),
282+
);
279283
return wrapIntoObservable(guardVal).pipe(takeUntilAbort(abortSignal));
280284
});
281285

packages/router/src/recognize.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import {Params, PRIMARY_OUTLET} from './shared';
2424
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
2525
import {getOutlet, sortByMatchingOutlets} from './utils/config';
2626
import {
27+
createPreMatchRouteSnapshot,
2728
emptyPathMatch,
2829
match,
30+
MatchResult,
2931
matchWithChecks,
3032
noLeftoversInUrl,
3133
split,
@@ -356,7 +358,7 @@ export class Recognizer {
356358
consumedSegments,
357359
route.redirectTo!,
358360
positionalParamSegments,
359-
currentSnapshot,
361+
createPreMatchRouteSnapshot(currentSnapshot),
360362
injector,
361363
);
362364

@@ -408,8 +410,19 @@ export class Recognizer {
408410
if (this.abortSignal.aborted) {
409411
throw new Error(this.abortSignal.reason);
410412
}
413+
414+
const createSnapshot = (result: MatchResult) =>
415+
this.createSnapshot(injector, route, result.consumedSegments, result.parameters, parentRoute);
411416
const result = await firstValueFrom(
412-
matchWithChecks(rawSegment, route, segments, injector, this.urlSerializer, this.abortSignal),
417+
matchWithChecks(
418+
rawSegment,
419+
route,
420+
segments,
421+
injector,
422+
this.urlSerializer,
423+
createSnapshot,
424+
this.abortSignal,
425+
),
413426
);
414427
if (route.path === '**') {
415428
// Prior versions of the route matching algorithm would stop matching at the wildcard route.

packages/router/src/utils/config_matching.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {EnvironmentInjector} from '@angular/core';
1010
import {Observable, of} from 'rxjs';
1111
import {map} from 'rxjs/operators';
1212

13-
import {Route} from '../models';
13+
import {PartialMatchRouteSnapshot, Route} from '../models';
1414
import {runCanMatchGuards} from '../operators/check_guards';
15+
import {ActivatedRouteSnapshot} from '../router_state';
1516
import {defaultUrlMatcher, PRIMARY_OUTLET} from '../shared';
1617
import {UrlSegment, UrlSegmentGroup, UrlSerializer} from '../url_tree';
1718

@@ -33,25 +34,49 @@ const noMatch: MatchResult = {
3334
positionalParamSegments: {},
3435
};
3536

37+
export function createPreMatchRouteSnapshot(
38+
snapshot: ActivatedRouteSnapshot,
39+
): PartialMatchRouteSnapshot {
40+
return {
41+
routeConfig: snapshot.routeConfig,
42+
url: snapshot.url,
43+
params: snapshot.params,
44+
queryParams: snapshot.queryParams,
45+
fragment: snapshot.fragment,
46+
data: snapshot.data,
47+
outlet: snapshot.outlet,
48+
title: snapshot.title,
49+
paramMap: snapshot.paramMap,
50+
queryParamMap: snapshot.queryParamMap,
51+
};
52+
}
53+
3654
export function matchWithChecks(
3755
segmentGroup: UrlSegmentGroup,
3856
route: Route,
3957
segments: UrlSegment[],
4058
injector: EnvironmentInjector,
4159
urlSerializer: UrlSerializer,
60+
createSnapshot: (result: MatchResult) => ActivatedRouteSnapshot,
4261
abortSignal: AbortSignal,
4362
): Observable<MatchResult> {
4463
const result = match(segmentGroup, route, segments);
4564
if (!result.matched) {
4665
return of(result);
4766
}
4867

68+
const currentSnapshot = createPreMatchRouteSnapshot(createSnapshot(result));
4969
// Only create the Route's `EnvironmentInjector` if it matches the attempted
5070
// navigation
5171
injector = getOrCreateRouteInjectorIfNeeded(route, injector);
52-
return runCanMatchGuards(injector, route, segments, urlSerializer, abortSignal).pipe(
53-
map((v) => (v === true ? result : {...noMatch})),
54-
);
72+
return runCanMatchGuards(
73+
injector,
74+
route,
75+
segments,
76+
urlSerializer,
77+
currentSnapshot,
78+
abortSignal,
79+
).pipe(map((v) => (v === true ? result : {...noMatch})));
5580
}
5681

5782
export function match(

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
Router,
2727
NavigationStart,
2828
RoutesRecognized,
29+
Route,
30+
UrlSegment,
2931
GuardsCheckStart,
3032
ChildActivationStart,
3133
ActivationStart,
@@ -50,6 +52,7 @@ import {
5052
CanActivateFn,
5153
CanActivateChildFn,
5254
CanDeactivateFn,
55+
PartialMatchRouteSnapshot,
5356
} from '../../src';
5457
import {wrapIntoObservable} from '../../src/utils/collection';
5558
import {RouterTestingHarness} from '../../testing';
@@ -2226,6 +2229,61 @@ export function guardsIntegrationSuite() {
22262229
// same time
22272230
expect(delayedGuardSpy.calls.count()).toEqual(1);
22282231
});
2232+
2233+
it('receives PreMatchRouteSnapshot as third argument', async () => {
2234+
const router = TestBed.inject(Router);
2235+
const fixture = await createRoot(router, RootCmp);
2236+
let capturedSnapshot: PartialMatchRouteSnapshot | undefined;
2237+
2238+
router.resetConfig([
2239+
{
2240+
path: 'a/:id',
2241+
canMatch: [
2242+
(route: Route, segments: UrlSegment[], snapshot: PartialMatchRouteSnapshot) => {
2243+
capturedSnapshot = snapshot;
2244+
return true;
2245+
},
2246+
],
2247+
component: SimpleCmp,
2248+
},
2249+
]);
2250+
2251+
await router.navigateByUrl('/a/1?q=2#f');
2252+
expect(capturedSnapshot).toBeDefined();
2253+
expect(capturedSnapshot!.params['id']).toBe('1');
2254+
expect(capturedSnapshot!.queryParams['q']).toBe('2');
2255+
expect(capturedSnapshot!.fragment).toBe('f');
2256+
});
2257+
2258+
it('can redirect based on snapshot params', async () => {
2259+
const router = TestBed.inject(Router);
2260+
const fixture = await createRoot(router, RootCmp);
2261+
2262+
router.resetConfig([
2263+
{
2264+
path: 'a/:id',
2265+
canMatch: [
2266+
(route: Route, segments: UrlSegment[], snapshot: PartialMatchRouteSnapshot) => {
2267+
const router = inject(Router);
2268+
if (snapshot.params['id'] === '1') {
2269+
return router.parseUrl('/b');
2270+
}
2271+
return true;
2272+
},
2273+
],
2274+
component: SimpleCmp,
2275+
},
2276+
{path: 'b', component: BlankCmp},
2277+
]);
2278+
2279+
await router.navigateByUrl('/a/1');
2280+
await advance(fixture);
2281+
expect(router.url).toEqual('/b');
2282+
2283+
await router.navigateByUrl('/a/2');
2284+
await advance(fixture);
2285+
expect(router.url).toEqual('/a/2');
2286+
});
22292287
});
22302288

22312289
it('should allow guards as functions', async () => {

0 commit comments

Comments
 (0)