svelte-query-params
Version:
A lightweight, dead-simple, type-safe reactive query parameter store built for Svelte 5.
285 lines (278 loc) • 8.93 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/lib/index.ts
var lib_exports = {};
__export(lib_exports, {
createUseQueryParams: () => createUseQueryParams
});
module.exports = __toCommonJS(lib_exports);
// src/lib/create-params.svelte.ts
var import_svelte = require("svelte");
// src/lib/adapters/browser.ts
function browser(options = {}) {
const { windowObj = window, replace = false } = options;
const replaceState = windowObj.history.replaceState.bind(windowObj.history);
const pushState = windowObj.history.pushState.bind(windowObj.history);
const update = replace ? replaceState : pushState;
return {
isBrowser: () => typeof window !== "undefined",
browser: {
read: () => windowObj.location,
save: (search, hash) => update(null, "", `${search.length ? search : "?"}${hash}`)
},
server: {
save: () => {
}
}
};
}
// src/lib/search-params.ts
var import_reactivity = require("svelte/reactivity");
var ReactiveSearchParams = class extends import_reactivity.SvelteURLSearchParams {
get raw() {
const raw = {};
for (const [key, value] of this.entries()) {
if (key in raw) {
const existing = raw[key];
if (Array.isArray(existing)) {
existing.push(value);
} else {
raw[key] = [existing, value];
}
} else {
raw[key] = value;
}
}
return raw;
}
get uniqueKeys() {
return [...new Set(this.keys())];
}
get search() {
return this.size ? `?${this.toString()}` : "";
}
clear() {
for (const key of this.uniqueKeys) {
this.delete(key);
}
}
/**
* Replaces all query params under the given key with a new array of values
*/
set(key, values) {
if (Array.isArray(values)) {
this.delete(key);
for (const arrayValue of values) {
this.append(key, arrayValue);
}
} else {
super.set(key, values);
}
}
setFromObject(query) {
for (const [key, value] of Object.entries(query)) {
this.set(key, value);
}
}
setFromSearch(query) {
this.clear();
const params = new URLSearchParams(query);
for (const [key, value] of params.entries()) {
this.append(key, value);
}
}
changed(key, value) {
const compare = Array.isArray(value) ? value : [value];
const existing = this.getAll(key).sort();
if (compare.length !== existing.length) return true;
const sortedNew = [...compare].sort();
for (let i = 0; i < existing.length; i++) {
if (existing[i] !== sortedNew[i]) return true;
}
return false;
}
equals(params) {
const paramKeys = Object.keys(params);
if (this.uniqueKeys.length !== paramKeys.length) return false;
for (const key of this.uniqueKeys) {
if (this.changed(key, params[key])) return false;
}
return true;
}
};
// src/lib/utils.ts
var import_valibot = require("valibot");
function parseObject(schemas, input) {
return Object.fromEntries(
Object.entries(schemas).map(([key, schema]) => [
key,
parseValue(key, schema, input[key])
])
);
}
function parseValue(key, schema, value) {
if (typeof schema === "function") return schema(value);
if (isZodSchema(schema)) return schema.parse(value);
if (isValibotSchema(schema)) return (0, import_valibot.parse)(schema, value);
const name = schema.constructor.name;
throw new Error(
`Unknown validator type (${name}) for param "${key}" (value: ${value})`
);
}
function isZodSchema(obj) {
return typeof obj === "object" && obj && "parse" in obj && "safeParse" in obj && typeof obj.parse === "function" && typeof obj.safeParse === "function";
}
function isValibotSchema(obj) {
return typeof obj === "object" && obj && "async" in obj && "kind" in obj && "_run" in obj && obj.kind === "schema" && typeof obj.async === "boolean" && typeof obj._run === "function";
}
function parseQueryParams(params, schemas) {
if (typeof schemas === "function") return schemas(params);
if (isZodSchema(schemas)) return schemas.parse(params);
if (isValibotSchema(schemas)) return (0, import_valibot.parse)(schemas, params);
return parseObject(schemas, params);
}
function mapValues(object, mapFn) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [key, mapFn(value)])
);
}
function debounce(func, delay) {
if (delay === 0) return func;
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
}
// src/lib/create-params.svelte.ts
function createUseQueryParams(validators, options = {}) {
const {
debounce: delay = 0,
windowObj = typeof window === "undefined" ? void 0 : window,
adapter = browser({ windowObj }),
serialise = (value) => typeof value === "string" ? value : JSON.stringify(value)
} = options;
const searchParams = new ReactiveSearchParams();
const parsedQuery = $derived(parseQueryParams(searchParams.raw, validators));
function readFromBrowser() {
searchParams.setFromSearch(adapter.browser.read().search);
}
const persistToBrowser = debounce((search, hash) => {
return (0, import_svelte.tick)().then(() => adapter.browser.save(search, hash));
}, delay);
function persistParams() {
adapter.isBrowser() ? persistToBrowser(searchParams.search, adapter.browser.read().hash) : adapter.server.save(searchParams.search);
}
function serialiseValue(value) {
return Array.isArray(value) ? value.map(serialise) : serialise(value);
}
let unsubscribe;
if (windowObj) {
unsubscribe = addWindowListener(windowObj, readFromBrowser);
}
return function useQueryParams(url) {
searchParams.setFromSearch(url.search);
const params = {};
for (const key of Object.keys(parsedQuery)) {
Object.defineProperty(params, key, {
enumerable: true,
configurable: true,
get() {
return parsedQuery[key];
},
set(newValue) {
const value = serialiseValue(newValue);
if (searchParams.changed(key, value)) {
searchParams.setFromObject({ [key]: value });
persistParams();
}
}
});
}
if (typeof window !== "undefined" && !unsubscribe) {
unsubscribe = addWindowListener(window, readFromBrowser);
}
return [
params,
{
get raw() {
return searchParams.raw;
},
get search() {
return searchParams.search;
},
get all() {
return { ...searchParams.raw, ...parsedQuery };
},
keys() {
return Object.keys(parsedQuery);
},
entries() {
return Object.entries(parsedQuery);
},
set(params2) {
const updated = mapValues(params2, serialiseValue);
if (!searchParams.equals(updated)) {
searchParams.clear();
searchParams.setFromObject(updated);
persistParams();
}
},
update(params2) {
const updated = mapValues(params2, serialiseValue);
if (!searchParams.equals(updated)) {
searchParams.setFromObject(updated);
persistParams();
}
},
remove(...params2) {
params2.map((param) => searchParams.delete(param));
persistParams();
},
unsubscribe() {
return unsubscribe?.();
}
}
];
};
}
function addWindowListener(windowObj, update) {
const replaceState = windowObj.history.replaceState.bind(windowObj.history);
const pushState = windowObj.history.pushState.bind(windowObj.history);
windowObj.history.replaceState = (...args) => {
replaceState(...args);
update();
};
windowObj.history.pushState = (...args) => {
pushState(...args);
update();
};
windowObj.addEventListener("popstate", update);
return () => {
windowObj.removeEventListener("popstate", update);
windowObj.history.pushState = pushState;
windowObj.history.replaceState = replaceState;
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createUseQueryParams
});