sveltekit-search-params
Version:
The fastest way to read **AND WRITE** from query search params in [sveltekit](https://github.com/sveltejs/kit).
308 lines (307 loc) • 10.7 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { browser, building } from '$app/environment';
import { goto } from '$app/navigation';
import { page as page_store } from '$app/stores';
import { derived, get, writable, readable, } from 'svelte/store';
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent, } from './lz-string/index.js';
// during building we fake the page store with an URL with no search params
// as it should be during prerendering. This allow the application to still build
// and the client side behavior is still persisted after the build
let page;
if (building) {
page = readable({
url: new URL('https://github.com/paoloricciuti/sveltekit-search-params'),
});
}
else {
page = page_store;
}
const GOTO_OPTIONS = {
keepFocus: true,
noScroll: true,
replaceState: true,
};
const GOTO_OPTIONS_PUSH = {
keepFocus: true,
noScroll: true,
replaceState: false,
};
function mixSearchAndOptions(searchParams, overrides, options) {
const uniqueKeys = Array.from(new Set(Array.from(searchParams?.keys?.() || []).concat(Object.keys(options ?? {}))));
let anyDefaultedParam = false;
return [
Object.fromEntries(uniqueKeys.map((key) => {
if (overrides[key] != undefined) {
return [key, overrides[key]];
}
let fnToCall = (value) => value;
const optionsKey = options?.[key];
if (typeof optionsKey !== 'boolean' &&
typeof optionsKey?.decode === 'function') {
fnToCall = optionsKey.decode;
}
const value = searchParams?.get(key);
let actualValue;
if (value == undefined &&
optionsKey?.defaultValue != undefined) {
actualValue = optionsKey.defaultValue;
anyDefaultedParam = true;
}
else {
actualValue = fnToCall(value);
}
return [key, actualValue];
})),
anyDefaultedParam,
];
}
function isComplexEqual(current, next, equalityFn = (current, next) => JSON.stringify(current) === JSON.stringify(next)) {
return (typeof current === 'object' &&
typeof next === 'object' &&
equalityFn(current, next));
}
function primitiveEncodeAndDecodeOptions({ encode, decode, }) {
function ssp(defaultValue) {
return { encode, decode, defaultValue };
}
return ssp;
}
function objectEncodeAndDecodeOptions(defaultValue) {
return {
encode: (value) => JSON.stringify(value),
decode: (value) => {
if (value === null)
return null;
try {
return JSON.parse(value);
}
catch {
return null;
}
},
defaultValue,
};
}
function arrayEncodeAndDecodeOptions(defaultValue) {
return {
encode: (value) => JSON.stringify(value),
decode: (value) => {
if (value === null)
return null;
try {
return JSON.parse(value);
}
catch {
return null;
}
},
defaultValue,
};
}
function lzEncodeAndDecodeOptions(defaultValue) {
return {
encode: (value) => compressToEncodedURIComponent(JSON.stringify(value)),
decode: (value) => {
if (!value)
return null;
try {
return JSON.parse(decompressFromEncodedURIComponent(value) ?? '');
}
catch {
return null;
}
},
defaultValue,
};
}
export const ssp = {
number: primitiveEncodeAndDecodeOptions({
encode: (value) => value.toString(),
decode: (value) => (value ? parseFloat(value) : null),
}),
boolean: primitiveEncodeAndDecodeOptions({
encode: (value) => value + '',
decode: (value) => value !== null && value !== 'false',
}),
string: primitiveEncodeAndDecodeOptions({
encode: (value) => value ?? '',
decode: (value) => value,
}),
object: objectEncodeAndDecodeOptions,
array: arrayEncodeAndDecodeOptions,
lz: lzEncodeAndDecodeOptions,
};
const batchedUpdates = new Set();
let batchTimeout;
const debouncedTimeouts = new Map();
export function queryParameters(options, { debounceHistory = 0, pushHistory = true, sort = true, showDefaults = true, equalityFn, } = {}) {
const overrides = writable({});
let currentValue;
let firstTime = true;
function _set(value, changeImmediately) {
if (!browser)
return;
firstTime = false;
const hash = window.location.hash;
const query = new URLSearchParams(window.location.search);
const toBatch = (query) => {
for (const field of Object.keys(value)) {
if (value[field] == undefined) {
query.delete(field);
continue;
}
let fnToCall = (value) => value.toString();
const optionsKey = options?.[field];
if (typeof optionsKey !== 'boolean' &&
typeof optionsKey?.encode === 'function') {
fnToCall = optionsKey.encode;
}
const newValue = fnToCall(value[field]);
if (newValue == undefined) {
query.delete(field);
}
else {
query.set(field, newValue);
}
}
};
batchedUpdates.add(toBatch);
clearTimeout(batchTimeout);
batchTimeout = setTimeout(async () => {
batchedUpdates.forEach((batched) => {
batched(query);
});
clearTimeout(debouncedTimeouts.get('queryParameters'));
if (browser) {
overrides.set(value);
async function navigate() {
if (sort) {
query.sort();
}
await goto(`?${query}${hash}`, pushHistory ? GOTO_OPTIONS_PUSH : GOTO_OPTIONS);
overrides.set({});
}
if (changeImmediately || debounceHistory === 0) {
navigate();
}
else {
debouncedTimeouts.set('queryParameters', setTimeout(navigate, debounceHistory));
}
}
batchedUpdates.clear();
});
}
const { subscribe } = derived([page, overrides], ([$page, $overrides], set) => {
const [valueToSet, anyDefaultedParam] = mixSearchAndOptions($page?.url?.searchParams, $overrides, options);
if (anyDefaultedParam && showDefaults) {
_set(structuredClone(valueToSet), firstTime);
}
if (isComplexEqual(currentValue, valueToSet, equalityFn)) {
return;
}
currentValue = structuredClone(valueToSet);
return set(structuredClone(valueToSet));
});
return {
set(newValue) {
_set(newValue);
},
subscribe,
update: (updater) => {
const currentValue = get({ subscribe });
const newValue = updater(currentValue);
_set(newValue);
},
};
}
const DEFAULT_ENCODER_DECODER = {
encode: (value) => value.toString(),
decode: (value) => (value ? value.toString() : null),
};
export function queryParam(name, { encode: encode = DEFAULT_ENCODER_DECODER.encode, decode: decode = DEFAULT_ENCODER_DECODER.decode, defaultValue, } = DEFAULT_ENCODER_DECODER, { debounceHistory = 0, pushHistory = true, sort = true, showDefaults = true, equalityFn, } = {}) {
const override = writable(null);
let firstTime = true;
let currentValue;
function _set(value, changeImmediately) {
if (!browser)
return;
firstTime = false;
const hash = window.location.hash;
const toBatch = (query) => {
if (value == undefined) {
query.delete(name);
}
else {
const newValue = encode(value);
if (newValue == undefined) {
query.delete(name);
}
else {
query.set(name, newValue);
}
}
};
batchedUpdates.add(toBatch);
clearTimeout(batchTimeout);
const query = new URLSearchParams(window.location.search);
batchTimeout = setTimeout(async () => {
batchedUpdates.forEach((batched) => {
batched(query);
});
clearTimeout(debouncedTimeouts.get(name));
if (browser) {
override.set(value);
async function navigate() {
if (sort) {
query.sort();
}
await goto(`?${query}${hash}`, pushHistory ? GOTO_OPTIONS_PUSH : GOTO_OPTIONS);
override.set(null);
}
if (changeImmediately || debounceHistory === 0) {
navigate();
}
else {
debouncedTimeouts.set(name, setTimeout(navigate, debounceHistory));
}
}
batchedUpdates.clear();
});
}
const { subscribe } = derived([page, override], ([$page, $override], set) => {
if ($override != undefined) {
if (isComplexEqual(currentValue, $override, equalityFn)) {
return;
}
currentValue = structuredClone($override);
return set($override);
}
const actualParam = $page?.url?.searchParams?.get?.(name);
if (actualParam == undefined && defaultValue != undefined) {
if (showDefaults) {
_set(structuredClone(defaultValue), firstTime);
}
if (isComplexEqual(currentValue, defaultValue, equalityFn)) {
return;
}
currentValue = structuredClone(defaultValue);
return set(structuredClone(defaultValue));
}
const retval = decode(actualParam);
if (isComplexEqual(currentValue, retval, equalityFn)) {
return;
}
currentValue = structuredClone(retval);
return set(retval);
});
return {
set(newValue) {
_set(newValue);
},
subscribe,
update: (updater) => {
const newValue = updater(currentValue);
_set(newValue);
},
};
}