alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
136 lines (130 loc) • 5.2 kB
text/typescript
import type { TObject } from "alepha";
import { useAlepha } from "alepha/react";
import { useRouter, useRouterState } from "alepha/react/router";
import { useEffect, useRef } from "react";
import type { FormModel } from "../services/FormModel.ts";
export interface UseFormQuerySyncOptions<TKey extends string> {
/**
* Form field keys to mirror to/from the URL query. Fields not listed
* here stay local to the form and never leak into the address bar.
*/
keys: readonly TKey[];
}
/**
* Two-way bind a `useForm` instance to the URL query params, keyed
* per-field (so the URL stays human-readable: `?status=new&zone=ops`).
*
* Direction 1 — URL → form:
* - On mount AND every time one of the watched query keys changes
* (typically via browser back/forward or external `router.push`),
* call `form.input[key].set(value)` for each listed key. Missing /
* empty params clear the field (set to `undefined`).
*
* Direction 2 — form → URL:
* - Subscribe to the form's `form:change` event for the listed keys.
* Each emission writes the current values for all listed keys to the
* URL via `router.setQueryParams` (replace-state, no history spam).
* Empty values (`undefined` / `""`) are stripped from the URL so the
* address bar stays clean.
*
* Replaces ad-hoc `localStorage` filter persistence: the URL becomes
* the canonical store, so the view is shareable via copy-paste and
* navigable via back/forward.
*
* @example
* const form = useForm({ schema: z.object({ status: z.string(), q: z.string() }) });
* useFormQuerySync(form, { keys: ["status", "q"] });
*/
export const useFormQuerySync = <T extends TObject, TKey extends string>(
form: FormModel<T>,
options: UseFormQuerySyncOptions<TKey>,
): void => {
const alepha = useAlepha();
const router = useRouter();
const state = useRouterState();
const keys = options.keys;
// URL → form. Recompute a stable signature of the watched slice of
// router.query so the effect fires precisely when it changes.
const querySig = keys
.map((k) => {
const raw = state.query?.[k];
return typeof raw === "string" ? raw : "";
})
.join("