feat(router): allow passing state to NavigationExtras (#27198)

This value will get written to the `history.state` entry.

FW-613 (related)

PR Close #27198
This commit is contained in:
Jason Aden 2018-11-28 16:18:22 -08:00 committed by Igor Minar
parent 26842491c6
commit 67f4a5d4bd
2 changed files with 56 additions and 11 deletions

View File

@ -145,6 +145,18 @@ export interface NavigationExtras {
* ``` * ```
*/ */
replaceUrl?: boolean; replaceUrl?: boolean;
/**
* State passed to any navigation. This value will be accessible through the `extras` object
* returned from `router.getCurrentTransition()` while a navigation is executing. Once a
* navigation completes, this value will be written to `history.state` when the `location.go`
* or `location.replaceState` method is called before activating of this route. Note that
* `history.state` will not pass an object equality test because the `navigationId` will be
* added to the state before being written.
*
* While `history.state` can accept any type of value, because the router adds the `navigationId`
* on each navigation, the `state` must always be an object.
*/
state?: {[k: string]: any};
} }
/** /**
@ -541,7 +553,7 @@ export class Router {
}), }),
// --- AFTER PREACTIVATION --- // --- AFTER PREACTIVATION ---
switchTap(t => { switchTap((t: NavigationTransition) => {
const { const {
targetSnapshot, targetSnapshot,
id: navigationId, id: navigationId,
@ -558,7 +570,7 @@ export class Router {
}); });
}), }),
map(t => { map((t: NavigationTransition) => {
const targetRouterState = createRouterState( const targetRouterState = createRouterState(
this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState); this.routeReuseStrategy, t.targetSnapshot !, t.currentRouterState);
return ({...t, targetRouterState}); return ({...t, targetRouterState});
@ -569,14 +581,14 @@ export class Router {
activation, we need to update router properties storing the current URL and the activation, we need to update router properties storing the current URL and the
RouterState, as well as updated the browser URL. All this should happen *before* RouterState, as well as updated the browser URL. All this should happen *before*
activating. */ activating. */
tap(t => { tap((t: NavigationTransition) => {
this.currentUrlTree = t.urlAfterRedirects; this.currentUrlTree = t.urlAfterRedirects;
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl); this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, t.rawUrl);
(this as{routerState: RouterState}).routerState = t.targetRouterState !; (this as{routerState: RouterState}).routerState = t.targetRouterState !;
if (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) { if (this.urlUpdateStrategy === 'deferred' && !t.extras.skipLocationChange) {
this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id); this.setBrowserUrl(this.rawUrlTree, !!t.extras.replaceUrl, t.id, t.extras.state);
} }
}), }),
@ -684,8 +696,9 @@ export class Router {
// Navigations coming from Angular router have a navigationId state property. When this // Navigations coming from Angular router have a navigationId state property. When this
// exists, restore the state. // exists, restore the state.
const state = change.state && change.state.navigationId ? change.state : null; const state = change.state && change.state.navigationId ? change.state : null;
setTimeout( setTimeout(() => {
() => { this.scheduleNavigation(rawUrlTree, source, state, {replaceUrl: true}); }, 0); this.scheduleNavigation(rawUrlTree, source, state, null, {replaceUrl: true});
}, 0);
}); });
} }
} }
@ -836,7 +849,7 @@ export class Router {
const urlTree = isUrlTree(url) ? url : this.parseUrl(url); const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree); const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
return this.scheduleNavigation(mergedTree, 'imperative', null, extras); return this.scheduleNavigation(mergedTree, 'imperative', null, extras.state || null, extras);
} }
/** /**
@ -862,6 +875,11 @@ export class Router {
* The first parameter of `navigate()` is a delta to be applied to the current URL * The first parameter of `navigate()` is a delta to be applied to the current URL
* or the one provided in the `relativeTo` property of the second parameter (the * or the one provided in the `relativeTo` property of the second parameter (the
* `NavigationExtras`). * `NavigationExtras`).
*
* In order to affect this browser's `history.state` entry, the `state`
* parameter can be passed. This must be an object because the router
* will add the `navigationId` property to this object before creating
* the new history item.
*/ */
navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}): navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}):
Promise<boolean> { Promise<boolean> {
@ -918,7 +936,7 @@ export class Router {
private scheduleNavigation( private scheduleNavigation(
rawUrl: UrlTree, source: NavigationTrigger, restoredState: {navigationId: number}|null, rawUrl: UrlTree, source: NavigationTrigger, restoredState: {navigationId: number}|null,
extras: NavigationExtras): Promise<boolean> { futureState: {[key: string]: any}|null, extras: NavigationExtras): Promise<boolean> {
const lastNavigation = this.getTransition(); const lastNavigation = this.getTransition();
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl), // If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
// and that navigation results in 'replaceState' that leads to the same URL, // and that navigation results in 'replaceState' that leads to the same URL,
@ -967,12 +985,14 @@ export class Router {
return promise.catch((e: any) => { return Promise.reject(e); }); return promise.catch((e: any) => { return Promise.reject(e); });
} }
private setBrowserUrl(url: UrlTree, replaceUrl: boolean, id: number) { private setBrowserUrl(
url: UrlTree, replaceUrl: boolean, id: number, state?: {[key: string]: any}) {
const path = this.urlSerializer.serialize(url); const path = this.urlSerializer.serialize(url);
state = state || {};
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) { if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
this.location.replaceState(path, '', {navigationId: id}); this.location.replaceState(path, '', {...state, navigationId: id});
} else { } else {
this.location.go(path, '', {navigationId: id}); this.location.go(path, '', {...state, navigationId: id});
} }
} }

View File

@ -134,6 +134,31 @@ describe('Integration', () => {
expect(event !.restoredState).toEqual(null); expect(event !.restoredState).toEqual(null);
}))); })));
it('should set history.state if passed using imperative navigation',
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
router.resetConfig([
{path: '', component: SimpleCmp},
{path: 'simple', component: SimpleCmp},
]);
const fixture = createRoot(router, RootCmp);
// let transition: NavigationTransitionx = null !;
// router.events.subscribe(e => {
// if (e instanceof NavigationStart) {
// transition = router.getCurrentTransition();
// }
// });
router.navigateByUrl('/simple', {state: {foo: 'bar'}});
tick();
const history = (location as any)._history;
expect(history[history.length - 1].state.foo).toBe('bar');
expect(history[history.length - 1].state).toEqual({foo: 'bar', navigationId: history.length});
// expect(transition.state).toBeDefined();
// expect(transition.state).toEqual({foo: 'bar'});
})));
it('should not pollute browser history when replaceUrl is set to true', it('should not pollute browser history when replaceUrl is set to true',
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => { fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
router.resetConfig([ router.resetConfig([