Skip to content

Commit

Permalink
feat(router): Add transient info to RouterLink input (angular#53784)
Browse files Browse the repository at this point in the history
This is a follow up to angular@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 angular#53784
  • Loading branch information
atscott authored and danieljancar committed Jan 26, 2024
1 parent 577c050 commit 365fb66
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 3 deletions.
3 changes: 2 additions & 1 deletion goldens/public-api/router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ class RouterLink implements OnChanges, OnDestroy {
constructor(router: Router, route: ActivatedRoute, tabIndexAttribute: string | null | undefined, renderer: Renderer2, el: ElementRef, locationStrategy?: LocationStrategy | undefined);
fragment?: string;
href: string | null;
info?: unknown;
// (undocumented)
static ngAcceptInputType_preserveFragment: unknown;
// (undocumented)
Expand All @@ -808,7 +809,7 @@ class RouterLink implements OnChanges, OnDestroy {
// (undocumented)
get urlTree(): UrlTree | null;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<RouterLink, [null, null, { attribute: "tabindex"; }, null, null, null]>;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/router/src/directives/router_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export class RouterLink implements OnChanges, OnDestroy {
* @see {@link Router#navigateByUrl}
*/
@Input() state?: {[k: string]: any};
/**
* Passed to {@link Router#navigateByUrl} as part of the
* `NavigationBehaviorOptions`.
* @see {@link NavigationBehaviorOptions#info}
* @see {@link Router#navigateByUrl}
*/
@Input() info?: unknown;
/**
* Passed to {@link Router#createUrlTree} as part of the
* `UrlCreationOptions`.
Expand Down Expand Up @@ -287,6 +294,7 @@ export class RouterLink implements OnChanges, OnDestroy {
skipLocationChange: this.skipLocationChange,
replaceUrl: this.replaceUrl,
state: this.state,
info: this.info,
};
this.router.navigateByUrl(this.urlTree, extras);

Expand Down
35 changes: 33 additions & 2 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
*/

import {CommonModule, HashLocationStrategy, Location, LocationStrategy, PlatformLocation, PopStateEvent} from '@angular/common';
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';
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';
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
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';
import {RouterTestingHarness} from '@angular/router/testing';
import {concat, EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
import {concat, EMPTY, firstValueFrom, Observable, Observer, of, Subscription} from 'rxjs';
import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators';

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

it('should set transient navigation info for routerlink', async () => {
let observedInfo: unknown;
const router = TestBed.inject(Router);
router.resetConfig([
{
path: 'simple',
component: SimpleCmp,
canActivate: [() => {
observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info;
return true;
}]
},
]);
@Component({
standalone: true,
imports: [RouterLink],
template: `<a #simpleLink [routerLink]="'/simple'" [info]="simpleLink"></a>`
})
class App {
}

const fixture = TestBed.createComponent(App);
fixture.autoDetectChanges();
const anchor = fixture.nativeElement.querySelector('a');
anchor.click();
await fixture.whenStable();

// An example use-case might be to pass the clicked link along with the navigation information
expect(observedInfo).toBeInstanceOf(HTMLAnchorElement);
});

it('should make transient navigation info available in redirect', async () => {
let observedInfo: unknown;
const router = TestBed.inject(Router);
Expand Down

0 comments on commit 365fb66

Please sign in to comment.