76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import { computed, signal, type ReadonlySignal, type Signal } from "./signal.ts";
|
|
|
|
export interface FormState<T extends Record<string, unknown>> {
|
|
values: Signal<T>;
|
|
errors: Signal<Partial<Record<keyof T, string>>>;
|
|
submitting: Signal<boolean>;
|
|
dirty: ReadonlySignal<boolean>;
|
|
field<K extends keyof T>(
|
|
name: K,
|
|
): {
|
|
value: T[K];
|
|
onInput(payload: { value?: unknown; checked?: boolean }): void;
|
|
};
|
|
set<K extends keyof T>(name: K, value: T[K]): void;
|
|
reset(next?: T): void;
|
|
validate(): Promise<boolean>;
|
|
submit<R>(operation: (values: T) => R | Promise<R>): Promise<R>;
|
|
}
|
|
|
|
export function useForm<T extends Record<string, unknown>>(
|
|
initial: T,
|
|
validator?: (
|
|
values: T,
|
|
) => Partial<Record<keyof T, string>> | Promise<Partial<Record<keyof T, string>>>,
|
|
): FormState<T> {
|
|
let baseline = structuredClone(initial);
|
|
const values = signal(structuredClone(initial));
|
|
const errors = signal<Partial<Record<keyof T, string>>>({});
|
|
const submitting = signal(false);
|
|
const dirty = computed(() => JSON.stringify(values.get()) !== JSON.stringify(baseline));
|
|
const set = <K extends keyof T>(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);
|
|
}
|
|
},
|
|
};
|
|
}
|