Skip to content

Commit

Permalink
feat(router): Add info property to NavigationExtras (#53303)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
atscott authored and dylhunn committed Dec 6, 2023
1 parent a9872cc commit 5c1d441
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 0 deletions.
1 change: 1 addition & 0 deletions goldens/public-api/router/index.md
Expand Up @@ -409,6 +409,7 @@ export interface Navigation {

// @public
export interface NavigationBehaviorOptions {
readonly info?: unknown;
onSameUrlNavigation?: OnSameUrlNavigation;
replaceUrl?: boolean;
skipLocationChange?: boolean;
Expand Down
26 changes: 26 additions & 0 deletions packages/router/src/models.ts
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions packages/router/src/router.ts
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions packages/router/test/integration.spec.ts
Expand Up @@ -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([{
Expand Down

0 comments on commit 5c1d441

Please sign in to comment.