From 5c1d4410298e20cb03d7a1ddf7931538b6a181b4 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Thu, 30 Nov 2023 15:17:15 -0800 Subject: [PATCH] feat(router): Add info property to `NavigationExtras` (#53303) This commit adds a property to the navigation options to allow developers to provide transient navigation info that is available for the duration of the navigation. This information can be retrieved at any time with `Router.getCurrentNavigation()!.extras.info`. Previously, developers were forced to either create a service to hold information like this or put it on the `state` object, which gets persisted to the session history. This feature was partially motivated by the [Navigation API](https://github.com/WICG/navigation-api#example-using-info) and would be something we would want/need to have feature parity if/when the Router supports managing navigations with that instead of `History`. PR Close #53303 --- goldens/public-api/router/index.md | 1 + packages/router/src/models.ts | 26 +++++++++++++++ packages/router/src/router.ts | 2 ++ packages/router/test/integration.spec.ts | 42 ++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/goldens/public-api/router/index.md b/goldens/public-api/router/index.md index c6d6291d5a00b..9700f6315dee3 100644 --- a/goldens/public-api/router/index.md +++ b/goldens/public-api/router/index.md @@ -409,6 +409,7 @@ export interface Navigation { // @public export interface NavigationBehaviorOptions { + readonly info?: unknown; onSameUrlNavigation?: OnSameUrlNavigation; replaceUrl?: boolean; skipLocationChange?: boolean; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index d73f4035cede6..14c039054ddf5 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -1269,4 +1269,30 @@ export interface NavigationBehaviorOptions { * */ state?: {[k: string]: any}; + + /** + * Use this to convey transient information about this particular navigation, such as how it + * happened. In this way, it's different from the persisted value `state` that will be set to + * `history.state`. This object is assigned directly to the Router's current `Navigation` + * (it is not copied or cloned), so it should be mutated with caution. + * + * One example of how this might be used is to trigger different single-page navigation animations + * depending on how a certain route was reached. For example, consider a photo gallery app, where + * you can reach the same photo URL and state via various routes: + * + * - Clicking on it in a gallery view + * - Clicking + * - "next" or "previous" when viewing another photo in the album + * - Etc. + * + * Each of these wants a different animation at navigate time. This information doesn't make sense + * to store in the persistent URL or history entry state, but it's still important to communicate + * from the rest of the application, into the router. + * + * This information could be used in coordination with the View Transitions feature and the + * `onViewTransitionCreated` callback. The information might be used in the callback to set + * classes on the document in order to control the transition animations and remove the classes + * when the transition has finished animating. + */ + readonly info?: unknown; } diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 79d73a0423d3a..6dc522866bbaf 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -190,6 +190,8 @@ export class Router { const mergedTree = this.urlHandlingStrategy.merge(e.url, currentTransition.currentRawUrl); const extras = { + // Persist transient navigation info from the original navigation request. + info: currentTransition.extras.info, skipLocationChange: currentTransition.extras.skipLocationChange, // The URL is already updated at this point if we have 'eager' URL // updates or if the navigation was triggered by the browser (back diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index bf1ce1ce890fa..c3f0a4fb9ee54 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -142,6 +142,48 @@ describe('Integration', () => { expectEvents(events, []); }); + it('should set transient navigation info', async () => { + let observedInfo: unknown; + const router = TestBed.inject(Router); + router.resetConfig([ + { + path: 'simple', + component: SimpleCmp, + canActivate: [() => { + observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; + return true; + }] + }, + ]); + + await router.navigateByUrl('/simple', {info: 'navigation info'}); + expect(observedInfo).toEqual('navigation info'); + }); + + it('should make transient navigation info available in redirect', async () => { + let observedInfo: unknown; + const router = TestBed.inject(Router); + router.resetConfig([ + { + path: 'redirect', + component: SimpleCmp, + canActivate: [() => coreInject(Router).parseUrl('/simple')] + }, + { + path: 'simple', + component: SimpleCmp, + canActivate: [() => { + observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; + return true; + }] + }, + ]); + + await router.navigateByUrl('/redirect', {info: 'navigation info'}); + expect(observedInfo).toBe('navigation info'); + expect(router.url).toEqual('/simple'); + }); + it('should ignore empty paths in relative links', fakeAsync(inject([Router], (router: Router) => { router.resetConfig([{