UNPKG

nuqs-svelte

Version:

Svelte adaptation of the `nuqs` library for managing URL query strings as state.

321 lines (320 loc) 9.7 kB
import { safeParse } from "./utils"; /** * Wrap a set of parse/serialize functions into a builder pattern parser * you can pass to one of the hooks, making its default value type safe. */ export function createParser(parser) { // eslint-disable-next-line @typescript-eslint/no-unused-vars function parseServerSideNullable(value) { if (typeof value === "undefined") { return null; } let str = ""; if (Array.isArray(value)) { // Follow the spec: // https://url.spec.whatwg.org/#dom-urlsearchparams-get if (value[0] === undefined) { return null; } str = value[0]; } if (typeof value === "string") { str = value; } return safeParse(parser.parse, str); } return { eq: (a, b) => a === b, ...parser, parseServerSide: /** parseServerSideNullable */ () => { throw new Error("parseServerSide is not implemented. This parser should not be used"); }, withDefault(defaultValue) { return { ...this, defaultValue, parseServerSide: /** (value) { return parseServerSideNullable(value) ?? defaultValue; }, */ () => { throw new Error("parseServerSide is not implemented. This parser should not be used"); }, }; }, withOptions(options) { return { ...this, ...options, }; }, }; } // Parsers implementations ----------------------------------------------------- export const parseAsString = createParser({ parse: (v) => v, serialize: (v) => `${v}`, }); export const parseAsInteger = createParser({ parse: (v) => { const int = parseInt(v); if (Number.isNaN(int)) { return null; } return int; }, serialize: (v) => Math.round(v).toFixed(), }); export const parseAsIndex = createParser({ parse: (v) => { const int = parseAsInteger.parse(v); if (int === null) { return null; } return int - 1; }, serialize: (v) => parseAsInteger.serialize(v + 1), }); export const parseAsHex = createParser({ parse: (v) => { const int = parseInt(v, 16); if (Number.isNaN(int)) { return null; } return int; }, serialize: (v) => { const hex = Math.round(v).toString(16); return hex.padStart(hex.length + (hex.length % 2), "0"); }, }); export const parseAsFloat = createParser({ parse: (v) => { const float = parseFloat(v); if (Number.isNaN(float)) { return null; } return float; }, serialize: (v) => v.toString(), }); export const parseAsBoolean = createParser({ parse: (v) => v === "true", serialize: (v) => (v ? "true" : "false"), }); function compareDates(a, b) { return a.valueOf() === b.valueOf(); } /** * Querystring encoded as the number of milliseconds since epoch, * and returned as a Date object. */ export const parseAsTimestamp = createParser({ parse: (v) => { const ms = parseInt(v); if (Number.isNaN(ms)) { return null; } return new Date(ms); }, serialize: (v) => v.valueOf().toString(), eq: compareDates, }); /** * Querystring encoded as an ISO-8601 string (UTC), * and returned as a Date object. */ export const parseAsIsoDateTime = createParser({ parse: (v) => { const date = new Date(v); if (Number.isNaN(date.valueOf())) { return null; } return date; }, serialize: (v) => v.toISOString(), eq: compareDates, }); /** * Querystring encoded as an ISO-8601 string (UTC) * without the time zone offset, and returned as * a Date object. * * The Date is parsed without the time zone offset, * making it at 00:00:00 UTC. */ export const parseAsIsoDate = createParser({ parse: (v) => { const date = new Date(v.slice(0, 10)); if (Number.isNaN(date.valueOf())) { return null; } return date; }, serialize: (v) => v.toISOString().slice(0, 10), eq: compareDates, }); /** * String-based enums provide better type-safety for known sets of values. * You will need to pass the parseAsStringEnum function a list of your enum values * in order to validate the query string. Anything else will return `null`, * or your default value if specified. * * Example: * ```ts * enum Direction { * up = 'UP', * down = 'DOWN', * left = 'LEFT', * right = 'RIGHT' * } * * const [direction, setDirection] = useQueryState( * 'direction', * parseAsStringEnum<Direction>(Object.values(Direction)) // pass a list of allowed values * .withDefault(Direction.up) * ) * ``` * * Note: the query string value will be the value of the enum, not its name * (example above: `direction=UP`). * * @param validValues The values you want to accept */ export function parseAsStringEnum(validValues) { return createParser({ parse: (query) => { const asEnum = query; if (validValues.includes(asEnum)) { return asEnum; } return null; }, serialize: (value) => value.toString(), }); } /** * String-based literals provide better type-safety for known sets of values. * You will need to pass the parseAsStringLiteral function a list of your string values * in order to validate the query string. Anything else will return `null`, * or your default value if specified. * * Example: * ```ts * const colors = ["red", "green", "blue"] as const * * const [color, setColor] = useQueryState( * 'color', * parseAsStringLiteral(colors) // pass a readonly list of allowed values * .withDefault("red") * ) * ``` * * @param validValues The values you want to accept */ export function parseAsStringLiteral(validValues) { return createParser({ parse: (query) => { const asConst = query; if (validValues.includes(asConst)) { return asConst; } return null; }, serialize: (value) => value.toString(), }); } /** * Number-based literals provide better type-safety for known sets of values. * You will need to pass the parseAsNumberLiteral function a list of your number values * in order to validate the query string. Anything else will return `null`, * or your default value if specified. * * Example: * ```ts * const diceSides = [1, 2, 3, 4, 5, 6] as const * * const [side, setSide] = useQueryState( * 'side', * parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values * .withDefault(4) * ) * ``` * * @param validValues The values you want to accept */ export function parseAsNumberLiteral(validValues) { return createParser({ parse: (query) => { const asConst = parseFloat(query); if (validValues.includes(asConst)) { return asConst; } return null; }, serialize: (value) => value.toString(), }); } /** * Encode any object shape into the querystring value as JSON. * Note: you may want to use `useQueryStates` for finer control over * multiple related query keys. * * @param runtimeParser Runtime parser (eg: Zod schema) to validate after JSON.parse */ export function parseAsJson(runtimeParser) { return createParser({ parse: (query) => { try { const obj = JSON.parse(query); return runtimeParser(obj); } catch { return null; } }, serialize: (value) => JSON.stringify(value), eq(a, b) { // Check for referential equality first return a === b || JSON.stringify(a) === JSON.stringify(b); }, }); } /** * A comma-separated list of items. * Items are URI-encoded for safety, so they may not look nice in the URL. * * @param itemParser Parser for each individual item in the array * @param separator The character to use to separate items (default ',') */ export function parseAsArrayOf(itemParser, separator = ",") { const itemEq = itemParser.eq ?? ((a, b) => a === b); const encodedSeparator = encodeURIComponent(separator); // todo: Handle default item values and make return type non-nullable return createParser({ parse: (query) => { if (query === "") { // Empty query should not go through the split/map/filter logic, // see https://github.com/47ng/nuqs/issues/329 return []; } return query .split(separator) .map((item, index) => safeParse(itemParser.parse, item.replaceAll(encodedSeparator, separator), `[${index}]`)) .filter((value) => value !== null && value !== undefined); }, serialize: (values) => values .map((value) => { const str = itemParser.serialize ? itemParser.serialize(value) : String(value); return str.replaceAll(separator, encodedSeparator); }) .join(separator), eq(a, b) { if (a === b) { return true; // Referentially stable } if (a.length !== b.length) { return false; } return a.every((value, index) => itemEq(value, b[index])); }, }); }