diff --git a/goldens/public-api/router/router.d.ts b/goldens/public-api/router/router.d.ts
index 73cb56deb0..bb9bc0a39f 100644
--- a/goldens/public-api/router/router.d.ts
+++ b/goldens/public-api/router/router.d.ts
@@ -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;
diff --git a/packages/router/src/directives/router_link.ts b/packages/router/src/directives/router_link.ts
index 01f782b6af..64acd67024 100644
--- a/packages/router/src/directives/router_link.ts
+++ b/packages/router/src/directives/router_link.ts
@@ -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,
diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts
index f560d20dcf..0e5b4b7679 100644
--- a/packages/router/src/router.ts
+++ b/packages/router/src/router.ts
@@ -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 {
diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts
index e7f6c7bae1..cae9479ef0 100644
--- a/packages/router/test/integration.spec.ts
+++ b/packages/router/test/integration.spec.ts
@@ -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: ''})
+ class ChildRootCmp {
+ }
+
+ @Component({
+ selector: 'link-cmp',
+ template:
+ `link
+
+ `
+ })
+ 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: `link`})
class RelativeLinkCmp {