UNPKG

tsurih

Version:

Svelte form validation library

316 lines (251 loc) 6.37 kB
import { Writable, get, writable } from "svelte/store"; import type { TConfig, TContext, TElement, TEventName, TValidator, } from "./types.js"; import { debounce, isArray, isNumber, isObject, isString, validators, } from "./utils.js"; const scrollToElement = (el: TElement | undefined) => { if (!el) { return; } el.focus(); el.scrollIntoView({ block: "center", behavior: "smooth", }); }; const unwrapFromMap = (map: any) => { const newObj = [...map.entries()].reduce<any>((acc, arr) => { acc[arr[0]] = arr[1]; return acc; }, {}); return newObj; }; export const tsurih = < TValues extends Record<string, unknown>, TSchema extends Record< keyof TValues, TValidator<TValues, keyof TValues, TConfig<TValues, TSchema>>[] > >( defaultValues: TValues ) => { const keysOfValues = Object.keys(defaultValues); type TCfg = TConfig<TValues, TSchema>; let config: TCfg = { form: undefined, dirty: new Map(), elements: new Set(), strategy: "blur", fields: new Map(), values: writable({}), errors: writable({}), isSubmitting: writable(false), defaultValues: {}, schema: undefined, }; config.values.set(defaultValues as TValues); for (const key of keysOfValues) { config.fields.set(key, key); config.dirty.set(key, false); } type TCtx<TFieldName extends keyof TValues> = TContext< TValues, TFieldName, TCfg >; const validate = async (ctx: TCtx<any>) => { if (ctx.isDirty && ctx.name && config.schema?.[ctx.name]) { await checkValidators(ctx, config.schema[ctx.name]); } }; const checkValidators = async ( ctx: TCtx<any>, validators: TValidator<TValues, TCtx<typeof ctx.name>["name"], TCfg>[] ) => { if (!validators || !isArray(validators)) { return; } for (const v of validators) { const result = await v(ctx); if (result) { ctx.setError(ctx.name as string, result); break; } else { ctx.setError(ctx.name as string, ""); } } }; const revalidate = async (fieldName: keyof TValues) => { if (!config.dirty.get(fieldName)) { config.dirty.set(fieldName, true); } const ctx = await createCtx(fieldName, get(config.values)[fieldName]); if (config.schema?.[fieldName]) { await checkValidators(ctx, config.schema?.[fieldName]); } }; const setError = (name: keyof TValues, message: string) => { if (name) { config.errors.update((state) => { state[name] = message; return { ...state, }; }); } }; const createCtx = <T extends keyof TValues>(name: T, value?: any) => { const ctx: TCtx<T> = { config, name, value, setError, isDirty: config.dirty.get(name) === true, }; return ctx; }; const onEvent = (type: "input" | "blur") => { return async (e: Event) => { const target: any = e.target || {}; if (target.name) { const typedName = target?.name as keyof TValues; const value = target?.type === "checkbox" ? target?.checked : target?.value; if (config.strategy === type && !config.dirty.get(typedName)) { config.dirty.set(typedName, true); } const ctx = createCtx(typedName, value); await validate(ctx); } }; }; const checkErrors = () => { let firstErrorName = ""; const errors = get(config.errors); for (const prop in errors) { if (errors.hasOwnProperty(prop) && errors[prop]) { firstErrorName = prop; break; } } if (firstErrorName) { const firstErrorElement = [...config.elements.values()].find( (el) => el.name === firstErrorName ); scrollToElement(firstErrorElement); return false; } return true; }; const onSubmit = ( callback?: | ((values: TValues) => void) | ((values: TValues) => Promise<void>) ) => { return async (e: Event) => { e.preventDefault(); e.stopPropagation(); if (!validate) { return; } fieldRegistration(); if (config.elements.size === 0) { return; } config.isSubmitting.set(true); let isValid = false; const values = get(config.values); for (const name in values) { if (!values.hasOwnProperty(name)) { continue; } if (!config.dirty.get(name)) { config.dirty.set(name, true); } const ctx = createCtx(name, values[name]); if (config.schema?.[name]) { await checkValidators(ctx, config.schema?.[name]); } } isValid = checkErrors(); if (isValid && callback) { callback(values as TValues); } config.isSubmitting.set(false); }; }; const reset = () => { config.dirty.clear(); config.values.set(config.defaultValues); config.elements.clear(); config.errors.set({}); }; const fieldRegistration = () => { if (!config.form) { return; } const els = config.form.querySelectorAll("[name]"); if (!els || els.length === 0) { return; } Array.from(els).forEach((el) => { const typedEl = el as TElement; if (!config.elements.has(typedEl)) { config.elements.add(typedEl); } }); config.elements.forEach((element) => { element.oninput = onEvent("input"); element.onblur = onEvent("blur"); }); }; const setSchema = (schema: TSchema) => { config.schema = schema; return returnObject; }; const setStrategy = (v: TEventName) => { config.strategy = v; return returnObject; }; const returnObject = { reset, reg: (passedForm: HTMLFormElement) => { config.form = passedForm; fieldRegistration(); }, getFormEl: () => config.form, errors: config.errors, onSubmit, isSubmitting: config.isSubmitting, fields: unwrapFromMap(config.fields) as Partial< Record<keyof TValues, keyof TValues> >, values: config.values as Writable<TValues>, revalidate, setSchema, setStrategy, }; return returnObject; }; export { TConfig, TContext, TEventName, debounce, isArray, isNumber, isObject, isString, validators, };