/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/router/src/utils/config_matching.ts
245 строк
7 KB
Andrew Scott
fix(router): pass outlet context to split to fix empty path named outlets
01 апр 2026, 12:48
01 апр 2026, 12:48
daa9b2a
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {EnvironmentInjector} from '@angular/core'; import {Observable, of} from 'rxjs'; import {map} from 'rxjs/operators'; import {PartialMatchRouteSnapshot, Route} from '../models'; import {runCanMatchGuards} from '../operators/check_guards'; import {ActivatedRouteSnapshot} from '../router_state'; import {defaultUrlMatcher, PRIMARY_OUTLET} from '../shared'; import {UrlSegment, UrlSegmentGroup, UrlSerializer} from '../url_tree'; import {getOrCreateRouteInjectorIfNeeded, getOutlet} from './config'; export interface MatchResult { matched: boolean; consumedSegments: UrlSegment[]; remainingSegments: UrlSegment[]; parameters: {[k: string]: string}; positionalParamSegments: {[k: string]: UrlSegment}; } const noMatch: MatchResult = { matched: false, consumedSegments: [], remainingSegments: [], parameters: {}, positionalParamSegments: {}, }; export function createPreMatchRouteSnapshot( snapshot: ActivatedRouteSnapshot, ): PartialMatchRouteSnapshot { return { routeConfig: snapshot.routeConfig, url: snapshot.url, params: snapshot.params, queryParams: snapshot.queryParams, fragment: snapshot.fragment, data: snapshot.data, outlet: snapshot.outlet, title: snapshot.title, paramMap: snapshot.paramMap, queryParamMap: snapshot.queryParamMap, }; } export function matchWithChecks( segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[], injector: EnvironmentInjector, urlSerializer: UrlSerializer, createSnapshot: (result: MatchResult) => ActivatedRouteSnapshot, abortSignal: AbortSignal, ): Observable<MatchResult> { const result = match(segmentGroup, route, segments); if (!result.matched) { return of(result); } const currentSnapshot = createPreMatchRouteSnapshot(createSnapshot(result)); // Only create the Route's `EnvironmentInjector` if it matches the attempted // navigation injector = getOrCreateRouteInjectorIfNeeded(route, injector); return runCanMatchGuards( injector, route, segments, urlSerializer, currentSnapshot, abortSignal, ).pipe(map((v) => (v === true ? result : {...noMatch}))); } export function match( segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[], ): MatchResult { if (route.path === '') { if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) { return {...noMatch}; } return { matched: true, consumedSegments: [], remainingSegments: segments, parameters: {}, positionalParamSegments: {}, }; } const matcher = route.matcher || defaultUrlMatcher; const res = matcher(segments, segmentGroup, route); if (!res) return {...noMatch}; const posParams: {[n: string]: string} = {}; Object.entries(res.posParams ?? {}).forEach(([k, v]) => { posParams[k] = v.path; }); const parameters = res.consumed.length > 0 ? {...posParams, ...res.consumed[res.consumed.length - 1].parameters} : posParams; return { matched: true, consumedSegments: res.consumed, remainingSegments: segments.slice(res.consumed.length), // TODO(atscott): investigate combining parameters and positionalParamSegments parameters, positionalParamSegments: res.posParams ?? {}, }; } export function split( segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], config: Route[], outlet?: string, ): { segmentGroup: UrlSegmentGroup; slicedSegments: UrlSegment[]; } { if ( slicedSegments.length > 0 && containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config, outlet) ) { const s = new UrlSegmentGroup( consumedSegments, createChildrenForEmptyPaths( config, new UrlSegmentGroup(slicedSegments, segmentGroup.children), ), ); return {segmentGroup: s, slicedSegments: []}; } if ( slicedSegments.length === 0 && containsEmptyPathMatches(segmentGroup, slicedSegments, config) ) { const s = new UrlSegmentGroup( segmentGroup.segments, addEmptyPathsToChildrenIfNeeded(segmentGroup, slicedSegments, config, segmentGroup.children), ); return {segmentGroup: s, slicedSegments}; } const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children); return {segmentGroup: s, slicedSegments}; } function addEmptyPathsToChildrenIfNeeded( segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], children: {[name: string]: UrlSegmentGroup}, ): {[name: string]: UrlSegmentGroup} { const res: {[name: string]: UrlSegmentGroup} = {}; for (const r of routes) { if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) { const s = new UrlSegmentGroup([], {}); res[getOutlet(r)] = s; } } return {...children, ...res}; } function createChildrenForEmptyPaths( routes: Route[], primarySegment: UrlSegmentGroup, ): {[name: string]: UrlSegmentGroup} { const res: {[name: string]: UrlSegmentGroup} = {}; res[PRIMARY_OUTLET] = primarySegment; for (const r of routes) { if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) { const s = new UrlSegmentGroup([], {}); res[getOutlet(r)] = s; } } return res; } function containsEmptyPathMatchesWithNamedOutlets( segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], outlet?: string, ): boolean { return routes.some((r) => { // 1. Can this route match as an empty path? const matchesEmpty = emptyPathMatch(segmentGroup, slicedSegments, r); if (!matchesEmpty) return false; // 2. Is this a named outlet? (We only pull in empty paths if they are named outlets). const isNamedOutlet = getOutlet(r) !== PRIMARY_OUTLET; if (!isNamedOutlet) return false; // 3. Are we already processing this outlet? If so, we ignore it as a pull-in // candidate. For example, if we are evaluating the 'secondary' outlet, we shouldn't // "pull in" an empty 'secondary' group. We should let standard // segment matching handle it (which looks at the actual characters in the URL). const isSelfEvaluating = outlet !== undefined && getOutlet(r) === outlet; return !isSelfEvaluating; }); } function containsEmptyPathMatches( segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], ): boolean { return routes.some((r) => emptyPathMatch(segmentGroup, slicedSegments, r)); } export function emptyPathMatch( segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route, ): boolean { if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') { return false; } return r.path === ''; } export function noLeftoversInUrl( segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string, ): boolean { return segments.length === 0 && !segmentGroup.children[outlet]; }