import { computed, signal, type ReadonlySignal, type Signal } from "./signal.ts"; export interface FormState> { values: Signal; errors: Signal>>; submitting: Signal; dirty: ReadonlySignal; field( name: K, ): { value: T[K]; onInput(payload: { value?: unknown; checked?: boolean }): void; }; set(name: K, value: T[K]): void; reset(next?: T): void; validate(): Promise; submit(operation: (values: T) => R | Promise): Promise; } export function useForm>( initial: T, validator?: ( values: T, ) => Partial> | Promise>>, ): FormState { let baseline = structuredClone(initial); const values = signal(structuredClone(initial)); const errors = signal>>({}); const submitting = signal(false); const dirty = computed(() => JSON.stringify(values.get()) !== JSON.stringify(baseline)); const set = (name: K, value: T[K]) => { values.set({ ...values.get(), [name]: value }); if (errors.get()[name]) errors.set({ ...errors.get(), [name]: undefined }); }; const validate = async () => { errors.set((await validator?.(values.get())) ?? {}); return Object.values(errors.get()).every((value) => !value); }; return { values, errors, submitting, dirty, field(name) { return { value: values.get()[name], onInput(payload) { const current = values.get()[name]; set( name, (typeof current === "boolean" ? Boolean(payload.checked) : payload.value) as T[typeof name], ); }, }; }, set, reset(next = initial) { baseline = structuredClone(next); values.set(structuredClone(next)); errors.set({}); }, validate, async submit(operation) { if (!(await validate())) throw new Error("WRN-FORM-INVALID"); submitting.set(true); try { return await operation(values.get()); } finally { submitting.set(false); } }, }; }