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
|
* 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:
|
||||||
*
|
*
|
||||||
|
@ -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$ =
|
|
||||||
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => {
|
|
||||||
this.routerEvents.next(new RoutesRecognized(
|
this.routerEvents.next(new RoutesRecognized(
|
||||||
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
|
id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot));
|
||||||
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);
|
if (this.navigationId !== id) return of (false);
|
||||||
|
|
||||||
return preActivation.checkGuards();
|
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
|
||||||
|
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolveData$ = mergeMap.call(preactivation2$, (shouldActivate: boolean) => {
|
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
|
||||||
if (this.navigationId !== id) return of (false);
|
if (this.navigationId !== id) return of (false);
|
||||||
|
|
||||||
if (shouldActivate) {
|
if (p.shouldActivate) {
|
||||||
return map.call(preActivation.resolveData(), () => shouldActivate);
|
return map.call(preActivation.resolveData(), () => p);
|
||||||
} 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();
|
||||||
|
@ -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', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user