UNPKG

svelte-simple-form

Version:

A lightweight, **type-safe**, and **reactive** form state management hook for **Svelte 5**, featuring:

161 lines (160 loc) 6.09 kB
import { tick, untrack } from 'svelte'; import { z } from 'zod'; import { checkPath, getByPath, getChangedPaths, setByPath } from './helper.js'; import { zodValidator } from './validation/zod.js'; export default function useForm({ initialValues, validation, onSubmit, onChange, onReset }) { if (Object.keys(initialValues || {}).some((k) => k.includes('.'))) { throw new Error('Nested paths are not supported'); } const form = $state({ initialValues, data: initialValues, errors: {}, isValid: true, isSubmitting: false, isDirty: false, touched: {}, setInitialValues: (values, { reset = false } = {}) => { const v = structuredClone($state.snapshot({ ...values })); form.initialValues = v; if (reset) form.reset(); }, setIsDirty: (dirty = true) => { form.isDirty = dirty; }, setIsSubmitting: (submitting = true) => { form.isSubmitting = submitting; }, reset: () => { form.data = structuredClone($state.snapshot(form.initialValues)); tick().then(() => { form.errors = {}; form.touched = {}; form.isValid = true; form.isSubmitting = false; form.isDirty = false; onReset?.(); }); }, resetField: (field) => { if (checkPath(form.initialValues, field)) { const val = getByPath(structuredClone($state.snapshot(form.initialValues)), field); setByPath(form.data, field, val); } else { console.warn(`Field ${field} does not exist on initial values`); setByPath(form.data, field, undefined); } tick().then(() => { Object.keys(form.touched).forEach((key) => { // @ts-ignore if (key.includes('.') && key.startsWith(field)) form.touched[key] = undefined; }); }); }, setError: (field, error) => { form.errors[field] = Array.isArray(error) ? error : [error]; }, validate: (field) => { if (validation?.zod) { let fields = Array.isArray(field) ? field : [field]; const related = validation.relatedFields || {}; const extraFields = fields.map((f) => related[f] || []).flat(); fields = [...new Set([...fields, ...extraFields])]; // Clear current errors for those fields for (const f of fields) { // @ts-ignore delete form.errors[f]; } const errors = zodValidator(validation.zod, form.data, field ? fields : undefined); form.errors = { ...form.errors, ...errors }; } return (Object.keys(form.errors).length === 0 || // @ts-ignore Object.keys(form.errors).every((key) => (form.errors[key]?.length || 0) === 0)); }, submit: async (callback) => { if (form.isSubmitting) return; if (validation) { form.validate(); await tick(); } if (!form.isValid) return; form.isSubmitting = true; if (callback) await callback(form.data); // @ts-ignore else if (onSubmit) await onSubmit($state.snapshot(form.data)); await tick(); form.isSubmitting = false; }, handler: (node) => { node.addEventListener('submit', (event) => { event.preventDefault(); form.submit(); }); } }); $effect(() => { const equal = Object.keys(form.data).every((key) => { // @ts-ignore const eq = JSON.stringify(form.data[key]) === JSON.stringify(form.initialValues[key]); return eq; }); form.isDirty = !equal; }); let prevData = $state.snapshot(form.data); $effect(() => { JSON.stringify(form.data); untrack(async () => { const currentData = structuredClone($state.snapshot(form.data)); const changedFields = getChangedPaths(prevData, currentData); // debug // console.log('Changed fields:', changedFields); for (const path of changedFields) { form.touched[path] = true; const v = $state.snapshot(getByPath(form.data, path)); if (onChange) { onChange(path, v); } // if (validation) { if (!checkPath(form.data, path)) { Object.keys(form.errors).forEach((key) => { // @ts-ignore if (key.includes('.') && key.startsWith(path)) form.errors[key] = undefined; }); Object.keys(form.touched).forEach((key) => { // @ts-ignore if (key.includes('.') && key.startsWith(path)) form.touched[key] = undefined; }); } } } if (validation) { form.validate(changedFields); } prevData = currentData; }); }); $effect(() => { JSON.stringify(form.errors); untrack(() => { form.isValid = Object.keys(form.errors).length === 0 || // @ts-ignore Object.keys(form.errors).every((key) => (form.errors[key]?.length || 0) === 0); }); }); return { form }; }