fix(router): should not create a route state if navigation is canceled (#12868)

Closes #12776
This commit is contained in:
Victor Savkin 2016-11-15 19:00:20 -08:00 committed by Victor Berchet
parent f79b320fc4
commit 773b31de8f
3 changed files with 94 additions and 49 deletions

View File

@ -154,6 +154,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with * When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with
* the user component in it. * the user component in it.
* *
* An empty path route inherits its parent's params and data. This is because it cannot have its
* own params, and, as a result, it often uses its parent's params and data as its own.
*
* ### Matching Strategy * ### Matching Strategy
* *
* By default the router will look at what is left in the url, and check if it starts with * By default the router will look at what is left in the url, and check if it starts with
@ -219,7 +222,8 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* has to have the primary and aux outlets defined. * has to have the primary and aux outlets defined.
* *
* The router will also merge the `params`, `data`, and `resolve` of the componentless parent into * The router will also merge the `params`, `data`, and `resolve` of the componentless parent into
* the `params`, `data`, and `resolve` of the children. * the `params`, `data`, and `resolve` of the children. This is done because there is no component
* that can inject the activated route of the componentless parent.
* *
* This is especially useful when child components are defined as follows: * This is especially useful when child components are defined as follows:
* *

View File

@ -623,7 +623,8 @@ export class Router {
Promise.resolve() Promise.resolve()
.then( .then(
(_) => this.runNavigate( (_) => this.runNavigate(
url, rawUrl, false, false, id, createEmptyState(url, this.rootComponentType))) url, rawUrl, false, false, id,
createEmptyState(url, this.rootComponentType).snapshot))
.then(resolve, reject); .then(resolve, reject);
} else { } else {
@ -634,7 +635,7 @@ export class Router {
private runNavigate( private runNavigate(
url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number, precreatedState: RouterState): Promise<boolean> { id: number, precreatedState: RouterStateSnapshot): Promise<boolean> {
if (id !== this.navigationId) { if (id !== this.navigationId) {
this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
this.routerEvents.next(new NavigationCancel( this.routerEvents.next(new NavigationCancel(
@ -644,68 +645,80 @@ export class Router {
} }
return new Promise((resolvePromise, rejectPromise) => { return new Promise((resolvePromise, rejectPromise) => {
let state: RouterState; // create an observable of the url and route state snapshot
let navigationIsSuccessful: boolean; // this operation do not result in any side effects
let preActivation: PreActivation; let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>;
let appliedUrl: UrlTree;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
let routerState$: any;
if (!precreatedState) { if (!precreatedState) {
const redirectsApplied$ = const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, url, this.config); applyRedirects(this.injector, this.configLoader, url, this.config);
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
appliedUrl = u; return map.call(
return recognize( recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)),
}); (snapshot: any) => {
const emitRecognzied$ = this.routerEvents.next(new RoutesRecognized(
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot));
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
newRouterStateSnapshot));
return newRouterStateSnapshot;
});
routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { return {appliedUrl, snapshot};
return createRouterState(routerStateSnapshot, this.currentRouterState); });
}); });
} else { } else {
appliedUrl = url; urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
routerState$ = of (precreatedState);
} }
const preactivation$ = map.call(routerState$, (newState: RouterState) => {
state = newState; // run preactivation: guards and data resolvers
let preActivation: PreActivation;
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
preActivation = preActivation =
new PreActivation(state.snapshot, this.currentRouterState.snapshot, this.injector); new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
preActivation.traverse(this.outletMap); preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
}); });
const preactivation2$ = mergeMap.call(preactivation$, () => { const preactivationCheckGuards =
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
if (this.navigationId !== id) return of (false);
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
if (this.navigationId !== id) return of (false); if (this.navigationId !== id) return of (false);
return preActivation.checkGuards(); if (p.shouldActivate) {
}); return map.call(preActivation.resolveData(), () => p);
const resolveData$ = mergeMap.call(preactivation2$, (shouldActivate: boolean) => {
if (this.navigationId !== id) return of (false);
if (shouldActivate) {
return map.call(preActivation.resolveData(), () => shouldActivate);
} else { } else {
return of (shouldActivate); return of (p);
} }
}); });
resolveData$
.forEach((shouldActivate: boolean) => { // create router state
// this operation has side effects => route state is being affected
const routerState$ =
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state = createRouterState(snapshot, this.currentRouterState);
return {appliedUrl, state, shouldActivate};
} else {
return {appliedUrl, state: null, shouldActivate};
}
});
// applied the new router state
// this operation has side effects
let navigationIsSuccessful: boolean;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
routerState$
.forEach(({appliedUrl, state, shouldActivate}: any) => {
if (!shouldActivate || id !== this.navigationId) { if (!shouldActivate || id !== this.navigationId) {
navigationIsSuccessful = false; navigationIsSuccessful = false;
return; return;
@ -733,8 +746,8 @@ export class Router {
() => { () => {
this.navigated = true; this.navigated = true;
if (navigationIsSuccessful) { if (navigationIsSuccessful) {
this.routerEvents.next( this.routerEvents.next(new NavigationEnd(
new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(appliedUrl))); id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
resolvePromise(true); resolvePromise(true);
} else { } else {
this.resetUrlToCurrentUrlTree(); this.resetUrlToCurrentUrlTree();

View File

@ -1156,8 +1156,6 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/initial'); expect(location.path()).toEqual('/initial');
}))); })));
// should not break the back button when trigger by initial navigation
}); });
describe('guards', () => { describe('guards', () => {
@ -1380,6 +1378,11 @@ describe('Integration', () => {
return true; return true;
} }
}, },
{
provide: 'alwaysFalse',
useValue:
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
},
] ]
}); });
}); });
@ -1504,6 +1507,31 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/33/user/fedor'); expect(location.path()).toEqual('/team/33/user/fedor');
}))); })));
it('should not create a route state if navigation is canceled',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{
path: 'main',
component: TeamCmp,
children: [
{path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']},
{path: 'component2', component: SimpleCmp}
]
}]);
router.navigateByUrl('/main/component1');
advance(fixture);
router.navigateByUrl('/main/component2');
advance(fixture);
const teamCmp = fixture.debugElement.children[1].componentInstance;
expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1');
expect(location.path()).toEqual('/main/component1');
})));
}); });
describe('should work when given a class', () => { describe('should work when given a class', () => {