feat(router): add `relativeTo` as an input to `routerLink` (#39720)

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

PR Close #39720
This commit is contained in:
Andrew Scott 2020-11-16 15:41:39 -08:00 committed by Alex Rickabaugh
parent dc6d40e5bc
commit 112324a614
4 changed files with 86 additions and 4 deletions

View File

@ -380,6 +380,7 @@ export declare class RouterLink implements OnChanges {
preserveFragment: boolean;
queryParams?: Params | null;
queryParamsHandling?: QueryParamsHandling | null;
relativeTo?: ActivatedRoute | null;
replaceUrl: boolean;
set routerLink(commands: any[] | string | null | undefined);
skipLocationChange: boolean;
@ -412,6 +413,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
preserveFragment: boolean;
queryParams?: Params | null;
queryParamsHandling?: QueryParamsHandling | null;
relativeTo?: ActivatedRoute | null;
replaceUrl: boolean;
set routerLink(commands: any[] | string | null | undefined);
skipLocationChange: boolean;

View File

@ -169,6 +169,17 @@ export class RouterLink implements OnChanges {
* @see {@link Router#navigateByUrl Router#navigateByUrl}
*/
@Input() state?: {[k: string]: any};
/**
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the
* `UrlCreationOptions`.
* Specify a value here when you do not want to use the default value
* for `routerLink`, which is the current activated route.
* Note that a value of `undefined` here will use the `routerLink` default.
* @see {@link UrlCreationOptions#relativeTo UrlCreationOptions#relativeTo}
* @see {@link Router#createUrlTree Router#createUrlTree}
*/
@Input() relativeTo?: ActivatedRoute|null;
private commands: any[] = [];
private preserve!: boolean;
@ -220,7 +231,9 @@ export class RouterLink implements OnChanges {
get urlTree(): UrlTree {
return this.router.createUrlTree(this.commands, {
relativeTo: this.route,
// If the `relativeTo` input is not defined, we want to use `this.route` by default.
// Otherwise, we should use the value provided by the user in the input.
relativeTo: this.relativeTo !== undefined ? this.relativeTo : this.route,
queryParams: this.queryParams,
fragment: this.fragment,
queryParamsHandling: this.queryParamsHandling,
@ -296,6 +309,17 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
* @see {@link Router#navigateByUrl Router#navigateByUrl}
*/
@Input() state?: {[k: string]: any};
/**
* Passed to {@link Router#createUrlTree Router#createUrlTree} as part of the
* `UrlCreationOptions`.
* Specify a value here when you do not want to use the default value
* for `routerLink`, which is the current activated route.
* Note that a value of `undefined` here will use the `routerLink` default.
* @see {@link UrlCreationOptions#relativeTo UrlCreationOptions#relativeTo}
* @see {@link Router#createUrlTree Router#createUrlTree}
*/
@Input() relativeTo?: ActivatedRoute|null;
private commands: any[] = [];
private subscription: Subscription;
// TODO(issue/24571): remove '!'.
@ -373,7 +397,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
get urlTree(): UrlTree {
return this.router.createUrlTree(this.commands, {
relativeTo: this.route,
// If the `relativeTo` input is not defined, we want to use `this.route` by default.
// Otherwise, we should use the value provided by the user in the input.
relativeTo: this.relativeTo !== undefined ? this.relativeTo : this.route,
queryParams: this.queryParams,
fragment: this.fragment,
queryParamsHandling: this.queryParamsHandling,

View File

@ -81,6 +81,9 @@ export interface UrlCreationOptions {
* }
* }
* ```
*
* A value of `null` or `undefined` indicates that the navigation commands should be applied
* relative to the root.
*/
relativeTo?: ActivatedRoute|null;
@ -1094,6 +1097,9 @@ export class Router {
*
* // navigate to /team/44/user/22
* router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});
*
* Note that a value of `null` or `undefined` for `relativeTo` indicates that the
* tree should be created relative to the root.
* ```
*/
createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree {

View File

@ -8,11 +8,12 @@
import {CommonModule, Location} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ChangeDetectionStrategy, Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ViewChild, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {describe} from '@angular/core/testing/src/testing_internal';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
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';
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';
import {EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
@ -5382,6 +5383,53 @@ describe('Integration', () => {
})));
});
it('can use `relativeTo` `route.parent` in `routerLink` to close secondary outlet',
fakeAsync(() => {
// Given
@Component({template: '<router-outlet name="secondary"></router-outlet>'})
class ChildRootCmp {
}
@Component({
selector: 'link-cmp',
template:
`<a [relativeTo]="route.parent" [routerLink]="[{outlets: {'secondary': null}}]">link</a>
<button [relativeTo]="route.parent" [routerLink]="[{outlets: {'secondary': null}}]">link</button>
`
})
class RelativeLinkCmp {
@ViewChild(RouterLink) buttonLink!: RouterLink;
@ViewChild(RouterLinkWithHref) aLink!: RouterLink;
constructor(readonly route: ActivatedRoute) {}
}
@NgModule({
declarations: [RelativeLinkCmp, ChildRootCmp],
imports: [RouterModule.forChild([{
path: 'childRoot',
component: ChildRootCmp,
children: [
{path: 'popup', outlet: 'secondary', component: RelativeLinkCmp},
]
}])]
})
class LazyLoadedModule {
}
const router = TestBed.inject(Router);
router.resetConfig([{path: 'root', loadChildren: () => LazyLoadedModule}]);
// When
router.navigateByUrl('/root/childRoot/(secondary:popup)');
const fixture = createRoot(router, RootCmp);
advance(fixture);
// Then
const relativeLinkCmp =
fixture.debugElement.query(By.directive(RelativeLinkCmp)).componentInstance;
expect(relativeLinkCmp.aLink.urlTree.toString()).toEqual('/root/childRoot');
expect(relativeLinkCmp.buttonLink.urlTree.toString()).toEqual('/root/childRoot');
}));
describe('relativeLinkResolution', () => {
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
class RelativeLinkCmp {