Skip to content

Instantly share code, notes, and snippets.

@PuruVJ
Created April 4, 2025 10:39
Show Gist options
  • Save PuruVJ/ed0abefbf512ff2aaa5dbbd377c50f5a to your computer and use it in GitHub Desktop.
Save PuruVJ/ed0abefbf512ff2aaa5dbbd377c50f5a to your computer and use it in GitHub Desktop.
import { replaceState } from '$app/navigation';
import { page } from '$app/state';
import { tick } from 'svelte';
export function box<T>(getter: () => T, setter?: (value: T) => void) {
let derived_val = $derived(getter());
$effect(() => {
setter?.($state.snapshot(derived_val) as T);
});
return {
get current() {
return derived_val;
},
set current(value) {
derived_val = value;
}
};
}
type Serde<T> = {
encode: (value: T) => string;
decode: (value: string) => T;
default?: T;
};
export const QueryParamSerde = {
boolean(default_value?: boolean): Serde<boolean> {
return {
encode: (v) => (v ? 'true' : 'false'),
decode: (v) => v === 'true',
default: default_value
};
},
string<T extends string = string>(default_value?: T): Serde<T> {
return {
encode: (v) => v,
decode: (v) => v as T,
default: default_value
};
},
number(default_value?: number): Serde<number> {
return {
encode: (v) => v + '',
decode: (v) => +v,
default: default_value
};
},
array<T extends string | number = string>(default_value?: T[]): Serde<T[]> {
return {
encode: (v) => v.join(','),
decode: (v) => v.split(',').filter(Boolean) as T[],
default: default_value
};
}
};
export function reactive_query_params<T extends Record<string, Serde<any>>>(
params: T
): {
[K in keyof T]: T[K] extends Serde<infer U>
? T[K]['default'] extends undefined
? U | undefined
: U
: never;
} & {
url_from<K extends keyof T>(field: K, value: T[K] extends Serde<infer U> ? U : never): string;
} {
const boxes: Record<string, ReturnType<typeof box<any>>> = {};
for (const [key, value] of Object.entries(params)) {
boxes[key] = box<any>(
() => {
const param_value = page.url.searchParams.get(key);
if (param_value) {
return value.decode(param_value);
}
if (value.default !== undefined) {
return value.default;
}
return value.decode('');
},
(val) => {
const encoded = val != null ? value.encode(val) : undefined;
if ((value.default == null || value.encode(value.default) !== encoded) && encoded) {
page.url.searchParams.set(key, encoded);
} else {
page.url.searchParams.delete(key);
}
// So we don't run it when router hasn't initialized yet
tick().then(() => replaceState(page.url, {}));
}
);
}
const returned = {} as ReturnType<typeof reactive_query_params>;
for (const [key, value] of Object.entries(boxes)) {
Object.defineProperty(returned, key, {
get() {
return value.current;
},
set(val: T) {
value.current = val;
}
});
}
returned.url_from = <K extends keyof T>(
field: K,
value: T[K] extends Serde<infer U> ? U : never
): string => {
const encoded = value != null ? params[field as string].encode(value) : undefined;
const new_url = new URL(page.url);
if (encoded) {
new_url.searchParams.set(field as string, encoded);
} else {
new_url.searchParams.delete(field as string);
}
return new_url.pathname + new_url.search;
};
return returned as any;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment