summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui/form.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-14 19:13:06 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-14 19:13:06 +0200
commit8d82ea9c0b7d557ffedf1d59a16ff8fa478ca7a0 (patch)
tree68f88b39b3309eb7b3bbcd0ddae1c9a67053d563 /apps/web/src/components/ui/form.tsx
parent6aa4ffe462fd15e94ec7feb41433b6247133fe4a (diff)
downloadopencut-8d82ea9c0b7d557ffedf1d59a16ff8fa478ca7a0.tar.gz
feat: draft persistence on Form via persistKey
Diffstat (limited to 'apps/web/src/components/ui/form.tsx')
-rw-r--r--apps/web/src/components/ui/form.tsx87
1 files changed, 86 insertions, 1 deletions
diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx
index a6a18752..96dfadc0 100644
--- a/apps/web/src/components/ui/form.tsx
+++ b/apps/web/src/components/ui/form.tsx
@@ -6,16 +6,101 @@ import { type Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
import {
Controller,
type ControllerProps,
+ type DefaultValues,
type FieldPath,
type FieldValues,
FormProvider,
useFormContext,
+ type UseFormReturn,
} from "react-hook-form";
import { cn } from "@/utils/ui";
import { Label } from "./label";
-const Form = FormProvider;
+const DRAFT_DEBOUNCE_MS = 500;
+
+export interface DraftStorage {
+ getItem(key: string): string | null;
+ setItem(key: string, value: string): void;
+ removeItem(key: string): void;
+}
+
+type FormProps<
+ TFieldValues extends FieldValues = FieldValues,
+ TContext = unknown,
+> = UseFormReturn<TFieldValues, TContext> & {
+ children: React.ReactNode;
+ persistKey?: string;
+ storage?: DraftStorage;
+};
+
+function Form<
+ TFieldValues extends FieldValues = FieldValues,
+ TContext = unknown,
+>({
+ persistKey,
+ storage,
+ children,
+ ...methods
+}: FormProps<TFieldValues, TContext>) {
+ const { watch, reset } = methods;
+ // To change keys after mount, re-mount the component with key={persistKey}
+ const persistKeyOnMount = React.useRef(persistKey);
+ const storageOnMount = React.useRef(storage);
+ const resetRef = React.useRef(reset);
+
+ React.useEffect(() => {
+ if (!persistKeyOnMount.current) return;
+ const store = storageOnMount.current ?? window.localStorage;
+ try {
+ const stored = store.getItem(persistKeyOnMount.current);
+ if (stored) {
+ resetRef.current(
+ JSON.parse(stored) as DefaultValues<TFieldValues>,
+ );
+ }
+ } catch {
+ // Storage may be unavailable (private browsing, storage blocked)
+ }
+ }, []);
+
+ React.useEffect(() => {
+ if (!persistKey) return;
+ const store = storageOnMount.current ?? window.localStorage;
+ let timer: ReturnType<typeof setTimeout>;
+ const subscription = watch((values) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ try {
+ store.setItem(persistKey, JSON.stringify(values));
+ } catch {
+ // Storage may be full or blocked
+ }
+ }, DRAFT_DEBOUNCE_MS);
+ });
+ return () => {
+ clearTimeout(timer);
+ subscription.unsubscribe();
+ };
+ }, [persistKey, watch]);
+
+ return <FormProvider {...methods}>{children}</FormProvider>;
+}
+
+export function clearFormDraft({
+ key,
+ storage,
+}: {
+ key: string;
+ storage?: DraftStorage;
+}): void {
+ const store = storage ?? window.localStorage;
+ try {
+ store.removeItem(key);
+ } catch {
+ // Storage may be unavailable
+ }
+}
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,