Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions goldens/public-api/router/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export declare class RouterLink implements OnChanges {
preserveFragment: boolean;
queryParams?: Params | null;
queryParamsHandling?: QueryParamsHandling | null;
relativeTo?: ActivatedRoute | null;
Comment thread
atscott marked this conversation as resolved.
replaceUrl: boolean;
set routerLink(commands: any[] | string | null | undefined);
skipLocationChange: boolean;
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 28 additions & 2 deletions packages/router/src/directives/router_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
atscott marked this conversation as resolved.
Outdated

private commands: any[] = [];
private preserve!: boolean;

Expand Down Expand Up @@ -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,
Comment thread
atscott marked this conversation as resolved.
Outdated
queryParams: this.queryParams,
fragment: this.fragment,
queryParamsHandling: this.queryParamsHandling,
Expand Down Expand Up @@ -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 '!'.
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
52 changes: 50 additions & 2 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down