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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user