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}]});
|
||||||
|
|
|
@ -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…
Reference in New Issue