Skip to content

Commit 142b47d

Browse files
committed
feat(router): add relativeTo as an input to routerLink
Allow configuration of `relativeTo` in the `routerLink` directive. This is related to the clearing of auxiliary routes, where you need to use `relativeTo: route.parent` in order to clear it from the activated auxiliary component itself. This is because `relativeTo: route` will consume the segment that we're trying to clear, so there is really no way to do this with routerLink at the moment. Related issue: #13523 Related (internal link): https://yaqs.corp.google.com/eng/q/5999443644645376
1 parent 9f20942 commit 142b47d

4 files changed

Lines changed: 79 additions & 4 deletions

File tree

‎goldens/public-api/router/router.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ export declare class RouterLink implements OnChanges {
380380
preserveFragment: boolean;
381381
queryParams?: Params | null;
382382
queryParamsHandling?: QueryParamsHandling | null;
383+
relativeTo?: ActivatedRoute | null;
383384
replaceUrl: boolean;
384385
set routerLink(commands: any[] | string | null | undefined);
385386
skipLocationChange: boolean;
@@ -412,6 +413,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
412413
preserveFragment: boolean;
413414
queryParams?: Params | null;
414415
queryParamsHandling?: QueryParamsHandling | null;
416+
relativeTo?: ActivatedRoute | null;
415417
replaceUrl: boolean;
416418
set routerLink(commands: any[] | string | null | undefined);
417419
skipLocationChange: boolean;

‎packages/router/src/directives/router_link.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ export class RouterLink implements OnChanges {
169169
* @see {@link Router#navigateByUrl Router#navigateByUrl}
170170
*/
171171
@Input() state?: {[k: string]: any};
172+
/**
173+
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the
174+
* `UrlCreationOptions`.
175+
* Specify a value here when you do not want to use the default value
176+
* for `routerLink`, which is the current activated route.
177+
* Note that a value of `undefined` here will use the `routerLink` default.
178+
* @see {@link UrlCreationOptions#relativeTo UrlCreationOptions#relativeTo}
179+
* @see {@link Router#createUrlTree Router#createUrlTree}
180+
*/
181+
@Input() relativeTo?: ActivatedRoute|null;
182+
172183
private commands: any[] = [];
173184
private preserve!: boolean;
174185

@@ -220,7 +231,7 @@ export class RouterLink implements OnChanges {
220231

221232
get urlTree(): UrlTree {
222233
return this.router.createUrlTree(this.commands, {
223-
relativeTo: this.route,
234+
relativeTo: this.relativeTo !== undefined ? this.relativeTo : this.route,
224235
queryParams: this.queryParams,
225236
fragment: this.fragment,
226237
queryParamsHandling: this.queryParamsHandling,
@@ -296,6 +307,17 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
296307
* @see {@link Router#navigateByUrl Router#navigateByUrl}
297308
*/
298309
@Input() state?: {[k: string]: any};
310+
/**
311+
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the
312+
* `UrlCreationOptions`.
313+
* Specify a value here when you do not want to use the default value
314+
* for `routerLink`, which is the current activated route.
315+
* Note that a value of `undefined` here will use the `routerLink` default.
316+
* @see {@link UrlCreationOptions#relativeTo UrlCreationOptions#relativeTo}
317+
* @see {@link Router#createUrlTree Router#createUrlTree}
318+
*/
319+
@Input() relativeTo?: ActivatedRoute|null;
320+
299321
private commands: any[] = [];
300322
private subscription: Subscription;
301323
// TODO(issue/24571): remove '!'.
@@ -373,7 +395,7 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
373395

374396
get urlTree(): UrlTree {
375397
return this.router.createUrlTree(this.commands, {
376-
relativeTo: this.route,
398+
relativeTo: this.relativeTo !== undefined ? this.relativeTo : this.route,
377399
queryParams: this.queryParams,
378400
fragment: this.fragment,
379401
queryParamsHandling: this.queryParamsHandling,

‎packages/router/src/router.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,9 @@ export class Router {
10941094
*
10951095
* // navigate to /team/44/user/22
10961096
* router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});
1097+
*
1098+
* Note that a value of `null` or `undefined` for `relativeTo` indicates that the
1099+
* tree should be created relative to the root.
10971100
* ```
10981101
*/
10991102
createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree {

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import {CommonModule, Location} from '@angular/common';
1010
import {SpyLocation} from '@angular/common/testing';
11-
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
11+
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ViewChild, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
1212
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
13+
import {describe} from '@angular/core/testing/src/testing_internal';
1314
import {By} from '@angular/platform-browser/src/dom/debug/by';
1415
import {expect} from '@angular/platform-browser/testing/src/matchers';
15-
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
16+
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterLink, RouterLinkWithHref, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
1617
import {EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
1718
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
1819

@@ -5382,6 +5383,53 @@ describe('Integration', () => {
53825383
})));
53835384
});
53845385

5386+
it('can use `relativeTo` `route.parent` in `routerLink` to close secondary outlet',
5387+
fakeAsync(() => {
5388+
// Given
5389+
@Component({template: '<router-outlet name="secondary"></router-outlet>'})
5390+
class ChildRootCmp {
5391+
}
5392+
5393+
@Component({
5394+
selector: 'link-cmp',
5395+
template:
5396+
`<a [relativeTo]="route.parent" [routerLink]="[{outlets: {'secondary': null}}]">link</a>
5397+
<button [relativeTo]="route.parent" [routerLink]="[{outlets: {'secondary': null}}]">link</button>
5398+
`
5399+
})
5400+
class RelativeLinkCmp {
5401+
@ViewChild(RouterLink) buttonLink!: RouterLink;
5402+
@ViewChild(RouterLinkWithHref) aLink!: RouterLink;
5403+
5404+
constructor(readonly route: ActivatedRoute) {}
5405+
}
5406+
@NgModule({
5407+
declarations: [RelativeLinkCmp, ChildRootCmp],
5408+
imports: [RouterModule.forChild([{
5409+
path: 'childRoot',
5410+
component: ChildRootCmp,
5411+
children: [
5412+
{path: 'popup', outlet: 'secondary', component: RelativeLinkCmp},
5413+
]
5414+
}])]
5415+
})
5416+
class LazyLoadedModule {
5417+
}
5418+
const router = TestBed.inject(Router);
5419+
router.resetConfig([{path: 'root', loadChildren: () => LazyLoadedModule}]);
5420+
5421+
// When
5422+
router.navigateByUrl('/root/childRoot/(secondary:popup)');
5423+
const fixture = createRoot(router, RootCmp);
5424+
advance(fixture);
5425+
5426+
// Then
5427+
const relativeLinkCmp =
5428+
fixture.debugElement.query(By.directive(RelativeLinkCmp)).componentInstance;
5429+
expect(relativeLinkCmp.aLink.urlTree.toString()).toEqual('/root/childRoot');
5430+
expect(relativeLinkCmp.buttonLink.urlTree.toString()).toEqual('/root/childRoot');
5431+
}));
5432+
53855433
describe('relativeLinkResolution', () => {
53865434
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
53875435
class RelativeLinkCmp {

0 commit comments

Comments
 (0)