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:
parent
dc6d40e5bc
commit
112324a614
2
goldens/public-api/router/router.d.ts
vendored
2
goldens/public-api/router/router.d.ts
vendored
@ -380,6 +380,7 @@ export declare class RouterLink implements OnChanges {
|
|||||||
preserveFragment: boolean;
|
preserveFragment: boolean;
|
||||||
queryParams?: Params | null;
|
queryParams?: Params | null;
|
||||||
queryParamsHandling?: QueryParamsHandling | null;
|
queryParamsHandling?: QueryParamsHandling | null;
|
||||||
|
relativeTo?: ActivatedRoute | null;
|
||||||
replaceUrl: boolean;
|
replaceUrl: boolean;
|
||||||
set routerLink(commands: any[] | string | null | undefined);
|
set routerLink(commands: any[] | string | null | undefined);
|
||||||
skipLocationChange: boolean;
|
skipLocationChange: boolean;
|
||||||
@ -412,6 +413,7 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
|||||||
preserveFragment: boolean;
|
preserveFragment: boolean;
|
||||||
queryParams?: Params | null;
|
queryParams?: Params | null;
|
||||||
queryParamsHandling?: QueryParamsHandling | null;
|
queryParamsHandling?: QueryParamsHandling | null;
|
||||||
|
relativeTo?: ActivatedRoute | null;
|
||||||
replaceUrl: boolean;
|
replaceUrl: boolean;
|
||||||
set routerLink(commands: any[] | string | null | undefined);
|
set routerLink(commands: any[] | string | null | undefined);
|
||||||
skipLocationChange: boolean;
|
skipLocationChange: boolean;
|
||||||
|
@ -169,6 +169,17 @@ export class RouterLink implements OnChanges {
|
|||||||
* @see {@link Router#navigateByUrl Router#navigateByUrl}
|
* @see {@link Router#navigateByUrl Router#navigateByUrl}
|
||||||
*/
|
*/
|
||||||
@Input() state?: {[k: string]: any};
|
@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 commands: any[] = [];
|
||||||
private preserve!: boolean;
|
private preserve!: boolean;
|
||||||
|
|
||||||
@ -220,7 +231,9 @@ export class RouterLink implements OnChanges {
|
|||||||
|
|
||||||
get urlTree(): UrlTree {
|
get urlTree(): UrlTree {
|
||||||
return this.router.createUrlTree(this.commands, {
|
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,
|
queryParams: this.queryParams,
|
||||||
fragment: this.fragment,
|
fragment: this.fragment,
|
||||||
queryParamsHandling: this.queryParamsHandling,
|
queryParamsHandling: this.queryParamsHandling,
|
||||||
@ -296,6 +309,17 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
|||||||
* @see {@link Router#navigateByUrl Router#navigateByUrl}
|
* @see {@link Router#navigateByUrl Router#navigateByUrl}
|
||||||
*/
|
*/
|
||||||
@Input() state?: {[k: string]: any};
|
@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 commands: any[] = [];
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
// TODO(issue/24571): remove '!'.
|
// TODO(issue/24571): remove '!'.
|
||||||
@ -373,7 +397,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
get urlTree(): UrlTree {
|
get urlTree(): UrlTree {
|
||||||
return this.router.createUrlTree(this.commands, {
|
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,
|
queryParams: this.queryParams,
|
||||||
fragment: this.fragment,
|
fragment: this.fragment,
|
||||||
queryParamsHandling: this.queryParamsHandling,
|
queryParamsHandling: this.queryParamsHandling,
|
||||||
|
@ -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;
|
relativeTo?: ActivatedRoute|null;
|
||||||
|
|
||||||
@ -1094,6 +1097,9 @@ export class Router {
|
|||||||
*
|
*
|
||||||
* // navigate to /team/44/user/22
|
* // navigate to /team/44/user/22
|
||||||
* router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});
|
* 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 {
|
createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree {
|
||||||
|
@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
import {CommonModule, Location} from '@angular/common';
|
import {CommonModule, Location} from '@angular/common';
|
||||||
import {SpyLocation} from '@angular/common/testing';
|
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 {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 {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
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 {EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
|
||||||
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
|
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', () => {
|
describe('relativeLinkResolution', () => {
|
||||||
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
|
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
|
||||||
class RelativeLinkCmp {
|
class RelativeLinkCmp {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user