Skip to content

Commit 44bbff7

Browse files
Revert "fix(router): Respect custom UrlSerializer handling of query parameters (angular#64449)"
This reverts commit 46ae034.
1 parent c1d870b commit 44bbff7

File tree

6 files changed

+15
-159
lines changed

6 files changed

+15
-159
lines changed

goldens/public-api/router/index.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export type ComponentInputBindingFeature = RouterFeature<RouterFeatureKind.Compo
205205
export function convertToParamMap(params: Params): ParamMap;
206206

207207
// @public
208-
export function createUrlTreeFromSnapshot(relativeTo: ActivatedRouteSnapshot, commands: readonly any[], queryParams?: Params | null, fragment?: string | null, urlSerializer?: DefaultUrlSerializer): UrlTree;
208+
export function createUrlTreeFromSnapshot(relativeTo: ActivatedRouteSnapshot, commands: readonly any[], queryParams?: Params | null, fragment?: string | null): UrlTree;
209209

210210
// @public
211211
export type Data = {

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,6 @@
931931
"noop2",
932932
"noop3",
933933
"normalizeQueryParams",
934-
"normalizeQueryParams2",
935934
"notFoundValueOrThrow",
936935
"observable",
937936
"observeOn",

packages/router/src/create_url_tree.ts

Lines changed: 10 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,7 @@ import {ɵRuntimeError as RuntimeError} from '@angular/core';
1111
import {RuntimeErrorCode} from './errors';
1212
import {ActivatedRouteSnapshot} from './router_state';
1313
import {Params, PRIMARY_OUTLET} from './shared';
14-
import {
15-
createRoot,
16-
DefaultUrlSerializer,
17-
squashSegmentGroup,
18-
UrlSegment,
19-
UrlSegmentGroup,
20-
UrlSerializer,
21-
UrlTree,
22-
} from './url_tree';
14+
import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
2315
import {last, shallowEqual} from './utils/collection';
2416

2517
/**
@@ -36,9 +28,6 @@ import {last, shallowEqual} from './utils/collection';
3628
* @param queryParams The query parameters for the `UrlTree`. `null` if the `UrlTree` does not have
3729
* any query parameters.
3830
* @param fragment The fragment for the `UrlTree`. `null` if the `UrlTree` does not have a fragment.
39-
* @param urlSerializer The `UrlSerializer` to use for handling query parameter normalization.
40-
* You should provide your application's custom `UrlSerializer` if one is configured to parse and
41-
* serialize query parameter values to and from objects other than strings/string arrays.
4231
*
4332
* @usageNotes
4433
*
@@ -81,16 +70,9 @@ export function createUrlTreeFromSnapshot(
8170
commands: readonly any[],
8271
queryParams: Params | null = null,
8372
fragment: string | null = null,
84-
urlSerializer = new DefaultUrlSerializer(),
8573
): UrlTree {
8674
const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo);
87-
return createUrlTreeFromSegmentGroup(
88-
relativeToUrlSegmentGroup,
89-
commands,
90-
queryParams,
91-
fragment,
92-
urlSerializer,
93-
);
75+
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment);
9476
}
9577

9678
export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup {
@@ -121,7 +103,6 @@ export function createUrlTreeFromSegmentGroup(
121103
commands: readonly any[],
122104
queryParams: Params | null,
123105
fragment: string | null,
124-
urlSerializer: UrlSerializer,
125106
): UrlTree {
126107
let root = relativeTo;
127108
while (root.parent) {
@@ -131,20 +112,20 @@ export function createUrlTreeFromSegmentGroup(
131112
// `UrlSegmentGroup`. All we need to do is update the `queryParams` and `fragment` without
132113
// applying any other logic.
133114
if (commands.length === 0) {
134-
return tree(root, root, root, queryParams, fragment, urlSerializer);
115+
return tree(root, root, root, queryParams, fragment);
135116
}
136117

137118
const nav = computeNavigation(commands);
138119

139120
if (nav.toRoot()) {
140-
return tree(root, root, new UrlSegmentGroup([], {}), queryParams, fragment, urlSerializer);
121+
return tree(root, root, new UrlSegmentGroup([], {}), queryParams, fragment);
141122
}
142123

143124
const position = findStartingPositionForTargetGroup(nav, root, relativeTo);
144125
const newSegmentGroup = position.processChildren
145126
? updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands)
146127
: updateSegmentGroup(position.segmentGroup, position.index, nav.commands);
147-
return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment, urlSerializer);
128+
return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment);
148129
}
149130

150131
function isMatrixParams(command: any): boolean {
@@ -159,43 +140,18 @@ function isCommandWithOutlets(command: any): command is {outlets: {[key: string]
159140
return typeof command === 'object' && command != null && command.outlets;
160141
}
161142

162-
/**
163-
* Normalizes a query parameter value by using the `UrlSerializer` to serialize then parse the value.
164-
*
165-
* This ensures that the value is consistent between parsing a URL in the browser on a fresh page load (or page refresh)
166-
* and a navigation where the query parameter value is passed directly to the router.
167-
*
168-
* This also allows custom `UrlSerializer` implementations to define how query parameter values are represented
169-
* in a `UrlTree`. Since `UrlSerializer` already has a `parse` that takes a string, it already has control
170-
* over how a browser URL is parsed into a `UrlTree` on initial load/page refresh.
171-
*/
172-
function normalizeQueryParams(k: string, v: unknown, urlSerializer: UrlSerializer): unknown {
173-
const tree = new UrlTree();
174-
tree.queryParams = {[k]: v};
175-
return urlSerializer.parse(urlSerializer.serialize(tree)).queryParams[k];
176-
}
177-
178143
function tree(
179144
oldRoot: UrlSegmentGroup,
180145
oldSegmentGroup: UrlSegmentGroup,
181146
newSegmentGroup: UrlSegmentGroup,
182147
queryParams: Params | null,
183148
fragment: string | null,
184-
urlSerializer: UrlSerializer,
185149
): UrlTree {
186-
const qp: Params = {};
187-
for (const [key, value] of Object.entries(queryParams ?? {})) {
188-
// This retains old behavior where each item in the array was stringified individually This
189-
// helps remove special-case handling for empty and single-item arrays where the default
190-
// serializer removes empty arrays when serialized then parsed or converts them to non-arrays
191-
// for single-item arrays. Changing this could have breaking change implications. Prior code
192-
// always returned arrays of strings for array inputs so tests, applications, serializers,
193-
// etc. may only be set up to handle string arrays. We could consider changing this in the
194-
// future to serialize the entire array as a single value. For now, this feels safer and is
195-
// at least a step in the right direction.
196-
qp[key] = Array.isArray(value)
197-
? value.map((v) => normalizeQueryParams(key, v, urlSerializer))
198-
: normalizeQueryParams(key, value, urlSerializer);
150+
let qp: any = {};
151+
if (queryParams) {
152+
Object.entries(queryParams).forEach(([name, value]) => {
153+
qp[name] = Array.isArray(value) ? value.map((v: any) => `${v}`) : `${value}`;
154+
});
199155
}
200156

201157
let rootCandidate: UrlSegmentGroup;

packages/router/src/recognize.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ export class Recognizer {
105105
this.urlTree.queryParams,
106106
this.urlTree.fragment,
107107
);
108+
// https://github.com/angular/angular/issues/47307
108109
// Creating the tree stringifies the query params
109-
// We don't want to do this here to preserve pre-existing behavior
110-
// so reassign them to the original.
110+
// We don't want to do this here so reassign them to the original.
111111
tree.queryParams = this.urlTree.queryParams;
112112
routeState.url = this.urlSerializer.serialize(tree);
113113
return {state: routeState, tree};

packages/router/src/router.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -495,13 +495,7 @@ export class Router {
495495
}
496496
relativeToUrlSegmentGroup = this.currentUrlTree.root;
497497
}
498-
return createUrlTreeFromSegmentGroup(
499-
relativeToUrlSegmentGroup,
500-
commands,
501-
q,
502-
f ?? null,
503-
this.urlSerializer,
504-
);
498+
return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, q, f ?? null);
505499
}
506500

507501
/**

packages/router/test/create_url_tree.spec.ts

Lines changed: 1 addition & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {Router} from '../src/router';
1616
import {RouterModule} from '../src/router_module';
1717
import {ActivatedRoute, ActivatedRouteSnapshot} from '../src/router_state';
1818
import {Params, PRIMARY_OUTLET} from '../src/shared';
19-
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from '../src/url_tree';
19+
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
2020
import {provideRouter, withRouterConfig} from '../src';
2121
import {timeout} from './helpers';
2222

@@ -68,13 +68,6 @@ describe('createUrlTree', () => {
6868
expect(serializer.serialize(t2)).toEqual('/a/c/c2?n=1');
6969
});
7070

71-
it('should support parameter with single-item array', async () => {
72-
await router.navigateByUrl('/a/c');
73-
const t1 = create(router.routerState.root.children[0].children[0], ['c2'], {m: [1]});
74-
expect(serializer.serialize(t1)).toEqual('/a/c/c2?m=1');
75-
expect(t1.queryParams['m']).toEqual(['1']);
76-
});
77-
7871
it('should set query params', async () => {
7972
const p = serializer.parse('/');
8073
const t = await createRoot(p, [], {a: 'hey'});
@@ -88,17 +81,6 @@ describe('createUrlTree', () => {
8881
expect(t.queryParams).toEqual({a: '1'});
8982
expect(t.queryParamMap.get('a')).toEqual('1');
9083
});
91-
92-
it('should retain query parameter order', async () => {
93-
await router.navigateByUrl('/a/c');
94-
const t = create(router.routerState.root.children[0].children[0], ['c2'], {
95-
z: 1,
96-
a: '2',
97-
m: [3, 4],
98-
b: '5',
99-
});
100-
expect(serializer.serialize(t)).toEqual('/a/c/c2?z=1&a=2&m=3&m=4&b=5');
101-
});
10284
});
10385

10486
it('should navigate to the root', async () => {
@@ -764,78 +746,3 @@ async function advance(fixture: ComponentFixture<unknown>) {
764746
await timeout();
765747
fixture.detectChanges();
766748
}
767-
768-
describe('createUrlTree with custom serializer', () => {
769-
class CustomUrlSerializer extends DefaultUrlSerializer {
770-
override parse(url: string): UrlTree {
771-
const tree = super.parse(url);
772-
const qp: {[key: string]: any} = {};
773-
Object.keys(tree.queryParams).forEach((key) => {
774-
const value = tree.queryParams[key];
775-
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
776-
try {
777-
qp[key] = JSON.parse(value);
778-
} catch {}
779-
} else {
780-
qp[key] = value;
781-
}
782-
});
783-
tree.queryParams = qp;
784-
return tree;
785-
}
786-
override serialize(tree: UrlTree): string {
787-
const qp: {[key: string]: any} = {};
788-
Object.keys(tree.queryParams).forEach((key) => {
789-
const value = tree.queryParams[key];
790-
if (typeof value === 'object' && value !== null) {
791-
qp[key] = JSON.stringify(value);
792-
} else {
793-
qp[key] = value;
794-
}
795-
});
796-
tree.queryParams = qp;
797-
return super.serialize(tree);
798-
}
799-
}
800-
801-
beforeEach(() => {
802-
TestBed.configureTestingModule({
803-
providers: [provideRouter([]), {provide: UrlSerializer, useClass: CustomUrlSerializer}],
804-
});
805-
});
806-
807-
it('should work with custom URL serializer that handles objects', async () => {
808-
const router = TestBed.inject(Router);
809-
const serializer = TestBed.inject(UrlSerializer);
810-
const tree = router.createUrlTree(['a'], {queryParams: {someObject: {a: 1}}});
811-
const url = serializer.serialize(tree);
812-
expect(url).toEqual('/a?someObject=%7B%22a%22:1%7D');
813-
const parsedTree = serializer.parse(url);
814-
expect(parsedTree.queryParams).toEqual({someObject: {a: 1}});
815-
});
816-
817-
it('should work with custom URL serializer and empty arrays', async () => {
818-
const router = TestBed.inject(Router);
819-
const serializer = TestBed.inject(UrlSerializer);
820-
const tree = router.createUrlTree(['a'], {queryParams: {empty: []}});
821-
const url = serializer.serialize(tree);
822-
// with default serializer, 'empty' would be removed because serialization maps arrays to multiple
823-
// key-value pairs, and an empty array would result in no key-value pairs
824-
expect(url).toEqual('/a?empty=%5B%5D');
825-
const parsedTree = serializer.parse(url);
826-
expect(parsedTree.queryParams).toEqual({empty: []});
827-
});
828-
829-
it('should work with custom URL serializer and single item arrays', async () => {
830-
const router = TestBed.inject(Router);
831-
const serializer = TestBed.inject(UrlSerializer);
832-
const tree = router.createUrlTree(['a'], {queryParams: {one: ['1']}});
833-
const url = serializer.serialize(tree);
834-
expect(url).toEqual('/a?one=%5B%221%22%5D');
835-
const parsedTree = serializer.parse(url);
836-
// with default serializer, 'one' would be removed because serialization maps arrays to multiple
837-
// key-value pairs, and a single item array would result in one key-value pair, which would be
838-
// deserialized to a string, not an array
839-
expect(parsedTree.queryParams).toEqual({one: ['1']});
840-
});
841-
});

0 commit comments

Comments
 (0)