Skip to content

Instantly share code, notes, and snippets.

@eric-hemasystems
Last active January 31, 2022 21:47
Show Gist options
  • Save eric-hemasystems/8653a4504648091daabc71754c3f821b to your computer and use it in GitHub Desktop.
Save eric-hemasystems/8653a4504648091daabc71754c3f821b to your computer and use it in GitHub Desktop.
Encode Complex Data for URLSearchParams

It's great that JavaScript now has the URLSearchParams object to help manage query string manipulation. Unfortunately the common conventions for encoding complex data (I think started by PHP) are not supported.

This provides a utility function to do that encoding. Might later add a similar function to do parsing. This allows you to keep using the built-in URLSearchParams object (which is integrated into other APIs such as URL), while also not being limited to simple values.

Tests used to verify correct functionality are included. See the tests for usage examples.

/* Conceptually the same as:
*
* search.append(key, data)
*
* Only data can be any value, object or array (mostly supporting nesting).
* Following PHP convention for encoding. `[]` suffix indicates value of array.
* `[key]` suffix indicates object (aka hash, dictionary, associative array,
* etc).
*
* The `prefix` param is more for internal use. Not really supporting nested
* arrays as it gets ambigous. PHP works around this by the fact that arrays
* and associative arrays are really the same thing so a key of 0 can also work
* as the array index. It won't thrown an error if you pass nested arrays but
* the data parsed later may be different than how it was when encoded.
*/
export function assign(search, key, data, prefix='') {
if( Array.isArray(data) ) {
data.forEach((val) => assign(search, `${key}[]`, val, prefix))
} else if( typeof(data) === 'object' ) {
Object.entries(data).forEach(([k, v]) => {
assign(search, `[${k}]`, v, `${prefix}${key}`)
})
} else {
search.append(`${prefix}${key}`, data)
}
}
import { assign } from 'url_search_params'
describe('url search params utilities', ()=> {
let search
beforeEach(()=> search = new URLSearchParams())
// `toString()` encodes the querystring which we want at runtime but for
// testing it is more readable to decode when comparing in tests.
function queryString() { return decodeURI(search.toString()) }
describe('assign', ()=> {
it('encodes a simple value', ()=> {
assign(search, 'foo', 'bar')
expect( queryString() ).toEqual('foo=bar')
})
it('encodes an array of values', ()=> {
assign(search, 'ltrs', ['a', 'b', 'c'])
expect( queryString() ).toEqual('ltrs[]=a&ltrs[]=b&ltrs[]=c')
})
it('encodes an object', ()=> {
assign(search, 'animals', {
cat: 'meow',
dog: 'woof',
cow: 'moo',
})
expect( queryString() ).toEqual('animals[cat]=meow&animals[dog]=woof&animals[cow]=moo')
})
it('encodes an object of arrays', ()=> {
assign(search, 'test', {
words: ['house', 'computer', 'desk'],
numbers: ['one', 'two'],
})
expect( queryString() ).toEqual('test[words][]=house&test[words][]=computer&test[words][]=desk&test[numbers][]=one&test[numbers][]=two')
})
it('encodes arrays with objects', ()=> {
assign(search, 'records', [
{
name: 'John Doe',
age: 18,
},
{
name: 'Jane Jackson',
age: 21,
},
])
expect( queryString() ).toEqual('records[][name]=John+Doe&records[][age]=18&records[][name]=Jane+Jackson&records[][age]=21')
})
it('encodes nested objects', ()=> {
assign(search, 'nested', {
a: { b: 'c' },
d: { e: 'f', g: 'h' },
})
expect( queryString() ).toEqual('nested[a][b]=c&nested[d][e]=f&nested[d][g]=h')
})
it('encodes deeply nested mix of objects and arrays', ()=> {
// We can nest arrays as long as they are not directly nested
const data = {a:[{b:[{c:{e:{f:[{g:'h', i:'j'}]}}}], k: 'l'}, {m: 'n'}]}
assign(search, 'stress_test', data)
const expected = 'stress_test[a][][b][][c][e][f][][g]=h&stress_test[a][][b][][c][e][f][][i]=j&stress_test[a][][k]=l&stress_test[a][][m]=n'
expect( queryString() ).toEqual(expected)
})
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment