Skip to content

Commit

Permalink
feat: Add urlKeys & clearOnDefault support for serializer (#720)
Browse files Browse the repository at this point in the history
* feat: Add urlKeys & clearOnDefault support for serializer

Closes #715.

* doc: Add docs for `urlKeys` in serializer

* doc: Wording
  • Loading branch information
franky47 authored Oct 30, 2024
1 parent b8dcc6f commit c701afd
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 12 deletions.
34 changes: 34 additions & 0 deletions packages/docs/content/docs/utilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions packages/nuqs/src/serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
35 changes: 23 additions & 12 deletions packages/nuqs/src/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Options } from './defs'
import type { inferParserType, ParserBuilder } from './parsers'
import { renderQueryString } from './url-encoding'

Expand All @@ -6,7 +7,15 @@ type ParserWithOptionalDefault<T> = ParserBuilder<T> & { defaultValue?: T }

export function createSerializer<
Parsers extends Record<string, ParserWithOptionalDefault<any>>
>(parsers: Parsers) {
>(
parsers: Parsers,
{
clearOnDefault = true,
urlKeys = {}
}: Pick<Options, 'clearOnDefault'> & {
urlKeys?: Partial<Record<keyof Parsers, string>>
} = {}
) {
type Values = Partial<inferParserType<Parsers>>

/**
Expand All @@ -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)
Expand Down

0 comments on commit c701afd

Please sign in to comment.