Skip to content

Commit a5a9b40

Browse files
committed
feat(router): Add transient info to RouterLink input (#53784)
This is a follow up to 5c1d441 which added the `info` property to navigation requests. `RouterLink` now supports passing that transient navigation info to the navigation request. This info object can be anything and doesn't have to be serializable. One use-case might be for passing the element that was clicked. This might be useful for something like view transitions. In the "animating with javascript" example from the blog (https://stackblitz.com/edit/stackblitz-starters-cklnkm) those links could have done this instead of needing to create a separate directive that tracks clicks. PR Close #53784
1 parent 91f250d commit a5a9b40

File tree

3 files changed

+43
-3
lines changed

3 files changed

+43
-3
lines changed

goldens/public-api/router/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ class RouterLink implements OnChanges, OnDestroy {
782782
constructor(router: Router, route: ActivatedRoute, tabIndexAttribute: string | null | undefined, renderer: Renderer2, el: ElementRef, locationStrategy?: LocationStrategy | undefined);
783783
fragment?: string;
784784
href: string | null;
785+
info?: unknown;
785786
// (undocumented)
786787
static ngAcceptInputType_preserveFragment: unknown;
787788
// (undocumented)
@@ -808,7 +809,7 @@ class RouterLink implements OnChanges, OnDestroy {
808809
// (undocumented)
809810
get urlTree(): UrlTree | null;
810811
// (undocumented)
811-
static ɵdir: i0.ɵɵDirectiveDeclaration<RouterLink, "[routerLink]", never, { "target": { "alias": "target"; "required": false; }; "queryParams": { "alias": "queryParams"; "required": false; }; "fragment": { "alias": "fragment"; "required": false; }; "queryParamsHandling": { "alias": "queryParamsHandling"; "required": false; }; "state": { "alias": "state"; "required": false; }; "relativeTo": { "alias": "relativeTo"; "required": false; }; "preserveFragment": { "alias": "preserveFragment"; "required": false; }; "skipLocationChange": { "alias": "skipLocationChange"; "required": false; }; "replaceUrl": { "alias": "replaceUrl"; "required": false; }; "routerLink": { "alias": "routerLink"; "required": false; }; }, {}, never, never, true, never>;
812+
static ɵdir: i0.ɵɵDirectiveDeclaration<RouterLink, "[routerLink]", never, { "target": { "alias": "target"; "required": false; }; "queryParams": { "alias": "queryParams"; "required": false; }; "fragment": { "alias": "fragment"; "required": false; }; "queryParamsHandling": { "alias": "queryParamsHandling"; "required": false; }; "state": { "alias": "state"; "required": false; }; "info": { "alias": "info"; "required": false; }; "relativeTo": { "alias": "relativeTo"; "required": false; }; "preserveFragment": { "alias": "preserveFragment"; "required": false; }; "skipLocationChange": { "alias": "skipLocationChange"; "required": false; }; "replaceUrl": { "alias": "replaceUrl"; "required": false; }; "routerLink": { "alias": "routerLink"; "required": false; }; }, {}, never, never, true, never>;
812813
// (undocumented)
813814
static ɵfac: i0.ɵɵFactoryDeclaration<RouterLink, [null, null, { attribute: "tabindex"; }, null, null, null]>;
814815
}

packages/router/src/directives/router_link.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ export class RouterLink implements OnChanges, OnDestroy {
160160
* @see {@link Router#navigateByUrl}
161161
*/
162162
@Input() state?: {[k: string]: any};
163+
/**
164+
* Passed to {@link Router#navigateByUrl} as part of the
165+
* `NavigationBehaviorOptions`.
166+
* @see {@link NavigationBehaviorOptions#info}
167+
* @see {@link Router#navigateByUrl}
168+
*/
169+
@Input() info?: unknown;
163170
/**
164171
* Passed to {@link Router#createUrlTree} as part of the
165172
* `UrlCreationOptions`.
@@ -287,6 +294,7 @@ export class RouterLink implements OnChanges, OnDestroy {
287294
skipLocationChange: this.skipLocationChange,
288295
replaceUrl: this.replaceUrl,
289296
state: this.state,
297+
info: this.info,
290298
};
291299
this.router.navigateByUrl(this.urlTree, extras);
292300

packages/router/test/integration.spec.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
*/
88

99
import {CommonModule, HashLocationStrategy, Location, LocationStrategy, PlatformLocation, PopStateEvent} from '@angular/common';
10-
import {ChangeDetectionStrategy, Component, EnvironmentInjector, inject as coreInject, Inject, Injectable, InjectionToken, NgModule, NgModuleRef, NgZone, OnDestroy, QueryList, Type, ViewChild, ViewChildren, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
10+
import {ApplicationRef, ChangeDetectionStrategy, Component, EnvironmentInjector, inject as coreInject, Inject, Injectable, InjectionToken, NgModule, NgModuleRef, NgZone, OnDestroy, QueryList, Type, ViewChild, ViewChildren, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
1111
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
1212
import {By} from '@angular/platform-browser/src/dom/debug/by';
1313
import {expect} from '@angular/platform-browser/testing/src/matchers';
1414
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterLink, RouterLinkActive, RouterModule, RouterOutlet, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
1515
import {RouterTestingHarness} from '@angular/router/testing';
16-
import {concat, EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
16+
import {concat, EMPTY, firstValueFrom, Observable, Observer, of, Subscription} from 'rxjs';
1717
import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators';
1818

1919
import {CanActivateChildFn, CanActivateFn, CanMatchFn, Data, ResolveFn} from '../src/models';
@@ -158,6 +158,37 @@ describe('Integration', () => {
158158
expect(observedInfo).toEqual('navigation info');
159159
});
160160

161+
it('should set transient navigation info for routerlink', async () => {
162+
let observedInfo: unknown;
163+
const router = TestBed.inject(Router);
164+
router.resetConfig([
165+
{
166+
path: 'simple',
167+
component: SimpleCmp,
168+
canActivate: [() => {
169+
observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info;
170+
return true;
171+
}]
172+
},
173+
]);
174+
@Component({
175+
standalone: true,
176+
imports: [RouterLink],
177+
template: `<a #simpleLink [routerLink]="'/simple'" [info]="simpleLink"></a>`
178+
})
179+
class App {
180+
}
181+
182+
const fixture = TestBed.createComponent(App);
183+
fixture.autoDetectChanges();
184+
const anchor = fixture.nativeElement.querySelector('a');
185+
anchor.click();
186+
await fixture.whenStable();
187+
188+
// An example use-case might be to pass the clicked link along with the navigation information
189+
expect(observedInfo).toBeInstanceOf(HTMLAnchorElement);
190+
});
191+
161192
it('should make transient navigation info available in redirect', async () => {
162193
let observedInfo: unknown;
163194
const router = TestBed.inject(Router);

0 commit comments

Comments
 (0)