feat: draft persistence on Form via persistKey
This commit is contained in:
parent
6aa4ffe462
commit
8d82ea9c0b
|
|
@ -6,16 +6,101 @@ import { type Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
type ControllerProps,
|
type ControllerProps,
|
||||||
|
type DefaultValues,
|
||||||
type FieldPath,
|
type FieldPath,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
useFormContext,
|
||||||
|
type UseFormReturn,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { Label } from "./label";
|
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<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue