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
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue