Skip to content

Commit af1f1e4

Browse files
Ritesh Shuklafacebook-github-bot
authored andcommitted
Implementation of URLSearchParams Methods (#50043)
Summary: This PR addresses the following issues and enhances the functionality of `URLSearchParams`: 1. Extended Initialization Parameters: Previously, only `Record<string, string>` was supported while initialising URLSearchParams. Now supports `string`, `Record<string, string>`, and `Array<[string, string]>` (aligning with web standards). 2. Added Implementation for `delete()`, `get()`, `getAll()`, `has()`, `sort()`, and `set()`. 3. Addition of Iteration Methods: Added `keys()`, `values()`, `entries()`, and `forEach()` methods. Outputs are identical to web. 4. Bug Fix: Incorrect Initialization from URL. Previously, `URLSearchParams` was initialized with an empty value when accessed via `url.searchParams`, even if the `URL` contained search parameters. ## Changelog: [General][Added] Implementation for URLSearchParams Pull Request resolved: #50043 Test Plan: Can be tested by below code ```js const isHermes = () => !!global.HermesInternal; const params = new URLSearchParams(""); console.log("Is Hermes Enabled:-",isHermes()) console.log("Values:", Array.from(params.values())); // ✅ ["value1", "value2"] console.log("Keys:", Array.from(params.keys())); // ✅ ["key1", "key2"] console.log("Entries:", Array.from(params.entries())); // ✅ [["key1", "value1"], ["key2", "value2"]] params.forEach((value, key) => { console.log(`${key}: ${value}`); }); console.log("Has 'key1'", params.has("key1")); // ✅ true console.log("Has 'key3'", params.has("key3")); // ✅ false console.log("Get 'key1':", params.get("key1")); // ✅ "value1" console.log("Get 'key3':", params.get("key3")); // ✅ null ``` Tested on both hermes and JSC. Hermes:- ![image](https://github.com/user-attachments/assets/9a467001-693a-4e38-b349-e8d92f961808) JSC:- ![image](https://github.com/user-attachments/assets/e5f89375-ede4-4ea0-9c62-f98f63c67188) Reviewed By: cipolleschi Differential Revision: D71965101 Pulled By: huntie fbshipit-source-id: c0a0f965ac1ff9577cdc8b3c11f0dd0ced538868
1 parent aadb1f1 commit af1f1e4

File tree

6 files changed

+208
-46
lines changed

6 files changed

+208
-46
lines changed

packages/react-native/Libraries/Blob/URL.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class URL {
160160

161161
get searchParams(): URLSearchParams {
162162
if (this._searchParamsInstance == null) {
163-
this._searchParamsInstance = new URLSearchParams();
163+
this._searchParamsInstance = new URLSearchParams(this.search);
164164
}
165165
return this._searchParamsInstance;
166166
}

packages/react-native/Libraries/Blob/URLSearchParams.js

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,135 @@
1111
// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/src
1212
// The reference code bloat comes from Unicode issues with URLs, so those won't work here.
1313
export class URLSearchParams {
14-
_searchParams: Array<[string, string]> = [];
14+
_searchParams: Map<string, string[]> = new Map();
1515

16-
constructor(params?: Record<string, string>) {
17-
if (typeof params === 'object') {
18-
Object.keys(params).forEach(key => this.append(key, params[key]));
16+
constructor(params?: Record<string, string> | string | [string, string][]) {
17+
if (params === null) {
18+
return;
19+
}
20+
21+
if (typeof params === 'string') {
22+
// URLSearchParams("key1=value1&key2=value2");
23+
params
24+
.replace(/^\?/, '')
25+
.split('&')
26+
.forEach(pair => {
27+
if (!pair) {
28+
return;
29+
}
30+
const [key, value] = pair
31+
.split('=')
32+
.map(part => decodeURIComponent(part.replace(/\+/g, ' ')));
33+
this.append(key, value);
34+
});
35+
} else if (Array.isArray(params)) {
36+
//URLSearchParams([["key1", "value1"], ["key2", "value2"]]);
37+
params.forEach(([key, value]) => this.append(key, value));
38+
} else if (typeof params === 'object') {
39+
//URLSearchParams({ key1: "value1", key2: "value2" });
40+
Object.entries(params).forEach(([key, value]) => this.append(key, value));
1941
}
2042
}
2143

2244
append(key: string, value: string): void {
23-
this._searchParams.push([key, value]);
45+
if (!this._searchParams.has(key)) {
46+
this._searchParams.set(key, [value]); // Initialize with an array if key is missing
47+
} else {
48+
this._searchParams.get(key)?.push(value); // Else push the value to the array
49+
}
50+
}
51+
52+
delete(name: string): void {
53+
this._searchParams.delete(name);
2454
}
2555

26-
delete(name: string): empty {
27-
throw new Error('URLSearchParams.delete is not implemented');
56+
get(name: string): string | null {
57+
const values = this._searchParams.get(name);
58+
return values ? values[0] : null;
2859
}
2960

30-
get(name: string): empty {
31-
throw new Error('URLSearchParams.get is not implemented');
61+
getAll(name: string): string[] {
62+
return this._searchParams.get(name) ?? [];
3263
}
3364

34-
getAll(name: string): empty {
35-
throw new Error('URLSearchParams.getAll is not implemented');
65+
has(name: string): boolean {
66+
return this._searchParams.has(name);
3667
}
3768

38-
has(name: string): empty {
39-
throw new Error('URLSearchParams.has is not implemented');
69+
set(name: string, value: string): void {
70+
this._searchParams.set(name, [value]);
4071
}
4172

42-
set(name: string, value: string): empty {
43-
throw new Error('URLSearchParams.set is not implemented');
73+
keys(): Iterator<string> {
74+
return this._searchParams.keys();
4475
}
4576

46-
sort(): empty {
47-
throw new Error('URLSearchParams.sort is not implemented');
77+
values(): Iterator<string> {
78+
function* generateValues(params: Map<string, string[]>): Iterator<string> {
79+
for (const valueArray of params.values()) {
80+
for (const value of valueArray) {
81+
yield value;
82+
}
83+
}
84+
}
85+
return generateValues(this._searchParams);
86+
}
87+
88+
entries(): Iterator<[string, string]> {
89+
function* generateEntries(
90+
params: Map<string, string[]>,
91+
): Iterator<[string, string]> {
92+
for (const [key, values] of params) {
93+
for (const value of values) {
94+
yield [key, value];
95+
}
96+
}
97+
}
98+
99+
return generateEntries(this._searchParams);
100+
}
101+
102+
forEach(
103+
callback: (value: string, key: string, searchParams: this) => void,
104+
): void {
105+
for (const [key, values] of this._searchParams) {
106+
for (const value of values) {
107+
callback(value, key, this);
108+
}
109+
}
110+
}
111+
112+
sort(): void {
113+
this._searchParams = new Map(
114+
[...this._searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)),
115+
);
48116
}
49117

50118
// $FlowFixMe[unsupported-syntax]
51119
[Symbol.iterator](): Iterator<[string, string]> {
52-
return this._searchParams[Symbol.iterator]();
120+
const entries: [string, string][] = [];
121+
122+
for (const [key, values] of this._searchParams) {
123+
for (const value of values) {
124+
entries.push([key, value]);
125+
}
126+
}
127+
128+
return entries[Symbol.iterator]();
53129
}
54130

55131
toString(): string {
56-
if (this._searchParams.length === 0) {
57-
return '';
58-
}
59-
const last = this._searchParams.length - 1;
60-
return this._searchParams.reduce((acc, curr, index) => {
61-
return (
62-
acc +
63-
encodeURIComponent(curr[0]) +
64-
'=' +
65-
encodeURIComponent(curr[1]) +
66-
(index === last ? '' : '&')
67-
);
68-
}, '');
132+
return Array.from(this._searchParams.entries())
133+
.map(([key, values]) =>
134+
values
135+
.map(
136+
value =>
137+
`${encodeURIComponent(key).replace(/%20/g, '+')}=${encodeURIComponent(
138+
value,
139+
).replace(/%20/g, '+')}`, // Convert only spaces to '+'
140+
)
141+
.join('&'),
142+
)
143+
.join('&');
69144
}
70145
}

packages/react-native/Libraries/Blob/URLSearchParams.js.flow

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010

1111
declare export class URLSearchParams {
1212
_searchParams: Array<[string, string]>;
13-
constructor(params?: Record<string, string>): void;
13+
constructor(
14+
params?: Record<string, string> | string | Array<[string, string]>,
15+
): void;
1416
append(key: string, value: string): void;
15-
delete(name: string): empty;
16-
get(name: string): empty;
17-
getAll(name: string): empty;
18-
has(name: string): empty;
19-
set(name: string, value: string): empty;
20-
sort(): empty;
17+
delete(name: string): void;
18+
get(name: string): string;
19+
getAll(name: string): Array<string>;
20+
has(name: string): boolean;
21+
set(name: string, value: string): void;
22+
sort(): void;
2123
@@iterator(): Iterator<[string, string]>;
2224
toString(): string;
25+
keys(): Iterator<string>;
26+
values(): Iterator<string>;
27+
entries(): Iterator<[string, string]>;
2328
}

packages/react-native/Libraries/Blob/__tests__/URL-test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'use strict';
1212

1313
const URL = require('../URL').URL;
14+
const URLSearchParams = require('../URL').URLSearchParams;
1415

1516
describe('URL', function () {
1617
it('should pass Mozilla Dev Network examples', () => {
@@ -52,5 +53,80 @@ describe('URL', function () {
5253
expect(url.pathname).toBe('/docs/path');
5354
expect(url.port).toBe('8080');
5455
expect(url.search).toBe('?query=testQuery&key=value');
56+
57+
// Test searchParams
58+
const searchParams = url.searchParams;
59+
expect(searchParams.get('query')).toBe('testQuery');
60+
expect(searchParams.get('key')).toBe('value');
61+
62+
const paramsFromString = new URLSearchParams(
63+
[
64+
'?param1=value1',
65+
'&param2=value2%20with%20spaces',
66+
'&param3=value3+with+spaces+legacy',
67+
].join(''),
68+
);
69+
expect(paramsFromString.get('param1')).toBe('value1');
70+
expect(paramsFromString.get('param2')).toBe('value2 with spaces');
71+
expect(paramsFromString.get('param3')).toBe('value3 with spaces legacy');
72+
expect(paramsFromString.toString()).toBe(
73+
'param1=value1&param2=value2+with+spaces&param3=value3+with+spaces+legacy',
74+
);
75+
76+
const paramsFromObject = new URLSearchParams({
77+
user: 'john',
78+
age: '30',
79+
active: 'true',
80+
});
81+
82+
expect(paramsFromObject.get('user')).toBe('john');
83+
expect(paramsFromObject.get('age')).toBe('30');
84+
expect(paramsFromObject.get('active')).toBe('true');
85+
86+
const valuesArray = Array.from(paramsFromObject.values());
87+
expect(valuesArray).toEqual(['john', '30', 'true']);
88+
const entriesArray = Array.from(paramsFromObject.entries());
89+
expect(entriesArray).toEqual([
90+
['user', 'john'],
91+
['age', '30'],
92+
['active', 'true'],
93+
]);
94+
95+
// URLSearchParams: Empty
96+
const emptyParams = new URLSearchParams('');
97+
expect([...emptyParams.entries()]).toEqual([]);
98+
99+
// URLSearchParams: Array (for multiple values of the same key)
100+
const paramsFromArray = new URLSearchParams([
101+
['key1', 'value1'],
102+
['key1', 'value2'],
103+
['key2', 'value3'],
104+
]);
105+
expect(paramsFromArray.getAll('key1')).toEqual(['value1', 'value2']);
106+
expect(paramsFromArray.get('key2')).toBe('value3');
107+
108+
// Manipulating existing search params in the URL
109+
const urlParams = url.searchParams;
110+
expect(urlParams.get('query')).toBe('testQuery');
111+
expect(urlParams.get('key')).toBe('value');
112+
113+
// Adding a new param
114+
urlParams.append('newKey', 'newValue');
115+
expect(urlParams.get('newKey')).toBe('newValue');
116+
117+
// Deleting a param
118+
urlParams.delete('key');
119+
expect(urlParams.get('key')).toBeNull();
120+
121+
// Checking if a param exists
122+
expect(urlParams.has('query')).toBe(true);
123+
expect(urlParams.has('key')).toBe(false);
124+
125+
// Sorting URLSearchParams
126+
const unsortedParams = new URLSearchParams(
127+
'?z=last&b=second&c=third&a=first',
128+
);
129+
unsortedParams.sort();
130+
expect(unsortedParams.toString()).toBe('a=first&b=second&c=third&z=last');
55131
});
56132
});

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,16 +1345,21 @@ declare export class URL {
13451345

13461346
exports[`public API should not change unintentionally Libraries/Blob/URLSearchParams.js.flow 1`] = `
13471347
"declare export class URLSearchParams {
1348-
constructor(params?: Record<string, string>): void;
1348+
constructor(
1349+
params?: Record<string, string> | string | Array<[string, string]>
1350+
): void;
13491351
append(key: string, value: string): void;
1350-
delete(name: string): empty;
1351-
get(name: string): empty;
1352-
getAll(name: string): empty;
1353-
has(name: string): empty;
1354-
set(name: string, value: string): empty;
1355-
sort(): empty;
1352+
delete(name: string): void;
1353+
get(name: string): string;
1354+
getAll(name: string): Array<string>;
1355+
has(name: string): boolean;
1356+
set(name: string, value: string): void;
1357+
sort(): void;
13561358
@@iterator(): Iterator<[string, string]>;
13571359
toString(): string;
1360+
keys(): Iterator<string>;
1361+
values(): Iterator<string>;
1362+
entries(): Iterator<[string, string]>;
13581363
}
13591364
"
13601365
`;

packages/rn-tester/js/examples/Urls/UrlExample.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function URLComponent(props: Props) {
3333
<RNTesterText testID="URL-pathname">{`pathname: ${parsedUrl.pathname}`}</RNTesterText>
3434
<RNTesterText testID="URL-port">{`port: ${parsedUrl.port}`}</RNTesterText>
3535
<RNTesterText testID="URL-search">{`search: ${parsedUrl.search}`}</RNTesterText>
36+
<RNTesterText testID="URL-search-params">{`searchParams: ${parsedUrl.searchParams.toString()}`}</RNTesterText>
3637
</View>
3738
);
3839
}

0 commit comments

Comments
 (0)