diff --git a/packages/react-native/Libraries/Blob/URL.js b/packages/react-native/Libraries/Blob/URL.js index c9f6d933a556e9..7aea70294668b2 100644 --- a/packages/react-native/Libraries/Blob/URL.js +++ b/packages/react-native/Libraries/Blob/URL.js @@ -160,7 +160,7 @@ export class URL { get searchParams(): URLSearchParams { if (this._searchParamsInstance == null) { - this._searchParamsInstance = new URLSearchParams(); + this._searchParamsInstance = new URLSearchParams(this.search); } return this._searchParamsInstance; } diff --git a/packages/react-native/Libraries/Blob/URLSearchParams.js b/packages/react-native/Libraries/Blob/URLSearchParams.js index 36353dfc25c872..3ab87c96861bdc 100644 --- a/packages/react-native/Libraries/Blob/URLSearchParams.js +++ b/packages/react-native/Libraries/Blob/URLSearchParams.js @@ -11,60 +11,135 @@ // Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/src // The reference code bloat comes from Unicode issues with URLs, so those won't work here. export class URLSearchParams { - _searchParams: Array<[string, string]> = []; + _searchParams: Map = new Map(); - constructor(params?: Record) { - if (typeof params === 'object') { - Object.keys(params).forEach(key => this.append(key, params[key])); + constructor(params?: Record | string | [string, string][]) { + if (params === null) { + return; + } + + if (typeof params === 'string') { + // URLSearchParams("key1=value1&key2=value2"); + params + .replace(/^\?/, '') + .split('&') + .forEach(pair => { + if (!pair) { + return; + } + const [key, value] = pair + .split('=') + .map(part => decodeURIComponent(part.replace(/\+/g, ' '))); + this.append(key, value); + }); + } else if (Array.isArray(params)) { + //URLSearchParams([["key1", "value1"], ["key2", "value2"]]); + params.forEach(([key, value]) => this.append(key, value)); + } else if (typeof params === 'object') { + //URLSearchParams({ key1: "value1", key2: "value2" }); + Object.entries(params).forEach(([key, value]) => this.append(key, value)); } } append(key: string, value: string): void { - this._searchParams.push([key, value]); + if (!this._searchParams.has(key)) { + this._searchParams.set(key, [value]); // Initialize with an array if key is missing + } else { + this._searchParams.get(key)?.push(value); // Else push the value to the array + } + } + + delete(name: string): void { + this._searchParams.delete(name); } - delete(name: string): empty { - throw new Error('URLSearchParams.delete is not implemented'); + get(name: string): string | null { + const values = this._searchParams.get(name); + return values ? values[0] : null; } - get(name: string): empty { - throw new Error('URLSearchParams.get is not implemented'); + getAll(name: string): string[] { + return this._searchParams.get(name) ?? []; } - getAll(name: string): empty { - throw new Error('URLSearchParams.getAll is not implemented'); + has(name: string): boolean { + return this._searchParams.has(name); } - has(name: string): empty { - throw new Error('URLSearchParams.has is not implemented'); + set(name: string, value: string): void { + this._searchParams.set(name, [value]); } - set(name: string, value: string): empty { - throw new Error('URLSearchParams.set is not implemented'); + keys(): Iterator { + return this._searchParams.keys(); } - sort(): empty { - throw new Error('URLSearchParams.sort is not implemented'); + values(): Iterator { + function* generateValues(params: Map): Iterator { + for (const valueArray of params.values()) { + for (const value of valueArray) { + yield value; + } + } + } + return generateValues(this._searchParams); + } + + entries(): Iterator<[string, string]> { + function* generateEntries( + params: Map, + ): Iterator<[string, string]> { + for (const [key, values] of params) { + for (const value of values) { + yield [key, value]; + } + } + } + + return generateEntries(this._searchParams); + } + + forEach( + callback: (value: string, key: string, searchParams: this) => void, + ): void { + for (const [key, values] of this._searchParams) { + for (const value of values) { + callback(value, key, this); + } + } + } + + sort(): void { + this._searchParams = new Map( + [...this._searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)), + ); } // $FlowFixMe[unsupported-syntax] [Symbol.iterator](): Iterator<[string, string]> { - return this._searchParams[Symbol.iterator](); + const entries: [string, string][] = []; + + for (const [key, values] of this._searchParams) { + for (const value of values) { + entries.push([key, value]); + } + } + + return entries[Symbol.iterator](); } toString(): string { - if (this._searchParams.length === 0) { - return ''; - } - const last = this._searchParams.length - 1; - return this._searchParams.reduce((acc, curr, index) => { - return ( - acc + - encodeURIComponent(curr[0]) + - '=' + - encodeURIComponent(curr[1]) + - (index === last ? '' : '&') - ); - }, ''); + return Array.from(this._searchParams.entries()) + .map(([key, values]) => + values + .map( + value => + `${encodeURIComponent(key).replace(/%20/g, '+')}=${encodeURIComponent( + value, + ).replace(/%20/g, '+')}`, // Convert only spaces to '+' + ) + .join('&'), + ) + .join('&'); } } diff --git a/packages/react-native/Libraries/Blob/URLSearchParams.js.flow b/packages/react-native/Libraries/Blob/URLSearchParams.js.flow index e2966053e3bd42..41a61b75459123 100644 --- a/packages/react-native/Libraries/Blob/URLSearchParams.js.flow +++ b/packages/react-native/Libraries/Blob/URLSearchParams.js.flow @@ -10,14 +10,17 @@ declare export class URLSearchParams { _searchParams: Array<[string, string]>; - constructor(params?: Record): void; + constructor(params?: Record |string|Array<[string, string]> ): void; append(key: string, value: string): void; - delete(name: string): empty; - get(name: string): empty; - getAll(name: string): empty; - has(name: string): empty; - set(name: string, value: string): empty; - sort(): empty; + delete(name: string): void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; + sort(): void; @@iterator(): Iterator<[string, string]>; toString(): string; + keys(): Iterator; + values(): Iterator; + entries(): Iterator<[string, string]>; } diff --git a/packages/react-native/Libraries/Blob/__tests__/URL-test.js b/packages/react-native/Libraries/Blob/__tests__/URL-test.js index a0d3be1b6ab1ab..b5223cd5b2435e 100644 --- a/packages/react-native/Libraries/Blob/__tests__/URL-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/URL-test.js @@ -12,6 +12,8 @@ const URL = require('../URL').URL; +const URLSearchParams = require('../URL').URLSearchParams; + describe('URL', function () { it('should pass Mozilla Dev Network examples', () => { const a = new URL('/', 'https://developer.mozilla.org'); @@ -52,5 +54,80 @@ describe('URL', function () { expect(url.pathname).toBe('/docs/path'); expect(url.port).toBe('8080'); expect(url.search).toBe('?query=testQuery&key=value'); + + // Test searchParams + const searchParams = url.searchParams; + expect(searchParams.get('query')).toBe('testQuery'); + expect(searchParams.get('key')).toBe('value'); + + const paramsFromString = new URLSearchParams( + [ + '?param1=value1', + '¶m2=value2%20with%20spaces', + '¶m3=value3+with+spaces+legacy', + ].join(''), + ); + expect(paramsFromString.get('param1')).toBe('value1'); + expect(paramsFromString.get('param2')).toBe('value2 with spaces'); + expect(paramsFromString.get('param3')).toBe('value3 with spaces legacy'); + expect(paramsFromString.toString()).toBe( + 'param1=value1¶m2=value2+with+spaces¶m3=value3+with+spaces+legacy', + ); + + const paramsFromObject = new URLSearchParams({ + user: 'john', + age: '30', + active: 'true', + }); + + expect(paramsFromObject.get('user')).toBe('john'); + expect(paramsFromObject.get('age')).toBe('30'); + expect(paramsFromObject.get('active')).toBe('true'); + + const valuesArray = Array.from(paramsFromObject.values()); + expect(valuesArray).toEqual(['john', '30', 'true']); + const entriesArray = Array.from(paramsFromObject.entries()); + expect(entriesArray).toEqual([ + ['user', 'john'], + ['age', '30'], + ['active', 'true'], + ]); + + // URLSearchParams: Empty + const emptyParams = new URLSearchParams(''); + expect([...emptyParams.entries()]).toEqual([]); + + // URLSearchParams: Array (for multiple values of the same key) + const paramsFromArray = new URLSearchParams([ + ['key1', 'value1'], + ['key1', 'value2'], + ['key2', 'value3'], + ]); + expect(paramsFromArray.getAll('key1')).toEqual(['value1', 'value2']); + expect(paramsFromArray.get('key2')).toBe('value3'); + + // Manipulating existing search params in the URL + const urlParams = url.searchParams; + expect(urlParams.get('query')).toBe('testQuery'); + expect(urlParams.get('key')).toBe('value'); + + // Adding a new param + urlParams.append('newKey', 'newValue'); + expect(urlParams.get('newKey')).toBe('newValue'); + + // Deleting a param + urlParams.delete('key'); + expect(urlParams.get('key')).toBeNull(); + + // Checking if a param exists + expect(urlParams.has('query')).toBe(true); + expect(urlParams.has('key')).toBe(false); + + // Sorting URLSearchParams + const unsortedParams = new URLSearchParams( + '?z=last&b=second&c=third&a=first', + ); + unsortedParams.sort(); + expect(unsortedParams.toString()).toBe('a=first&b=second&c=third&z=last'); }); }); diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index ae6d0c468e048a..2efdc94cb5af30 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -1303,16 +1303,21 @@ declare export class URL { exports[`public API should not change unintentionally Libraries/Blob/URLSearchParams.js.flow 1`] = ` "declare export class URLSearchParams { - constructor(params?: Record): void; + constructor( + params?: Record | string | Array<[string, string]> + ): void; append(key: string, value: string): void; - delete(name: string): empty; - get(name: string): empty; - getAll(name: string): empty; - has(name: string): empty; - set(name: string, value: string): empty; - sort(): empty; + delete(name: string): void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; + sort(): void; @@iterator(): Iterator<[string, string]>; toString(): string; + keys(): Iterator; + values(): Iterator; + entries(): Iterator<[string, string]>; } " `; diff --git a/packages/rn-tester/js/examples/Urls/UrlExample.js b/packages/rn-tester/js/examples/Urls/UrlExample.js index ed7af41bce7c56..0e29aaeee72a19 100644 --- a/packages/rn-tester/js/examples/Urls/UrlExample.js +++ b/packages/rn-tester/js/examples/Urls/UrlExample.js @@ -33,6 +33,7 @@ function URLComponent(props: Props) { {`pathname: ${parsedUrl.pathname}`} {`port: ${parsedUrl.port}`} {`search: ${parsedUrl.search}`} + {`searchParams: ${parsedUrl.searchParams.toString()}`} ); }