diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index b6eeded31a..6c3f8b327f 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -20,20 +20,24 @@ class NoMatch {} export function recognize( rootComponentType: Type| null, config: Routes, urlTree: UrlTree, url: string, - paramsInheritanceStrategy: ParamsInheritanceStrategy = - 'emptyOnly'): Observable { - return new Recognizer(rootComponentType, config, urlTree, url, paramsInheritanceStrategy) + paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly', + relativeLinkResolution: 'legacy' | 'corrected' = 'legacy'): Observable { + return new Recognizer( + rootComponentType, config, urlTree, url, paramsInheritanceStrategy, + relativeLinkResolution) .recognize(); } class Recognizer { constructor( private rootComponentType: Type|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 { 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); @@ -134,8 +138,8 @@ class Recognizer { const childConfig: Route[] = getChildConfig(route); - const {segmentGroup, slicedSegments} = - split(rawSegment, consumedSegments, rawSlicedSegments, childConfig); + const {segmentGroup, slicedSegments} = split( + rawSegment, consumedSegments, rawSlicedSegments, childConfig, this.relativeLinkResolution); if (slicedSegments.length === 0 && segmentGroup.hasChildren()) { const children = this.processChildren(childConfig, segmentGroup); @@ -232,7 +236,7 @@ function getPathIndexShift(segmentGroup: UrlSegmentGroup): number { function split( segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], - config: Route[]) { + config: Route[], relativeLinkResolution: 'legacy' | 'corrected') { if (slicedSegments.length > 0 && containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) { const s = new UrlSegmentGroup( @@ -248,7 +252,8 @@ function split( containsEmptyPathMatches(segmentGroup, slicedSegments, config)) { const s = new UrlSegmentGroup( segmentGroup.segments, addEmptyPathsToChildrenIfNeeded( - segmentGroup, slicedSegments, config, segmentGroup.children)); + segmentGroup, consumedSegments, slicedSegments, config, + segmentGroup.children, relativeLinkResolution)); s._sourceSegment = segmentGroup; s._segmentIndexShift = consumedSegments.length; return {segmentGroup: s, slicedSegments}; @@ -261,14 +266,19 @@ function split( } function addEmptyPathsToChildrenIfNeeded( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], - children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} { + segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], + routes: Route[], children: {[name: string]: UrlSegmentGroup}, + relativeLinkResolution: 'legacy' | 'corrected'): {[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([], {}); s._sourceSegment = segmentGroup; - s._segmentIndexShift = segmentGroup.segments.length; + if (relativeLinkResolution === 'legacy') { + s._segmentIndexShift = segmentGroup.segments.length; + } else { + s._segmentIndexShift = consumedSegments.length; + } res[getOutlet(r)] = s; } } diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index cd8fb98aa4..8ce079e304 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -297,6 +297,11 @@ export class Router { */ urlUpdateStrategy: 'deferred'|'eager' = 'deferred'; + /** + * See {@link RouterModule} for more information. + */ + relativeLinkResolution: 'legacy'|'corrected' = 'legacy'; + /** * Creates the router service. */ @@ -676,7 +681,7 @@ export class Router { urlAndSnapshot$ = redirectsApplied$.pipe(mergeMap((appliedUrl: UrlTree) => { return recognize( this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl), - this.paramsInheritanceStrategy) + this.paramsInheritanceStrategy, this.relativeLinkResolution) .pipe(map((snapshot: any) => { (this.events as Subject) .next(new RoutesRecognized( diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index 7566ee1400..384f7e987a 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -417,6 +417,36 @@ export interface ExtraOptions { * - `'eager'`, updates browser URL at the beginning of navigation. */ 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: + * + * `Link to A` + * + * However, this will work: + * + * `Link to 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( @@ -465,6 +495,10 @@ export function setupRouter( router.urlUpdateStrategy = opts.urlUpdateStrategy; } + if (opts.relativeLinkResolution) { + router.relativeLinkResolution = opts.relativeLinkResolution; + } + return router; } diff --git a/packages/router/test/recognize.spec.ts b/packages/router/test/recognize.spec.ts index b2537d13c6..0494f2fe66 100644 --- a/packages/router/test/recognize.spec.ts +++ b/packages/router/test/recognize.spec.ts @@ -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)', () => { const url = tree(''); recognize( diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index 6575a15279..c97c290390 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -119,6 +119,7 @@ export interface ExtraOptions { onSameUrlNavigation?: 'reload' | 'ignore'; paramsInheritanceStrategy?: 'emptyOnly' | 'always'; preloadingStrategy?: any; + relativeLinkResolution?: 'legacy' | 'corrected'; scrollOffset?: [number, number] | (() => [number, number]); scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'; urlUpdateStrategy?: 'deferred' | 'eager'; @@ -320,6 +321,7 @@ export declare class Router { navigated: boolean; onSameUrlNavigation: 'reload' | 'ignore'; paramsInheritanceStrategy: 'emptyOnly' | 'always'; + relativeLinkResolution: 'legacy' | 'corrected'; routeReuseStrategy: RouteReuseStrategy; readonly routerState: RouterState; readonly url: string;