-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat:
encodeState
and decodeState
functions
- Loading branch information
1 parent
e336827
commit abc4dcb
Showing
10 changed files
with
259 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
export { | ||
encodeState, | ||
decodeState, | ||
encode, | ||
decode, | ||
type Type, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { encodeState, decodeState } from './encodeState'; | ||
|
||
describe('encodeState', () => { | ||
it('should encode a simple state object', () => { | ||
const state = { str: 'test', num: 123, bool: true }; | ||
const expected = 'str=test&num=%E2%88%93123&bool=%F0%9F%97%B5true'; | ||
expect(encodeState(state)).toEqual(expected); | ||
}); | ||
|
||
it('should encode a nested state object', () => { | ||
const state = { | ||
str: 'test', | ||
num: 123, | ||
nested: { bool: true, arr: [1, 2, 3] }, | ||
}; | ||
const expected = | ||
'str=test&num=%E2%88%93123&nested=%7B%27bool%27%3A%27%F0%9F%97%B5true%27%2C%27arr%27%3A%5B%27%E2%88%931%27%2C%27%E2%88%932%27%2C%27%E2%88%933%27%5D%7D'; | ||
expect(encodeState(state)).toEqual(expected); | ||
}); | ||
|
||
it('should encode a state object with default values', () => { | ||
const state = { str: 'test', num: 123, bool: true }; | ||
const defaults = { str: '', num: 0, bool: false }; | ||
const expected = 'str=test&num=%E2%88%93123&bool=%F0%9F%97%B5true'; | ||
|
||
expect(encodeState(state, defaults)).toEqual(expected); | ||
expect(encodeState({ ...state, bool: false }, defaults)).toEqual( | ||
'str=test&num=%E2%88%93123', | ||
); | ||
expect(encodeState({ ...state, bool: false, str: '' }, defaults)).toEqual( | ||
'num=%E2%88%93123', | ||
); | ||
}); | ||
|
||
it('should preserve existing params', () => { | ||
const state = { str: 'test', num: 123, bool: true }; | ||
const existing = new URLSearchParams('key1=value1&key2=value2'); | ||
const defaults = { str: '', num: 0, bool: false }; | ||
|
||
expect(encodeState(state, defaults, existing)).toEqual( | ||
'key1=value1&key2=value2&str=test&num=%E2%88%93123&bool=%F0%9F%97%B5true', | ||
); | ||
expect( | ||
encodeState(state, null as unknown as typeof state, existing), | ||
).toEqual( | ||
'key1=value1&key2=value2&str=test&num=%E2%88%93123&bool=%F0%9F%97%B5true', | ||
); | ||
expect( | ||
encodeState(state, defaults, null as unknown as URLSearchParams), | ||
).toEqual('str=test&num=%E2%88%93123&bool=%F0%9F%97%B5true'); | ||
}); | ||
|
||
it('should return an empty string for an empty state object', () => { | ||
expect(encodeState({})).toEqual(''); | ||
expect(encodeState(undefined as unknown as object)).toEqual(''); | ||
expect(encodeState(null as unknown as object)).toEqual(''); | ||
}); | ||
}); | ||
|
||
describe('decodeState', () => { | ||
it('should decode a simple state object', () => { | ||
const uriString = 'key1=value1&key2=value2'; | ||
const expected = { key1: 'value1', key2: 'value2' }; | ||
expect(decodeState(uriString)).toEqual(expected); | ||
}); | ||
|
||
it('should decode a state object with nested values', () => { | ||
const uriString = 'key1=value1&key2={"nestedKey":"nestedValue"}'; | ||
const expected = { key1: 'value1', key2: { nestedKey: 'nestedValue' } }; | ||
expect(decodeState(uriString)).toEqual(expected); | ||
}); | ||
|
||
it('should decode a state object with default values', () => { | ||
const expected = { key1: 'value1', key2: 'value2' }; | ||
const defaults = { key1: '', key2: '' }; | ||
expect(decodeState('', defaults)).toEqual(defaults); | ||
expect(decodeState('key1=value1', defaults)).toEqual({ | ||
...expected, | ||
key2: defaults.key2, | ||
}); | ||
}); | ||
|
||
it('should return an empty object for an empty URI string', () => { | ||
expect(decodeState('')).toEqual({}); | ||
expect(decodeState(undefined as unknown as string)).toEqual({}); | ||
expect(decodeState(null as unknown as string)).toEqual({}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { encode, decode } from './encoder'; | ||
import { type JSONCompatible, getParams } from './utils'; | ||
|
||
/** | ||
* Encodes the state object into a URL query string. | ||
* | ||
* @param {JSONCompatible<T>} state - The state object to encode. | ||
* @param {JSONCompatible<T>} [defaults] - Optional default values for the state object. | ||
* @return {string} The encoded URL query string representing the state object. | ||
* | ||
* * Example: | ||
* ```ts | ||
* export const form = { name: '' }; | ||
* encodeState({ name: 'test' }, form, 'someExistingParam=123'); | ||
* ``` | ||
*/ | ||
export function encodeState<T>( | ||
state: never extends T ? object : JSONCompatible<T>, | ||
defaults?: JSONCompatible<T>, | ||
paramsToKeep?: string | URLSearchParams, | ||
) { | ||
const params = getParams(paramsToKeep); | ||
Object.entries(state || {}).forEach(([key, value]) => { | ||
const initialVal = defaults?.[key as keyof typeof defaults]; | ||
if (JSON.stringify(value) !== JSON.stringify(initialVal)) { | ||
params.set(key, encode(value)); | ||
} | ||
}); | ||
return params.toString(); | ||
} | ||
|
||
/** | ||
* Decodes a URI string into an object of type T. | ||
* | ||
* @param {string} uriString - The URI string to decode. | ||
* @return {T} The decoded object of type T. | ||
* | ||
* * Example: | ||
* ```ts | ||
* export const form = { name: '' }; | ||
* decodeState('key=value&name=Alex', form); | ||
* ``` | ||
*/ | ||
export function decodeState<T>( | ||
uriString: string | URLSearchParams, | ||
defaults?: never extends T ? object : JSONCompatible<T>, | ||
) { | ||
return { | ||
...(defaults || {}), | ||
...Object.fromEntries( | ||
[...getParams(uriString).entries()].map(([key, value]) => [ | ||
key, | ||
decode(value) ?? defaults?.[key as keyof typeof defaults], | ||
]), | ||
), | ||
} as never extends T ? object : T; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,48 @@ | ||
import React from 'react'; | ||
import { typeOf, type JSONCompatible, type DeepReadonly } from './utils'; | ||
import { encode, decode } from './encoder'; | ||
import { encodeState, decodeState } from './encodeState'; | ||
|
||
/** | ||
* Returns stringify and parse functions | ||
* A hook that returns stringify and parse functions for encoding and decoding state | ||
* to and from URL search parameters. | ||
* | ||
* @param {?JSONCompatible<T>} [stateShape] Optional fallback values for state | ||
* @template T - The type of the state shape | ||
* @param {JSONCompatible<T>} [stateShape] Optional fallback values for state | ||
* @returns {} `stringify` and `parse` functions | ||
* | ||
* * Example: | ||
* ```ts | ||
* export const form = { name: '' }; | ||
* const { parse, stringify } = useUrlEncode(form); | ||
* | ||
* stringify({ name: 'John' }, 'someExistingParamToKeep=123'); | ||
* // by default it's uses router.push with scroll: false | ||
* parse('name=Tom'); | ||
* ``` | ||
*/ | ||
export function useUrlEncode<T>(stateShape?: JSONCompatible<T>) { | ||
const stringify = React.useCallback( | ||
function ( | ||
state: | ||
| DeepReadonly<NonNullable<typeof stateShape>> | ||
| NonNullable<typeof stateShape>, | ||
strOrSearchParams?: string | URLSearchParams, | ||
paramsToKeep?: string | URLSearchParams, | ||
): string { | ||
const params = getParams(strOrSearchParams); | ||
if (typeOf(state) === 'object') { | ||
Object.entries(state).forEach(([key, value]) => { | ||
const initialVal = stateShape?.[key as keyof typeof stateShape]; | ||
if (JSON.stringify(value) !== JSON.stringify(initialVal)) { | ||
params.set(key, encode(value)); | ||
} else { | ||
params.delete(key); | ||
} | ||
}); | ||
} | ||
|
||
return params.toString(); | ||
return typeOf(state) === 'object' | ||
? encodeState(state, stateShape, paramsToKeep) | ||
: ''; | ||
}, | ||
[stateShape], | ||
); | ||
|
||
const parse = React.useCallback( | ||
function (strOrSearchParams?: string | URLSearchParams) { | ||
const params = getParams(strOrSearchParams); | ||
return { | ||
...stateShape, | ||
...Object.fromEntries( | ||
[...params.entries()].map(([key, value]) => [ | ||
key, | ||
decode(value) ?? stateShape?.[key as keyof typeof stateShape], | ||
]), | ||
), | ||
} as DeepReadonly<NonNullable<typeof stateShape>>; | ||
function (strOrSearchParams: string | URLSearchParams) { | ||
return decodeState(strOrSearchParams, stateShape) as DeepReadonly< | ||
NonNullable<typeof stateShape> | ||
>; | ||
}, | ||
[stateShape], | ||
); | ||
|
||
return { parse, stringify }; | ||
} | ||
|
||
const getParams = (strOrSearchParams?: string | URLSearchParams) => | ||
new URLSearchParams( | ||
typeof strOrSearchParams === 'string' | ||
? strOrSearchParams | ||
: strOrSearchParams?.toString?.() || '', | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters