feat(router): add navigationSource and restoredState to NavigationStart event (#21728)
Currently, NavigationStart there is no way to know if an navigation was triggered imperatively or via the location change. These two use cases should be handled differently for a variety of use cases (e.g., scroll position restoration). This PR adds a navigation source field and restored navigation id (passed to navigations triggered by a URL change). PR Close #21728
This commit is contained in:
parent
108fa15792
commit
3b7bab7d22
|
@ -14,6 +14,7 @@ import {LocationStrategy} from './location_strategy';
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface PopStateEvent {
|
export interface PopStateEvent {
|
||||||
pop?: boolean;
|
pop?: boolean;
|
||||||
|
state?: any;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
@ -56,6 +57,7 @@ export class Location {
|
||||||
this._subject.emit({
|
this._subject.emit({
|
||||||
'url': this.path(true),
|
'url': this.path(true),
|
||||||
'pop': true,
|
'pop': true,
|
||||||
|
'state': ev.state,
|
||||||
'type': ev.type,
|
'type': ev.type,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -103,16 +105,16 @@ export class Location {
|
||||||
* Changes the browsers URL to the normalized version of the given URL, and pushes a
|
* Changes the browsers URL to the normalized version of the given URL, and pushes a
|
||||||
* new item onto the platform's history.
|
* new item onto the platform's history.
|
||||||
*/
|
*/
|
||||||
go(path: string, query: string = ''): void {
|
go(path: string, query: string = '', state: any = null): void {
|
||||||
this._platformStrategy.pushState(null, '', path, query);
|
this._platformStrategy.pushState(state, '', path, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the browsers URL to the normalized version of the given URL, and replaces
|
* Changes the browsers URL to the normalized version of the given URL, and replaces
|
||||||
* the top item on the platform's history stack.
|
* the top item on the platform's history stack.
|
||||||
*/
|
*/
|
||||||
replaceState(path: string, query: string = ''): void {
|
replaceState(path: string, query: string = '', state: any = null): void {
|
||||||
this._platformStrategy.replaceState(null, '', path, query);
|
this._platformStrategy.replaceState(state, '', path, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -58,7 +58,10 @@ export const LOCATION_INITIALIZED = new InjectionToken<Promise<any>>('Location I
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export interface LocationChangeEvent { type: string; }
|
export interface LocationChangeEvent {
|
||||||
|
type: string;
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @experimental
|
* @experimental
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {ISubscription} from 'rxjs/Subscription';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpyLocation implements Location {
|
export class SpyLocation implements Location {
|
||||||
urlChanges: string[] = [];
|
urlChanges: string[] = [];
|
||||||
private _history: LocationState[] = [new LocationState('', '')];
|
private _history: LocationState[] = [new LocationState('', '', null)];
|
||||||
private _historyIndex: number = 0;
|
private _historyIndex: number = 0;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_subject: EventEmitter<any> = new EventEmitter();
|
_subject: EventEmitter<any> = new EventEmitter();
|
||||||
|
@ -34,6 +34,8 @@ export class SpyLocation implements Location {
|
||||||
|
|
||||||
path(): string { return this._history[this._historyIndex].path; }
|
path(): string { return this._history[this._historyIndex].path; }
|
||||||
|
|
||||||
|
private state(): string { return this._history[this._historyIndex].state; }
|
||||||
|
|
||||||
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
|
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
|
||||||
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
|
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
|
||||||
const currPath =
|
const currPath =
|
||||||
|
@ -60,13 +62,13 @@ export class SpyLocation implements Location {
|
||||||
return this._baseHref + url;
|
return this._baseHref + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
go(path: string, query: string = '') {
|
go(path: string, query: string = '', state: any = null) {
|
||||||
path = this.prepareExternalUrl(path);
|
path = this.prepareExternalUrl(path);
|
||||||
|
|
||||||
if (this._historyIndex > 0) {
|
if (this._historyIndex > 0) {
|
||||||
this._history.splice(this._historyIndex + 1);
|
this._history.splice(this._historyIndex + 1);
|
||||||
}
|
}
|
||||||
this._history.push(new LocationState(path, query));
|
this._history.push(new LocationState(path, query, state));
|
||||||
this._historyIndex = this._history.length - 1;
|
this._historyIndex = this._history.length - 1;
|
||||||
|
|
||||||
const locationState = this._history[this._historyIndex - 1];
|
const locationState = this._history[this._historyIndex - 1];
|
||||||
|
@ -79,7 +81,7 @@ export class SpyLocation implements Location {
|
||||||
this._subject.emit({'url': url, 'pop': false});
|
this._subject.emit({'url': url, 'pop': false});
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceState(path: string, query: string = '') {
|
replaceState(path: string, query: string = '', state: any = null) {
|
||||||
path = this.prepareExternalUrl(path);
|
path = this.prepareExternalUrl(path);
|
||||||
|
|
||||||
const history = this._history[this._historyIndex];
|
const history = this._history[this._historyIndex];
|
||||||
|
@ -89,6 +91,7 @@ export class SpyLocation implements Location {
|
||||||
|
|
||||||
history.path = path;
|
history.path = path;
|
||||||
history.query = query;
|
history.query = query;
|
||||||
|
history.state = state;
|
||||||
|
|
||||||
const url = path + (query.length > 0 ? ('?' + query) : '');
|
const url = path + (query.length > 0 ? ('?' + query) : '');
|
||||||
this.urlChanges.push('replace: ' + url);
|
this.urlChanges.push('replace: ' + url);
|
||||||
|
@ -97,14 +100,14 @@ export class SpyLocation implements Location {
|
||||||
forward() {
|
forward() {
|
||||||
if (this._historyIndex < (this._history.length - 1)) {
|
if (this._historyIndex < (this._history.length - 1)) {
|
||||||
this._historyIndex++;
|
this._historyIndex++;
|
||||||
this._subject.emit({'url': this.path(), 'pop': true});
|
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
if (this._historyIndex > 0) {
|
if (this._historyIndex > 0) {
|
||||||
this._historyIndex--;
|
this._historyIndex--;
|
||||||
this._subject.emit({'url': this.path(), 'pop': true});
|
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,10 +121,5 @@ export class SpyLocation implements Location {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocationState {
|
class LocationState {
|
||||||
path: string;
|
constructor(public path: string, public query: string, public state: any) {}
|
||||||
query: string;
|
|
||||||
constructor(path: string, query: string) {
|
|
||||||
this.path = path;
|
|
||||||
this.query = query;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,9 @@ export class ServerPlatformLocation implements PlatformLocation {
|
||||||
}
|
}
|
||||||
(this as{hash: string}).hash = value;
|
(this as{hash: string}).hash = value;
|
||||||
const newUrl = this.url;
|
const newUrl = this.url;
|
||||||
scheduleMicroTask(
|
scheduleMicroTask(() => this._hashUpdate.next({
|
||||||
() => this._hashUpdate.next({ type: 'hashchange', oldUrl, newUrl } as LocationChangeEvent));
|
type: 'hashchange', state: null, oldUrl, newUrl
|
||||||
|
} as LocationChangeEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceState(state: any, title: string, newUrl: string): void {
|
replaceState(state: any, title: string, newUrl: string): void {
|
||||||
|
|
|
@ -9,6 +9,16 @@
|
||||||
import {Route} from './config';
|
import {Route} from './config';
|
||||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Identifies the trigger of the navigation.
|
||||||
|
*
|
||||||
|
* * 'imperative'--triggered by `router.navigateByUrl` or `router.navigate`.
|
||||||
|
* * 'popstate'--triggered by a popstate event
|
||||||
|
* * 'hashchange'--triggered by a hashchange event
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export type NavigationTrigger = 'imperative' | 'popstate' | 'hashchange';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific
|
* @whatItDoes Base for events the Router goes through, as opposed to events tied to a specific
|
||||||
|
@ -42,6 +52,43 @@ export class RouterEvent {
|
||||||
* @stable
|
* @stable
|
||||||
*/
|
*/
|
||||||
export class NavigationStart extends RouterEvent {
|
export class NavigationStart extends RouterEvent {
|
||||||
|
/**
|
||||||
|
* Identifies the trigger of the navigation.
|
||||||
|
*
|
||||||
|
* * 'imperative'--triggered by `router.navigateByUrl` or `router.navigate`.
|
||||||
|
* * 'popstate'--triggered by a popstate event
|
||||||
|
* * 'hashchange'--triggered by a hashchange event
|
||||||
|
*/
|
||||||
|
navigationTrigger?: 'imperative'|'popstate'|'hashchange';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This contains the navigation id that pushed the history record that the router navigates
|
||||||
|
* back to. This is not null only when the navigation is triggered by a popstate event.
|
||||||
|
*
|
||||||
|
* The router assigns a navigationId to every router transition/navigation. Even when the user
|
||||||
|
* clicks on the back button in the browser, a new navigation id will be created. So from
|
||||||
|
* the perspective of the router, the router never "goes back". By using the `restoredState`
|
||||||
|
* and its navigationId, you can implement behavior that differentiates between creating new
|
||||||
|
* states
|
||||||
|
* and popstate events. In the latter case you can restore some remembered state (e.g., scroll
|
||||||
|
* position).
|
||||||
|
*/
|
||||||
|
restoredState?: {navigationId: number}|null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** @docsNotRequired */
|
||||||
|
id: number,
|
||||||
|
/** @docsNotRequired */
|
||||||
|
url: string,
|
||||||
|
/** @docsNotRequired */
|
||||||
|
navigationTrigger: 'imperative'|'popstate'|'hashchange' = 'imperative',
|
||||||
|
/** @docsNotRequired */
|
||||||
|
restoredState: {navigationId: number}|null = null) {
|
||||||
|
super(id, url);
|
||||||
|
this.navigationTrigger = navigationTrigger;
|
||||||
|
this.restoredState = restoredState;
|
||||||
|
}
|
||||||
|
|
||||||
/** @docsNotRequired */
|
/** @docsNotRequired */
|
||||||
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
|
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {applyRedirects} from './apply_redirects';
|
||||||
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config';
|
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, validateConfig} from './config';
|
||||||
import {createRouterState} from './create_router_state';
|
import {createRouterState} from './create_router_state';
|
||||||
import {createUrlTree} from './create_url_tree';
|
import {createUrlTree} from './create_url_tree';
|
||||||
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
|
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
|
||||||
import {PreActivation} from './pre_activation';
|
import {PreActivation} from './pre_activation';
|
||||||
import {recognize} from './recognize';
|
import {recognize} from './recognize';
|
||||||
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
import {DefaultRouteReuseStrategy, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
|
@ -164,8 +164,6 @@ function defaultErrorHandler(error: any): any {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavigationSource = 'imperative' | 'popstate' | 'hashchange';
|
|
||||||
|
|
||||||
type NavigationParams = {
|
type NavigationParams = {
|
||||||
id: number,
|
id: number,
|
||||||
rawUrl: UrlTree,
|
rawUrl: UrlTree,
|
||||||
|
@ -173,7 +171,8 @@ type NavigationParams = {
|
||||||
resolve: any,
|
resolve: any,
|
||||||
reject: any,
|
reject: any,
|
||||||
promise: Promise<boolean>,
|
promise: Promise<boolean>,
|
||||||
source: NavigationSource,
|
source: NavigationTrigger,
|
||||||
|
state: {navigationId: number} | null
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -223,6 +222,7 @@ export class Router {
|
||||||
* Indicates if at least one navigation happened.
|
* Indicates if at least one navigation happened.
|
||||||
*/
|
*/
|
||||||
navigated: boolean = false;
|
navigated: boolean = false;
|
||||||
|
private lastSuccessfulId: number = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by RouterModule. This allows us to
|
* Used by RouterModule. This allows us to
|
||||||
|
@ -311,8 +311,12 @@ export class Router {
|
||||||
if (!this.locationSubscription) {
|
if (!this.locationSubscription) {
|
||||||
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
||||||
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
||||||
const source: NavigationSource = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
|
const source: NavigationTrigger = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
|
||||||
setTimeout(() => { this.scheduleNavigation(rawUrlTree, source, {replaceUrl: true}); }, 0);
|
const state = change.state && change.state.navigationId ?
|
||||||
|
{navigationId: change.state.navigationId} :
|
||||||
|
null;
|
||||||
|
setTimeout(
|
||||||
|
() => { this.scheduleNavigation(rawUrlTree, source, state, {replaceUrl: true}); }, 0);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -341,6 +345,7 @@ export class Router {
|
||||||
validateConfig(config);
|
validateConfig(config);
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.navigated = false;
|
this.navigated = false;
|
||||||
|
this.lastSuccessfulId = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @docsNotRequired */
|
/** @docsNotRequired */
|
||||||
|
@ -449,7 +454,7 @@ export class Router {
|
||||||
const urlTree = url instanceof UrlTree ? url : this.parseUrl(url);
|
const urlTree = url instanceof UrlTree ? 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', extras);
|
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -522,8 +527,9 @@ export class Router {
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNavigation(rawUrl: UrlTree, source: NavigationSource, extras: NavigationExtras):
|
private scheduleNavigation(
|
||||||
Promise<boolean> {
|
rawUrl: UrlTree, source: NavigationTrigger, state: {navigationId: number}|null,
|
||||||
|
extras: NavigationExtras): Promise<boolean> {
|
||||||
const lastNavigation = this.navigations.value;
|
const lastNavigation = this.navigations.value;
|
||||||
|
|
||||||
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
||||||
|
@ -558,21 +564,22 @@ export class Router {
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = ++this.navigationId;
|
const id = ++this.navigationId;
|
||||||
this.navigations.next({id, source, rawUrl, extras, resolve, reject, promise});
|
this.navigations.next({id, source, state, rawUrl, extras, resolve, reject, promise});
|
||||||
|
|
||||||
// Make sure that the error is propagated even though `processNavigations` catch
|
// Make sure that the error is propagated even though `processNavigations` catch
|
||||||
// handler does not rethrow
|
// handler does not rethrow
|
||||||
return promise.catch((e: any) => Promise.reject(e));
|
return promise.catch((e: any) => Promise.reject(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject}: NavigationParams):
|
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject, source,
|
||||||
void {
|
state}: NavigationParams): void {
|
||||||
const url = this.urlHandlingStrategy.extract(rawUrl);
|
const url = this.urlHandlingStrategy.extract(rawUrl);
|
||||||
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
|
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
|
||||||
|
|
||||||
if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
|
if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
|
||||||
this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
|
this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
|
||||||
(this.events as Subject<Event>).next(new NavigationStart(id, this.serializeUrl(url)));
|
(this.events as Subject<Event>)
|
||||||
|
.next(new NavigationStart(id, this.serializeUrl(url), source, state));
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(
|
.then(
|
||||||
(_) => this.runNavigate(
|
(_) => this.runNavigate(
|
||||||
|
@ -584,7 +591,8 @@ export class Router {
|
||||||
} else if (
|
} else if (
|
||||||
urlTransition && this.rawUrlTree &&
|
urlTransition && this.rawUrlTree &&
|
||||||
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
|
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
|
||||||
(this.events as Subject<Event>).next(new NavigationStart(id, this.serializeUrl(url)));
|
(this.events as Subject<Event>)
|
||||||
|
.next(new NavigationStart(id, this.serializeUrl(url), source, state));
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(
|
.then(
|
||||||
(_) => this.runNavigate(
|
(_) => this.runNavigate(
|
||||||
|
@ -727,9 +735,9 @@ export class Router {
|
||||||
if (!skipLocationChange) {
|
if (!skipLocationChange) {
|
||||||
const path = this.urlSerializer.serialize(this.rawUrlTree);
|
const path = this.urlSerializer.serialize(this.rawUrlTree);
|
||||||
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
|
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
|
||||||
this.location.replaceState(path);
|
this.location.replaceState(path, '', {navigationId: id});
|
||||||
} else {
|
} else {
|
||||||
this.location.go(path);
|
this.location.go(path, '', {navigationId: id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,6 +751,7 @@ export class Router {
|
||||||
() => {
|
() => {
|
||||||
if (navigationIsSuccessful) {
|
if (navigationIsSuccessful) {
|
||||||
this.navigated = true;
|
this.navigated = true;
|
||||||
|
this.lastSuccessfulId = id;
|
||||||
(this.events as Subject<Event>)
|
(this.events as Subject<Event>)
|
||||||
.next(new NavigationEnd(
|
.next(new NavigationEnd(
|
||||||
id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
|
id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
|
||||||
|
@ -784,7 +793,8 @@ export class Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetUrlToCurrentUrlTree(): void {
|
private resetUrlToCurrentUrlTree(): void {
|
||||||
this.location.replaceState(this.urlSerializer.serialize(this.rawUrlTree));
|
this.location.replaceState(
|
||||||
|
this.urlSerializer.serialize(this.rawUrlTree), '', {navigationId: this.lastSuccessfulId});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,28 @@ describe('Integration', () => {
|
||||||
]);
|
]);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
it('should set the restoredState to null when executing imperative navigations',
|
||||||
|
fakeAsync(inject([Router], (router: Router) => {
|
||||||
|
router.resetConfig([
|
||||||
|
{path: '', component: SimpleCmp},
|
||||||
|
{path: 'simple', component: SimpleCmp},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
let event: NavigationStart;
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof NavigationStart) {
|
||||||
|
event = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.navigateByUrl('/simple');
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(event !.navigationTrigger).toEqual('imperative');
|
||||||
|
expect(event !.restoredState).toEqual(null);
|
||||||
|
})));
|
||||||
|
|
||||||
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([
|
||||||
|
@ -466,21 +488,34 @@ describe('Integration', () => {
|
||||||
[{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}]
|
[{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}]
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
let event: NavigationStart;
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof NavigationStart) {
|
||||||
|
event = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.navigateByUrl('/team/33/simple');
|
router.navigateByUrl('/team/33/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/33/simple');
|
expect(location.path()).toEqual('/team/33/simple');
|
||||||
|
const simpleNavStart = event !;
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor');
|
router.navigateByUrl('/team/22/user/victor');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
const userVictorNavStart = event !;
|
||||||
|
|
||||||
|
|
||||||
location.back();
|
location.back();
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/33/simple');
|
expect(location.path()).toEqual('/team/33/simple');
|
||||||
|
expect(event !.navigationTrigger).toEqual('hashchange');
|
||||||
|
expect(event !.restoredState !.navigationId).toEqual(simpleNavStart.id);
|
||||||
|
|
||||||
location.forward();
|
location.forward();
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/22/user/victor');
|
expect(location.path()).toEqual('/team/22/user/victor');
|
||||||
|
expect(event !.navigationTrigger).toEqual('hashchange');
|
||||||
|
expect(event !.restoredState !.navigationId).toEqual(userVictorNavStart.id);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
it('should navigate to the same url when config changes',
|
it('should navigate to the same url when config changes',
|
||||||
|
@ -868,6 +903,40 @@ describe('Integration', () => {
|
||||||
expect(locationUrlBeforeEmittingError).toEqual('/simple');
|
expect(locationUrlBeforeEmittingError).toEqual('/simple');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should reset the url with the right state when navigation errors', fakeAsync(() => {
|
||||||
|
const router: Router = TestBed.get(Router);
|
||||||
|
const location: SpyLocation = TestBed.get(Location);
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([
|
||||||
|
{path: 'simple1', component: SimpleCmp}, {path: 'simple2', component: SimpleCmp},
|
||||||
|
{path: 'throwing', component: ThrowingCmp}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
let event: NavigationStart;
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof NavigationStart) {
|
||||||
|
event = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.navigateByUrl('/simple1');
|
||||||
|
advance(fixture);
|
||||||
|
const simple1NavStart = event !;
|
||||||
|
|
||||||
|
router.navigateByUrl('/throwing').catch(() => null);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
router.navigateByUrl('/simple2');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
location.back();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(event !.restoredState !.navigationId).toEqual(simple1NavStart.id);
|
||||||
|
}));
|
||||||
|
|
||||||
it('should not trigger another navigation when resetting the url back due to a NavigationError',
|
it('should not trigger another navigation when resetting the url back due to a NavigationError',
|
||||||
fakeAsync(() => {
|
fakeAsync(() => {
|
||||||
const router = TestBed.get(Router);
|
const router = TestBed.get(Router);
|
||||||
|
|
|
@ -182,12 +182,12 @@ export declare class Location {
|
||||||
constructor(platformStrategy: LocationStrategy);
|
constructor(platformStrategy: LocationStrategy);
|
||||||
back(): void;
|
back(): void;
|
||||||
forward(): void;
|
forward(): void;
|
||||||
go(path: string, query?: string): void;
|
go(path: string, query?: string, state?: any): void;
|
||||||
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
||||||
normalize(url: string): string;
|
normalize(url: string): string;
|
||||||
path(includeHash?: boolean): string;
|
path(includeHash?: boolean): string;
|
||||||
prepareExternalUrl(url: string): string;
|
prepareExternalUrl(url: string): string;
|
||||||
replaceState(path: string, query?: string): void;
|
replaceState(path: string, query?: string, state?: any): void;
|
||||||
subscribe(onNext: (value: PopStateEvent) => void, onThrow?: ((exception: any) => void) | null, onReturn?: (() => void) | null): ISubscription;
|
subscribe(onNext: (value: PopStateEvent) => void, onThrow?: ((exception: any) => void) | null, onReturn?: (() => void) | null): ISubscription;
|
||||||
static joinWithSlash(start: string, end: string): string;
|
static joinWithSlash(start: string, end: string): string;
|
||||||
static normalizeQueryParams(params: string): string;
|
static normalizeQueryParams(params: string): string;
|
||||||
|
@ -199,6 +199,7 @@ export declare const LOCATION_INITIALIZED: InjectionToken<Promise<any>>;
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface LocationChangeEvent {
|
export interface LocationChangeEvent {
|
||||||
|
state: any;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,6 +416,7 @@ export declare enum Plural {
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export interface PopStateEvent {
|
export interface PopStateEvent {
|
||||||
pop?: boolean;
|
pop?: boolean;
|
||||||
|
state?: any;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,12 @@ export declare class SpyLocation implements Location {
|
||||||
urlChanges: string[];
|
urlChanges: string[];
|
||||||
back(): void;
|
back(): void;
|
||||||
forward(): void;
|
forward(): void;
|
||||||
go(path: string, query?: string): void;
|
go(path: string, query?: string, state?: any): void;
|
||||||
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
||||||
normalize(url: string): string;
|
normalize(url: string): string;
|
||||||
path(): string;
|
path(): string;
|
||||||
prepareExternalUrl(url: string): string;
|
prepareExternalUrl(url: string): string;
|
||||||
replaceState(path: string, query?: string): void;
|
replaceState(path: string, query?: string, state?: any): void;
|
||||||
setBaseHref(url: string): void;
|
setBaseHref(url: string): void;
|
||||||
setInitialPath(url: string): void;
|
setInitialPath(url: string): void;
|
||||||
simulateHashChange(pathname: string): void;
|
simulateHashChange(pathname: string): void;
|
||||||
|
|
|
@ -208,6 +208,17 @@ export interface NavigationExtras {
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare class NavigationStart extends RouterEvent {
|
export declare class NavigationStart extends RouterEvent {
|
||||||
|
navigationTrigger?: 'imperative' | 'popstate' | 'hashchange';
|
||||||
|
restoredState?: {
|
||||||
|
navigationId: number;
|
||||||
|
} | null;
|
||||||
|
constructor(
|
||||||
|
id: number,
|
||||||
|
url: string,
|
||||||
|
navigationTrigger?: 'imperative' | 'popstate' | 'hashchange',
|
||||||
|
restoredState?: {
|
||||||
|
navigationId: number;
|
||||||
|
} | null);
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue