From 6c76c91e151b53dfaccb4be43d43a8d857715dd7 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 30 Jul 2024 12:26:22 -0700 Subject: [PATCH] feat(router): Add defaultQueryParamsHandling to router configuration (#57198) This commit adds an option to specify the default value for `queryParamsHandling` in `Router.createUrlTree` when another option is not specified (or is `null|undefined`). resolves #12664 PR Close #57198 --- goldens/public-api/router/index.api.md | 3 +- packages/router/src/models.ts | 5 ++- packages/router/src/router.ts | 2 +- packages/router/src/router_config.ts | 16 +++++++- packages/router/test/create_url_tree.spec.ts | 42 +++++++++++++++++++- 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/goldens/public-api/router/index.api.md b/goldens/public-api/router/index.api.md index a0eb2368ae2..c368bd0220d 100644 --- a/goldens/public-api/router/index.api.md +++ b/goldens/public-api/router/index.api.md @@ -587,7 +587,7 @@ export function provideRouter(routes: Routes, ...features: RouterFeatures[]): En export function provideRoutes(routes: Routes): Provider[]; // @public -export type QueryParamsHandling = 'merge' | 'preserve' | ''; +export type QueryParamsHandling = 'merge' | 'preserve' | 'replace' | ''; // @public export class RedirectCommand { @@ -744,6 +744,7 @@ export const ROUTER_INITIALIZER: InjectionToken<(compRef: ComponentRef) => // @public export interface RouterConfigOptions { canceledNavigationResolution?: 'replace' | 'computed'; + defaultQueryParamsHandling?: QueryParamsHandling; onSameUrlNavigation?: OnSameUrlNavigation; paramsInheritanceStrategy?: 'emptyOnly' | 'always'; resolveNavigationPromiseOnError?: boolean; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index a0dc7cb7f45..6863daf03fa 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -275,13 +275,14 @@ export type LoadChildren = LoadChildrenCallback; * One of: * - `"merge"` : Merge new parameters with current parameters. * - `"preserve"` : Preserve current parameters. - * - `""` : Replace current parameters with new parameters. This is the default behavior. + * - `"replace"` : Replace current parameters with new parameters. This is the default behavior. + * - `""` : For legacy reasons, the same as `'replace'`. * * @see {@link UrlCreationOptions#queryParamsHandling} * @see {@link RouterLink} * @publicApi */ -export type QueryParamsHandling = 'merge' | 'preserve' | ''; +export type QueryParamsHandling = 'merge' | 'preserve' | 'replace' | ''; /** * The type for the function that can be used to handle redirects when the path matches a `Route` config. diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 71c052385d8..c345d5fdb68 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -452,7 +452,7 @@ export class Router { navigationExtras; const f = preserveFragment ? this.currentUrlTree.fragment : fragment; let q: Params | null = null; - switch (queryParamsHandling) { + switch (queryParamsHandling ?? this.options.defaultQueryParamsHandling) { case 'merge': q = {...this.currentUrlTree.queryParams, ...queryParams}; break; diff --git a/packages/router/src/router_config.ts b/packages/router/src/router_config.ts index d2dd44e7fd0..13c40c0bed9 100644 --- a/packages/router/src/router_config.ts +++ b/packages/router/src/router_config.ts @@ -8,7 +8,7 @@ import {InjectionToken} from '@angular/core'; -import {OnSameUrlNavigation} from './models'; +import {OnSameUrlNavigation, QueryParamsHandling} from './models'; /** * Error handler that is invoked when a navigation error occurs. @@ -113,6 +113,20 @@ export interface RouterConfigOptions { */ urlUpdateStrategy?: 'deferred' | 'eager'; + /** + * The default strategy to use for handling query params in `Router.createUrlTree` when one is not provided. + * + * The `createUrlTree` method is used internally by `Router.navigate` and `RouterLink`. + * Note that `QueryParamsHandling` does not apply to `Router.navigateByUrl`. + * + * When neither the default nor the queryParamsHandling option is specified in the call to `createUrlTree`, + * the current parameters will be replaced by new parameters. + * + * @see {@link Router#createUrlTree} + * @see {@link QueryParamsHandling} + */ + defaultQueryParamsHandling?: QueryParamsHandling; + /** * When `true`, the `Promise` will instead resolve with `false`, as it does with other failed * navigations (for example, when guards are rejected). diff --git a/packages/router/test/create_url_tree.spec.ts b/packages/router/test/create_url_tree.spec.ts index 61afd5a29a2..a0c21461a2b 100644 --- a/packages/router/test/create_url_tree.spec.ts +++ b/packages/router/test/create_url_tree.spec.ts @@ -11,12 +11,13 @@ import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing' import {By} from '@angular/platform-browser'; import {createUrlTreeFromSnapshot} from '../src/create_url_tree'; -import {Routes} from '../src/models'; +import {QueryParamsHandling, Routes} from '../src/models'; import {Router} from '../src/router'; import {RouterModule} from '../src/router_module'; import {ActivatedRoute, ActivatedRouteSnapshot} from '../src/router_state'; import {Params, PRIMARY_OUTLET} from '../src/shared'; import {DefaultUrlSerializer, UrlTree} from '../src/url_tree'; +import {provideRouter, withRouterConfig} from '../src'; describe('createUrlTree', async () => { const serializer = new DefaultUrlSerializer(); @@ -574,6 +575,45 @@ describe('createUrlTree', async () => { }); }); +describe('defaultQueryParamsHandling', () => { + async function setupRouter(defaultQueryParamsHandling: QueryParamsHandling): Promise { + TestBed.configureTestingModule({ + providers: [ + provideRouter( + [{path: '**', component: class {}}], + withRouterConfig({ + defaultQueryParamsHandling, + }), + ), + ], + }); + + const router = TestBed.inject(Router); + await router.navigateByUrl('/initial?a=1'); + return router; + } + + it('can use "merge" as the default', async () => { + const router = await setupRouter('merge'); + await router.navigate(['new'], {queryParams: {'b': 2}}); + expect(router.url).toEqual('/new?a=1&b=2'); + }); + + it('can use "perserve" as the default', async () => { + const router = await setupRouter('preserve'); + await router.navigate(['new'], {queryParams: {'b': 2}}); + expect(router.url).toEqual('/new?a=1'); + }); + + it('can override the default by providing a new option', async () => { + const router = await setupRouter('preserve'); + await router.navigate(['new'], {queryParams: {'b': 2}, queryParamsHandling: 'merge'}); + expect(router.url).toEqual('/new?a=1&b=2'); + await router.navigate(['replace'], {queryParamsHandling: 'replace'}); + expect(router.url).toEqual('/replace'); + }); +}); + async function createRoot( tree: UrlTree, commands: any[],