feat(router): add urlUpdateStrategy allow updating the browser URL at the beginning of navigation (#24820)
Fixes #24616 PR Close #24820
This commit is contained in:
		
							parent
							
								
									4d8b8ad372
								
							
						
					
					
						commit
						328971ffcc
					
				| @ -285,6 +285,18 @@ export class Router { | |||||||
|    */ |    */ | ||||||
|   paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly'; |   paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly'; | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Defines when the router updates the browser URL. The default behavior is to update after | ||||||
|  |    * successful navigation. However, some applications may prefer a mode where the URL gets | ||||||
|  |    * updated at the beginning of navigation. The most common use case would be updating the | ||||||
|  |    * URL early so if navigation fails, you can show an error message with the URL that failed. | ||||||
|  |    * Available options are: | ||||||
|  |    * | ||||||
|  |    * - `'deferred'`, the default, updates the browser URL after navigation has finished. | ||||||
|  |    * - `'eager'`, updates browser URL at the beginning of navigation. | ||||||
|  |    */ | ||||||
|  |   urlUpdateStrategy: 'deferred'|'eager' = 'deferred'; | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Creates the router service. |    * Creates the router service. | ||||||
|    */ |    */ | ||||||
| @ -610,6 +622,9 @@ export class Router { | |||||||
| 
 | 
 | ||||||
|     if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) && |     if ((this.onSameUrlNavigation === 'reload' ? true : urlTransition) && | ||||||
|         this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) { |         this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) { | ||||||
|  |       if (this.urlUpdateStrategy === 'eager' && !extras.skipLocationChange) { | ||||||
|  |         this.setBrowserUrl(rawUrl, !!extras.replaceUrl, id); | ||||||
|  |       } | ||||||
|       (this.events as Subject<Event>) |       (this.events as Subject<Event>) | ||||||
|           .next(new NavigationStart(id, this.serializeUrl(url), source, state)); |           .next(new NavigationStart(id, this.serializeUrl(url), source, state)); | ||||||
|       Promise.resolve() |       Promise.resolve() | ||||||
| @ -791,13 +806,8 @@ export class Router { | |||||||
| 
 | 
 | ||||||
|           (this as{routerState: RouterState}).routerState = state; |           (this as{routerState: RouterState}).routerState = state; | ||||||
| 
 | 
 | ||||||
|           if (!skipLocationChange) { |           if (this.urlUpdateStrategy === 'deferred' && !skipLocationChange) { | ||||||
|             const path = this.urlSerializer.serialize(this.rawUrlTree); |             this.setBrowserUrl(this.rawUrlTree, replaceUrl, id); | ||||||
|             if (this.location.isCurrentPathEqualTo(path) || replaceUrl) { |  | ||||||
|               this.location.replaceState(path, '', {navigationId: id}); |  | ||||||
|             } else { |  | ||||||
|               this.location.go(path, '', {navigationId: id}); |  | ||||||
|             } |  | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           new ActivateRoutes( |           new ActivateRoutes( | ||||||
| @ -843,6 +853,15 @@ export class Router { | |||||||
|             }); |             }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private setBrowserUrl(url: UrlTree, replaceUrl: boolean, id: number) { | ||||||
|  |     const path = this.urlSerializer.serialize(url); | ||||||
|  |     if (this.location.isCurrentPathEqualTo(path) || replaceUrl) { | ||||||
|  |       this.location.replaceState(path, '', {navigationId: id}); | ||||||
|  |     } else { | ||||||
|  |       this.location.go(path, '', {navigationId: id}); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private resetStateAndUrl(storedState: RouterState, storedUrl: UrlTree, rawUrl: UrlTree): void { |   private resetStateAndUrl(storedState: RouterState, storedUrl: UrlTree, rawUrl: UrlTree): void { | ||||||
|     (this as{routerState: RouterState}).routerState = storedState; |     (this as{routerState: RouterState}).routerState = storedState; | ||||||
|     this.currentUrlTree = storedUrl; |     this.currentUrlTree = storedUrl; | ||||||
|  | |||||||
| @ -405,6 +405,18 @@ export interface ExtraOptions { | |||||||
|    * */ |    * */ | ||||||
|   malformedUriErrorHandler?: |   malformedUriErrorHandler?: | ||||||
|       (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree; |       (error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Defines when the router updates the browser URL. The default behavior is to update after | ||||||
|  |    * successful navigation. However, some applications may prefer a mode where the URL gets | ||||||
|  |    * updated at the beginning of navigation. The most common use case would be updating the | ||||||
|  |    * URL early so if navigation fails, you can show an error message with the URL that failed. | ||||||
|  |    * Available options are: | ||||||
|  |    * | ||||||
|  |    * - `'deferred'`, the default, updates the browser URL after navigation has finished. | ||||||
|  |    * - `'eager'`, updates browser URL at the beginning of navigation. | ||||||
|  |    */ | ||||||
|  |   urlUpdateStrategy?: 'deferred'|'eager'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function setupRouter( | export function setupRouter( | ||||||
| @ -449,6 +461,10 @@ export function setupRouter( | |||||||
|     router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; |     router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (opts.urlUpdateStrategy) { | ||||||
|  |     router.urlUpdateStrategy = opts.urlUpdateStrategy; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   return router; |   return router; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -469,6 +469,31 @@ describe('Integration', () => { | |||||||
|        expect(fixture.nativeElement).toHaveText('team 33 [ , right:  ]'); |        expect(fixture.nativeElement).toHaveText('team 33 [ , right:  ]'); | ||||||
|      }))); |      }))); | ||||||
| 
 | 
 | ||||||
|  |   it('should eagerly update the URL with urlUpdateStrategy="eagar"', | ||||||
|  |      fakeAsync(inject([Router, Location], (router: Router, location: Location) => { | ||||||
|  |        const fixture = TestBed.createComponent(RootCmp); | ||||||
|  |        advance(fixture); | ||||||
|  | 
 | ||||||
|  |        router.resetConfig([{path: 'team/:id', component: TeamCmp}]); | ||||||
|  | 
 | ||||||
|  |        router.navigateByUrl('/team/22'); | ||||||
|  |        advance(fixture); | ||||||
|  |        expect(location.path()).toEqual('/team/22'); | ||||||
|  | 
 | ||||||
|  |        expect(fixture.nativeElement).toHaveText('team 22 [ , right:  ]'); | ||||||
|  | 
 | ||||||
|  |        router.urlUpdateStrategy = 'eager'; | ||||||
|  |        (router as any).hooks.beforePreactivation = () => { | ||||||
|  |          expect(location.path()).toEqual('/team/33'); | ||||||
|  |          expect(fixture.nativeElement).toHaveText('team 22 [ , right:  ]'); | ||||||
|  |          return of (null); | ||||||
|  |        }; | ||||||
|  |        router.navigateByUrl('/team/33'); | ||||||
|  | 
 | ||||||
|  |        advance(fixture); | ||||||
|  |        expect(fixture.nativeElement).toHaveText('team 33 [ , right:  ]'); | ||||||
|  |      }))); | ||||||
|  | 
 | ||||||
|   it('should navigate back and forward', |   it('should navigate back and forward', | ||||||
|      fakeAsync(inject([Router, Location], (router: Router, location: Location) => { |      fakeAsync(inject([Router, Location], (router: Router, location: Location) => { | ||||||
|        const fixture = createRoot(router, RootCmp); |        const fixture = createRoot(router, RootCmp); | ||||||
| @ -868,6 +893,8 @@ describe('Integration', () => { | |||||||
|        ]); |        ]); | ||||||
|      }))); |      }))); | ||||||
| 
 | 
 | ||||||
|  |   // Errors should behave the same for both deferred and eager URL update strategies
 | ||||||
|  |   ['deferred', 'eager'].forEach((strat: any) => { | ||||||
|     it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => { |     it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => { | ||||||
|          const router: Router = TestBed.get(Router); |          const router: Router = TestBed.get(Router); | ||||||
|          const location: SpyLocation = TestBed.get(Location); |          const location: SpyLocation = TestBed.get(Location); | ||||||
| @ -875,6 +902,7 @@ describe('Integration', () => { | |||||||
| 
 | 
 | ||||||
|          router.resetConfig( |          router.resetConfig( | ||||||
|              [{path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp}]); |              [{path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp}]); | ||||||
|  |          router.urlUpdateStrategy = strat; | ||||||
| 
 | 
 | ||||||
|          router.navigateByUrl('/simple'); |          router.navigateByUrl('/simple'); | ||||||
|          advance(fixture); |          advance(fixture); | ||||||
| @ -904,7 +932,7 @@ describe('Integration', () => { | |||||||
|            {path: 'simple1', component: SimpleCmp}, {path: 'simple2', component: SimpleCmp}, |            {path: 'simple1', component: SimpleCmp}, {path: 'simple2', component: SimpleCmp}, | ||||||
|            {path: 'throwing', component: ThrowingCmp} |            {path: 'throwing', component: ThrowingCmp} | ||||||
|          ]); |          ]); | ||||||
| 
 |          router.urlUpdateStrategy = strat; | ||||||
| 
 | 
 | ||||||
|          let event: NavigationStart; |          let event: NavigationStart; | ||||||
|          router.events.subscribe(e => { |          router.events.subscribe(e => { | ||||||
| @ -938,6 +966,7 @@ describe('Integration', () => { | |||||||
| 
 | 
 | ||||||
|          router.resetConfig( |          router.resetConfig( | ||||||
|              [{path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp}]); |              [{path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp}]); | ||||||
|  |          router.urlUpdateStrategy = strat; | ||||||
| 
 | 
 | ||||||
|          const events: any[] = []; |          const events: any[] = []; | ||||||
|          router.events.forEach((e: any) => { |          router.events.forEach((e: any) => { | ||||||
| @ -956,6 +985,8 @@ describe('Integration', () => { | |||||||
|          expect(events).toEqual(['/simple', '/throwing']); |          expect(events).toEqual(['/simple', '/throwing']); | ||||||
|        })); |        })); | ||||||
| 
 | 
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   it('should dispatch NavigationCancel after the url has been reset back', fakeAsync(() => { |   it('should dispatch NavigationCancel after the url has been reset back', fakeAsync(() => { | ||||||
|        TestBed.configureTestingModule( |        TestBed.configureTestingModule( | ||||||
|            {providers: [{provide: 'returnsFalse', useValue: () => false}]}); |            {providers: [{provide: 'returnsFalse', useValue: () => false}]}); | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								tools/public_api_guard/router/router.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								tools/public_api_guard/router/router.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -121,6 +121,7 @@ export interface ExtraOptions { | |||||||
|     preloadingStrategy?: any; |     preloadingStrategy?: any; | ||||||
|     scrollOffset?: [number, number] | (() => [number, number]); |     scrollOffset?: [number, number] | (() => [number, number]); | ||||||
|     scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'; |     scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'; | ||||||
|  |     urlUpdateStrategy?: 'deferred' | 'eager'; | ||||||
|     useHash?: boolean; |     useHash?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -323,6 +324,7 @@ export declare class Router { | |||||||
|     readonly routerState: RouterState; |     readonly routerState: RouterState; | ||||||
|     readonly url: string; |     readonly url: string; | ||||||
|     urlHandlingStrategy: UrlHandlingStrategy; |     urlHandlingStrategy: UrlHandlingStrategy; | ||||||
|  |     urlUpdateStrategy: 'deferred' | 'eager'; | ||||||
|     constructor(rootComponentType: Type<any> | null, urlSerializer: UrlSerializer, rootContexts: ChildrenOutletContexts, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes); |     constructor(rootComponentType: Type<any> | null, urlSerializer: UrlSerializer, rootContexts: ChildrenOutletContexts, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes); | ||||||
|     createUrlTree(commands: any[], navigationExtras?: NavigationExtras): UrlTree; |     createUrlTree(commands: any[], navigationExtras?: NavigationExtras): UrlTree; | ||||||
|     dispose(): void; |     dispose(): void; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user