From c701afd26107d7137a283477097d52a1a7226286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Wed, 30 Oct 2024 10:28:24 +0100 Subject: [PATCH] feat: Add urlKeys & clearOnDefault support for serializer (#720) * feat: Add urlKeys & clearOnDefault support for serializer Closes #715. * doc: Add docs for `urlKeys` in serializer * doc: Wording --- packages/docs/content/docs/utilities.mdx | 34 ++++++++++++++++ packages/nuqs/src/serializer.test.ts | 49 ++++++++++++++++++++++++ packages/nuqs/src/serializer.ts | 35 +++++++++++------ 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/packages/docs/content/docs/utilities.mdx b/packages/docs/content/docs/utilities.mdx index eec7f560..424c6c4b 100644 --- a/packages/docs/content/docs/utilities.mdx +++ b/packages/docs/content/docs/utilities.mdx @@ -62,6 +62,40 @@ serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar ``` +### Shorter search params keys + +Just like [`useQueryStates{:ts}`](./batching#shorter-search-params-keys), you can +specify a `urlKeys{:ts}` object to map the variable names defined by the parsers +to shorter keys in the URL: + +```ts +const serialize = createSerializer( + { + // 1. Use variable names that make sense for your domain/business logic + latitude: parseAsFloat, + longitude: parseAsFloat, + zoomLevel: parseAsInteger + }, + { + // 2. Remap them to shorter keys in the URL + urlKeys: { + latitude: 'lat', + longitude: 'lng', + zoomLevel: 'z' + } + } +) + +// 3. Use your variable names when calling the serializer, +// and the shorter keys will be rendered in the URL: +serialize({ + latitude: 45.18, + longitude: 5.72, + zoomLevel: 12 +}) +// ?lat=45.18&lng=5.72&z=12 +``` + ## Parser type inference To access the underlying type returned by a parser, you can use the diff --git a/packages/nuqs/src/serializer.test.ts b/packages/nuqs/src/serializer.test.ts index b147af6e..e924cc66 100644 --- a/packages/nuqs/src/serializer.test.ts +++ b/packages/nuqs/src/serializer.test.ts @@ -125,4 +125,53 @@ describe('serializer', () => { '?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}' ) }) + test('support for global clearOnDefault option', () => { + const serialize = createSerializer( + { + int: parseAsInteger.withDefault(0), + str: parseAsString.withDefault(''), + bool: parseAsBoolean.withDefault(false), + arr: parseAsArrayOf(parseAsString).withDefault([]), + json: parseAsJson(x => x).withDefault({ foo: 'bar' }) + }, + { clearOnDefault: false } + ) + const result = serialize({ + int: 0, + str: '', + bool: false, + arr: [], + json: { foo: 'bar' } + }) + expect(result).toBe( + '?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}' + ) + }) + test('parser clearOnDefault takes precedence over global clearOnDefault', () => { + const serialize = createSerializer( + { + int: parseAsInteger + .withDefault(0) + .withOptions({ clearOnDefault: true }), + str: parseAsString.withDefault('') + }, + { clearOnDefault: false } + ) + const result = serialize({ + int: 0, + str: '' + }) + expect(result).toBe('?str=') + }) + test('supports urlKeys', () => { + const serialize = createSerializer(parsers, { + urlKeys: { + bool: 'b', + int: 'i', + str: 's' + } + }) + const result = serialize({ str: 'foo', int: 1, bool: true }) + expect(result).toBe('?s=foo&i=1&b=true') + }) }) diff --git a/packages/nuqs/src/serializer.ts b/packages/nuqs/src/serializer.ts index 09c582c8..6b9b2a54 100644 --- a/packages/nuqs/src/serializer.ts +++ b/packages/nuqs/src/serializer.ts @@ -1,3 +1,4 @@ +import type { Options } from './defs' import type { inferParserType, ParserBuilder } from './parsers' import { renderQueryString } from './url-encoding' @@ -6,7 +7,15 @@ type ParserWithOptionalDefault = ParserBuilder & { defaultValue?: T } export function createSerializer< Parsers extends Record> ->(parsers: Parsers) { +>( + parsers: Parsers, + { + clearOnDefault = true, + urlKeys = {} + }: Pick & { + urlKeys?: Partial> + } = {} +) { type Values = Partial> /** @@ -23,36 +32,38 @@ export function createSerializer< */ function serialize(base: Base, values: Values | null): string function serialize( - baseOrValues: Base | Values | null, - values: Values | null = {} + arg1BaseOrValues: Base | Values | null, + arg2values: Values | null = {} ) { - const [base, search] = isBase(baseOrValues) - ? splitBase(baseOrValues) + const [base, search] = isBase(arg1BaseOrValues) + ? splitBase(arg1BaseOrValues) : ['', new URLSearchParams()] - const vals = isBase(baseOrValues) ? values : baseOrValues - if (vals === null) { + const values = isBase(arg1BaseOrValues) ? arg2values : arg1BaseOrValues + if (values === null) { for (const key in parsers) { - search.delete(key) + const urlKey = urlKeys[key] ?? key + search.delete(urlKey) } return base + renderQueryString(search) } for (const key in parsers) { const parser = parsers[key] - const value = vals[key] + const value = values[key] if (!parser || value === undefined) { continue } + const urlKey = urlKeys[key] ?? key const isMatchingDefault = parser.defaultValue !== undefined && (parser.eq ?? ((a, b) => a === b))(value, parser.defaultValue) if ( value === null || - ((parser.clearOnDefault ?? true) && isMatchingDefault) + ((parser.clearOnDefault ?? clearOnDefault ?? true) && isMatchingDefault) ) { - search.delete(key) + search.delete(urlKey) } else { - search.set(key, parser.serialize(value)) + search.set(urlKey, parser.serialize(value)) } } return base + renderQueryString(search)