fix(router): should not create a route state if navigation is canceled (#12868)
Closes #12776
This commit is contained in:
parent
f79b320fc4
commit
773b31de8f
|
@ -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:
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue