fix(router): Ensure Route injector is created before running CanMatch guards (#46394)

Once a `Route` matches via the `match` or `path` property, we need to
immediately create the injector for the route (if it has providers)
before running the `CanMatch` guards. This is necessary because the
`CanMatch` guards might be provided in the `Route` providers.

Fixes #46386

PR Close #46394
This commit is contained in:
Andrew Scott
2022-06-16 09:47:11 -07:00
committed by Andrew Kushnir
parent bb7c80477b
commit ce20ed067f
4 changed files with 50 additions and 13 deletions
+3 -4
View File
@@ -15,7 +15,7 @@ import {runCanLoadGuards} from './operators/check_guards';
import {RouterConfigLoader} from './router_config_loader';
import {navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {forEach, wrapIntoObservable} from './utils/collection';
import {forEach} from './utils/collection';
import {getOrCreateRouteInjectorIfNeeded, getOutlet, sortByMatchingOutlets} from './utils/config';
import {isImmediateMatch, match, matchWithChecks, noLeftoversInUrl, split} from './utils/config_matching';
@@ -289,9 +289,8 @@ class ApplyRedirects {
switchMap(({matched, consumedSegments, remainingSegments}) => {
if (!matched) return noMatch(rawSegmentGroup);
// Only create the Route's `EnvironmentInjector` if it matches the attempted
// navigation
injector = getOrCreateRouteInjectorIfNeeded(route, injector);
// If the route has an injector created from providers, we should start using that.
injector = route._injector ?? injector;
const childConfig$ = this.getChildConfig(injector, route, segments);
return childConfig$.pipe(mergeMap((routerConfig: LoadedRouterConfig) => {
+3 -4
View File
@@ -173,7 +173,7 @@ export class Recognizer {
let matchResult: Observable<{
snapshot: ActivatedRouteSnapshot,
consumedSegments: UrlSegment[],
remainingSegments: UrlSegment[]
remainingSegments: UrlSegment[],
}|null>;
if (route.path === '**') {
@@ -219,9 +219,8 @@ export class Recognizer {
return of(null);
}
const {snapshot, consumedSegments, remainingSegments} = result;
// Only create the Route's `EnvironmentInjector` if it matches the attempted
// navigation
injector = getOrCreateRouteInjectorIfNeeded(route, injector);
// If the route has an injector created from providers, we should start using that.
injector = route._injector ?? injector;
const childInjector = route._loadedInjector ?? injector;
const childConfig: Route[] = getChildConfig(route);
+8 -4
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {EnvironmentInjector, Injector} from '@angular/core';
import {Observable, of} from 'rxjs';
import {map} from 'rxjs/operators';
@@ -16,7 +16,7 @@ import {defaultUrlMatcher, PRIMARY_OUTLET} from '../shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer} from '../url_tree';
import {forEach} from './collection';
import {getOutlet} from './config';
import {getOrCreateRouteInjectorIfNeeded, getOutlet} from './config';
export interface MatchResult {
matched: boolean;
@@ -35,12 +35,16 @@ const noMatch: MatchResult = {
};
export function matchWithChecks(
segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[], injector: Injector,
urlSerializer: UrlSerializer): Observable<MatchResult> {
segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[],
injector: EnvironmentInjector, urlSerializer: UrlSerializer): Observable<MatchResult> {
const result = match(segmentGroup, route, segments);
if (!result.matched) {
return of(result);
}
// Only create the Route's `EnvironmentInjector` if it matches the attempted
// navigation
injector = getOrCreateRouteInjectorIfNeeded(route, injector);
return runCanMatchGuards(injector, route, segments, urlSerializer)
.pipe(
map((v) => v === true ? result : {...noMatch}),
+36 -1
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {EnvironmentInjector, NgModuleRef} from '@angular/core';
import {EnvironmentInjector, Injectable, NgModuleRef} from '@angular/core';
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Observable, of} from 'rxjs';
import {delay, tap} from 'rxjs/operators';
@@ -229,6 +229,41 @@ describe('applyRedirects', () => {
});
});
it('should support CanMatch providers on the route', () => {
@Injectable({providedIn: 'root'})
class CanMatchGuard {
canMatch() {
return true;
}
}
const routes = [
{
path: 'a',
component: ComponentA,
canMatch: [CanMatchGuard],
providers: [CanMatchGuard],
},
{
path: 'a',
component: ComponentA,
providers: [],
}
];
applyRedirects(testModule.injector, null!, serializer, tree('a'), routes).subscribe({
next: () => {
// The 'a' segment matched, so we needed to create the injector for the `Route`
expect(getProvidersInjector(routes[0])).toBeDefined();
// The second `Route` did not match because the first did so we should not create an
// injector for it
expect(getProvidersInjector(routes[1])).not.toBeDefined();
},
error: () => {
throw 'Should not be reached';
}
});
})
describe('lazy loading', () => {
it('should load config on demand', () => {
const loadedConfig = {