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
* 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
*
* 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.
*
* 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:
*

View File

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

View File

@ -1156,8 +1156,6 @@ describe('Integration', () => {
advance(fixture);
expect(location.path()).toEqual('/initial');
})));
// should not break the back button when trigger by initial navigation
});
describe('guards', () => {
@ -1380,6 +1378,11 @@ describe('Integration', () => {
return true;
}
},
{
provide: 'alwaysFalse',
useValue:
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
},
]
});
});
@ -1504,6 +1507,31 @@ describe('Integration', () => {
advance(fixture);
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', () => {