import { get, writable } from 'svelte/store'; import { z } from 'zod/v4'; export type FormInput = { value: T; error: string | null; required: boolean; }; type FormInputs = { [K in keyof T]: FormInput; }; export function createForm>(schema: T, initialValues: z.infer) { // Create a writable store for the inputs const inputsStore = writable>>(initializeInputs(initialValues)); const errorsStore = writable | undefined>(); function initializeInputs(initialValues: z.infer): FormInputs> { const inputs: FormInputs> = {} as FormInputs>; const shape = schema instanceof z.ZodObject ? (schema.shape as Record) : {}; for (const key in initialValues) { if (Object.prototype.hasOwnProperty.call(initialValues, key)) { const fieldSchema = shape[key]; inputs[key as keyof z.infer] = { value: initialValues[key as keyof z.infer], error: null, required: fieldSchema ? isRequired(fieldSchema) : false }; } } return inputs; } function validate() { let success = true; inputsStore.update((inputs) => { const values = Object.fromEntries( Object.entries(inputs).map(([key, input]) => [key, input.value]) ); const result = schema.safeParse(values); errorsStore.set(result.error); if (!result.success) { success = false; for (const input of Object.keys(inputs)) { const error = result.error.issues.find((e) => e.path[0] === input); if (error) { inputs[input as keyof z.infer].error = error.message; } else { inputs[input as keyof z.infer].error = null; } } } else { for (const input of Object.keys(inputs)) { inputs[input as keyof z.infer].error = null; } } for (const key in result.data) { if (Object.prototype.hasOwnProperty.call(inputs, key)) { inputs[key as keyof z.infer].value = result.data[key]; } } return inputs; }); return success ? data() : null; } function data() { const inputs = get(inputsStore); const values = Object.fromEntries( Object.entries(inputs).map(([key, input]) => { input.value = trimValue(input.value); return [key, input.value]; }) ) as z.infer; return values; } function reset() { inputsStore.update((inputs) => { for (const input of Object.keys(inputs)) { const current = inputs[input as keyof z.infer]; inputs[input as keyof z.infer] = { ...current, value: initialValues[input as keyof z.infer], error: null }; } return inputs; }); } function setValue(key: keyof z.infer, value: z.infer[keyof z.infer]) { inputsStore.update((inputs) => { inputs[key].value = value; return inputs; }); } function trimValue(value: any) { if (typeof value === 'string') { value = value.trim(); } else if (Array.isArray(value)) { value = value.map((item: any) => { if (typeof item === 'string') { return item.trim(); } return item; }); } return value; } function isRequired(fieldSchema: z.ZodTypeAny): boolean { // Handle string allow empty if (fieldSchema instanceof z.ZodString) { return fieldSchema.minLength !== null && fieldSchema.minLength > 0; } // Handle unions like callbackUrlSchema if (fieldSchema instanceof z.ZodUnion) { return !fieldSchema.def.options.some((o: any) => { return o.def.type == 'optional'; }); } // Handle pipes like emptyToUndefined if (fieldSchema instanceof z.ZodPipe) { return isRequired(fieldSchema.def.out as z.ZodTypeAny); } // Handle the normal cases if (fieldSchema instanceof z.ZodOptional || fieldSchema instanceof z.ZodDefault) { return false; } return true; } return { schema, inputs: inputsStore, errors: errorsStore, data, validate, setValue, reset }; }