fix(router): Fix _lastPathIndex in deeply nested empty paths (#22394)
PR Close #22394
This commit is contained in:
parent
1e28495c89
commit
968f153491
@ -20,20 +20,24 @@ class NoMatch {}
|
|||||||
|
|
||||||
export function recognize(
|
export function recognize(
|
||||||
rootComponentType: Type<any>| null, config: Routes, urlTree: UrlTree, url: string,
|
rootComponentType: Type<any>| null, config: Routes, urlTree: UrlTree, url: string,
|
||||||
paramsInheritanceStrategy: ParamsInheritanceStrategy =
|
paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly',
|
||||||
'emptyOnly'): Observable<RouterStateSnapshot> {
|
relativeLinkResolution: 'legacy' | 'corrected' = 'legacy'): Observable<RouterStateSnapshot> {
|
||||||
return new Recognizer(rootComponentType, config, urlTree, url, paramsInheritanceStrategy)
|
return new Recognizer(
|
||||||
|
rootComponentType, config, urlTree, url, paramsInheritanceStrategy,
|
||||||
|
relativeLinkResolution)
|
||||||
.recognize();
|
.recognize();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Recognizer {
|
class Recognizer {
|
||||||
constructor(
|
constructor(
|
||||||
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
|
private rootComponentType: Type<any>|null, private config: Routes, private urlTree: UrlTree,
|
||||||
private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy) {}
|
private url: string, private paramsInheritanceStrategy: ParamsInheritanceStrategy,
|
||||||
|
private relativeLinkResolution: 'legacy'|'corrected') {}
|
||||||
|
|
||||||
recognize(): Observable<RouterStateSnapshot> {
|
recognize(): Observable<RouterStateSnapshot> {
|
||||||
try {
|
try {
|
||||||
const rootSegmentGroup = split(this.urlTree.root, [], [], this.config).segmentGroup;
|
const rootSegmentGroup =
|
||||||
|
split(this.urlTree.root, [], [], this.config, this.relativeLinkResolution).segmentGroup;
|
||||||
|
|
||||||
const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);
|
const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);
|
||||||
|
|
||||||
@ -134,8 +138,8 @@ class Recognizer {
|
|||||||
|
|
||||||
const childConfig: Route[] = getChildConfig(route);
|
const childConfig: Route[] = getChildConfig(route);
|
||||||
|
|
||||||
const {segmentGroup, slicedSegments} =
|
const {segmentGroup, slicedSegments} = split(
|
||||||
split(rawSegment, consumedSegments, rawSlicedSegments, childConfig);
|
rawSegment, consumedSegments, rawSlicedSegments, childConfig, this.relativeLinkResolution);
|
||||||
|
|
||||||
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
||||||
const children = this.processChildren(childConfig, segmentGroup);
|
const children = this.processChildren(childConfig, segmentGroup);
|
||||||
@ -232,7 +236,7 @@ function getPathIndexShift(segmentGroup: UrlSegmentGroup): number {
|
|||||||
|
|
||||||
function split(
|
function split(
|
||||||
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
||||||
config: Route[]) {
|
config: Route[], relativeLinkResolution: 'legacy' | 'corrected') {
|
||||||
if (slicedSegments.length > 0 &&
|
if (slicedSegments.length > 0 &&
|
||||||
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
||||||
const s = new UrlSegmentGroup(
|
const s = new UrlSegmentGroup(
|
||||||
@ -248,7 +252,8 @@ function split(
|
|||||||
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
|
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
|
||||||
const s = new UrlSegmentGroup(
|
const s = new UrlSegmentGroup(
|
||||||
segmentGroup.segments, addEmptyPathsToChildrenIfNeeded(
|
segmentGroup.segments, addEmptyPathsToChildrenIfNeeded(
|
||||||
segmentGroup, slicedSegments, config, segmentGroup.children));
|
segmentGroup, consumedSegments, slicedSegments, config,
|
||||||
|
segmentGroup.children, relativeLinkResolution));
|
||||||
s._sourceSegment = segmentGroup;
|
s._sourceSegment = segmentGroup;
|
||||||
s._segmentIndexShift = consumedSegments.length;
|
s._segmentIndexShift = consumedSegments.length;
|
||||||
return {segmentGroup: s, slicedSegments};
|
return {segmentGroup: s, slicedSegments};
|
||||||
@ -261,14 +266,19 @@ function split(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addEmptyPathsToChildrenIfNeeded(
|
function addEmptyPathsToChildrenIfNeeded(
|
||||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
|
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
||||||
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
|
routes: Route[], children: {[name: string]: UrlSegmentGroup},
|
||||||
|
relativeLinkResolution: 'legacy' | 'corrected'): {[name: string]: UrlSegmentGroup} {
|
||||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||||
for (const r of routes) {
|
for (const r of routes) {
|
||||||
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
||||||
const s = new UrlSegmentGroup([], {});
|
const s = new UrlSegmentGroup([], {});
|
||||||
s._sourceSegment = segmentGroup;
|
s._sourceSegment = segmentGroup;
|
||||||
|
if (relativeLinkResolution === 'legacy') {
|
||||||
s._segmentIndexShift = segmentGroup.segments.length;
|
s._segmentIndexShift = segmentGroup.segments.length;
|
||||||
|
} else {
|
||||||
|
s._segmentIndexShift = consumedSegments.length;
|
||||||
|
}
|
||||||
res[getOutlet(r)] = s;
|
res[getOutlet(r)] = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,6 +297,11 @@ export class Router {
|
|||||||
*/
|
*/
|
||||||
urlUpdateStrategy: 'deferred'|'eager' = 'deferred';
|
urlUpdateStrategy: 'deferred'|'eager' = 'deferred';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link RouterModule} for more information.
|
||||||
|
*/
|
||||||
|
relativeLinkResolution: 'legacy'|'corrected' = 'legacy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the router service.
|
* Creates the router service.
|
||||||
*/
|
*/
|
||||||
@ -676,7 +681,7 @@ export class Router {
|
|||||||
urlAndSnapshot$ = redirectsApplied$.pipe(mergeMap((appliedUrl: UrlTree) => {
|
urlAndSnapshot$ = redirectsApplied$.pipe(mergeMap((appliedUrl: UrlTree) => {
|
||||||
return recognize(
|
return recognize(
|
||||||
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl),
|
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl),
|
||||||
this.paramsInheritanceStrategy)
|
this.paramsInheritanceStrategy, this.relativeLinkResolution)
|
||||||
.pipe(map((snapshot: any) => {
|
.pipe(map((snapshot: any) => {
|
||||||
(this.events as Subject<Event>)
|
(this.events as Subject<Event>)
|
||||||
.next(new RoutesRecognized(
|
.next(new RoutesRecognized(
|
||||||
|
@ -417,6 +417,36 @@ export interface ExtraOptions {
|
|||||||
* - `'eager'`, updates browser URL at the beginning of navigation.
|
* - `'eager'`, updates browser URL at the beginning of navigation.
|
||||||
*/
|
*/
|
||||||
urlUpdateStrategy?: 'deferred'|'eager';
|
urlUpdateStrategy?: 'deferred'|'eager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables a bug fix that corrects relative link resolution in components with empty paths.
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* const routes = [
|
||||||
|
* {
|
||||||
|
* path: '',
|
||||||
|
* component: ContainerComponent,
|
||||||
|
* children: [
|
||||||
|
* { path: 'a', component: AComponent },
|
||||||
|
* { path: 'b', component: BComponent },
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ];
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* From the `ContainerComponent`, this will not work:
|
||||||
|
*
|
||||||
|
* `<a [routerLink]="['./a']">Link to A</a>`
|
||||||
|
*
|
||||||
|
* However, this will work:
|
||||||
|
*
|
||||||
|
* `<a [routerLink]="['../a']">Link to A</a>`
|
||||||
|
*
|
||||||
|
* In other words, you're required to use `../` rather than `./`. The current default in v6
|
||||||
|
* is `legacy`, and this option will be removed in v7 to default to the corrected behavior.
|
||||||
|
*/
|
||||||
|
relativeLinkResolution?: 'legacy'|'corrected';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRouter(
|
export function setupRouter(
|
||||||
@ -465,6 +495,10 @@ export function setupRouter(
|
|||||||
router.urlUpdateStrategy = opts.urlUpdateStrategy;
|
router.urlUpdateStrategy = opts.urlUpdateStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.relativeLinkResolution) {
|
||||||
|
router.relativeLinkResolution = opts.relativeLinkResolution;
|
||||||
|
}
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,6 +452,45 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set url segment and index properly with the "corrected" option for nested empty-path segments',
|
||||||
|
() => {
|
||||||
|
const url = tree('a/b') as any;
|
||||||
|
recognize(
|
||||||
|
RootComponent, [{
|
||||||
|
path: 'a',
|
||||||
|
children: [{
|
||||||
|
path: 'b',
|
||||||
|
component: ComponentB,
|
||||||
|
children: [{
|
||||||
|
path: '',
|
||||||
|
component: ComponentC,
|
||||||
|
children: [{path: '', component: ComponentD}]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
url, 'a/b', 'emptyOnly', 'corrected')
|
||||||
|
.forEach((s: any) => {
|
||||||
|
expect(s.root._urlSegment).toBe(url.root);
|
||||||
|
expect(s.root._lastPathIndex).toBe(-1);
|
||||||
|
|
||||||
|
const a = s.firstChild(s.root) !;
|
||||||
|
expect(a._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(a._lastPathIndex).toBe(0);
|
||||||
|
|
||||||
|
const b = s.firstChild(a) !;
|
||||||
|
expect(b._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(b._lastPathIndex).toBe(1);
|
||||||
|
|
||||||
|
const c = s.firstChild(b) !;
|
||||||
|
expect(c._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(c._lastPathIndex).toBe(1);
|
||||||
|
|
||||||
|
const d = s.firstChild(c) !;
|
||||||
|
expect(d._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(d._lastPathIndex).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should set url segment and index properly when nested empty-path segments (2)', () => {
|
it('should set url segment and index properly when nested empty-path segments (2)', () => {
|
||||||
const url = tree('');
|
const url = tree('');
|
||||||
recognize(
|
recognize(
|
||||||
|
2
tools/public_api_guard/router/router.d.ts
vendored
2
tools/public_api_guard/router/router.d.ts
vendored
@ -119,6 +119,7 @@ export interface ExtraOptions {
|
|||||||
onSameUrlNavigation?: 'reload' | 'ignore';
|
onSameUrlNavigation?: 'reload' | 'ignore';
|
||||||
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
paramsInheritanceStrategy?: 'emptyOnly' | 'always';
|
||||||
preloadingStrategy?: any;
|
preloadingStrategy?: any;
|
||||||
|
relativeLinkResolution?: 'legacy' | 'corrected';
|
||||||
scrollOffset?: [number, number] | (() => [number, number]);
|
scrollOffset?: [number, number] | (() => [number, number]);
|
||||||
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
|
scrollPositionRestoration?: 'disabled' | 'enabled' | 'top';
|
||||||
urlUpdateStrategy?: 'deferred' | 'eager';
|
urlUpdateStrategy?: 'deferred' | 'eager';
|
||||||
@ -320,6 +321,7 @@ export declare class Router {
|
|||||||
navigated: boolean;
|
navigated: boolean;
|
||||||
onSameUrlNavigation: 'reload' | 'ignore';
|
onSameUrlNavigation: 'reload' | 'ignore';
|
||||||
paramsInheritanceStrategy: 'emptyOnly' | 'always';
|
paramsInheritanceStrategy: 'emptyOnly' | 'always';
|
||||||
|
relativeLinkResolution: 'legacy' | 'corrected';
|
||||||
routeReuseStrategy: RouteReuseStrategy;
|
routeReuseStrategy: RouteReuseStrategy;
|
||||||
readonly routerState: RouterState;
|
readonly routerState: RouterState;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user