UNPKG

svelte-formup

Version:
368 lines (362 loc) 13.7 kB
import { getContext, setContext } from 'svelte'; import { writable, derived, get } from 'svelte/store'; import { identity, run_all, listen, prevent_default, subscribe, query_selector_all, toggle_class, noop, blank_object } from 'svelte/internal'; const isString = (value) => typeof value === "string"; const isHTMLFormElement = (node) => node.tagName === "FORM"; const runAll = (callbacks) => { callbacks && run_all(callbacks.filter(Boolean)); }; const asArray = (value) => { if (Array.isArray(value)) return value; if (value) return [value]; return []; }; const validatePath = (path) => isString(path) && path; const findSchemaPathById = (id) => id?.split(/\s+/g).map((id2) => findSchemaPathForElement(document.getElementById(id2))).find(identity); const findSchemaPathForElement = (node) => node && !isHTMLFormElement(node) && (validatePath(node.dataset?.pathAt) || validatePath(node.name) || findSchemaPathById(node.htmlFor) || validatePath(node.id)); const withPathOf = (node, callback, path = findSchemaPathForElement(node)) => { if (path) return callback(path); }; const listenOn = (node, events, listener) => asArray(events).map((event) => listen(node, event, listener)); const VALIDATE_EVENTS = new WeakSet(); const DIRTY_EVENTS = new WeakSet(); const dedupeEvents = (events, listener) => (event) => { if (!events.has(event)) { events.add(event); listener(event); } }; const listenWith = (callback) => (event) => withPathOf(event.target, callback); function validate(context, node, options) { let dispose; const destroy = () => runAll(dispose); const update = (options2 = {}) => { destroy(); if (isString(options2)) options2 = {at: options2}; const { at: path = findSchemaPathForElement(node), debounce = context.debounce, validateOn = options2.on || context.validateOn, dirtyOn = options2.on || options2.validateOn || context.dirtyOn } = options2; const validateAt = (path2) => context.validateAt(path2, {debounce}); const dirtyAt = (path2) => context.setDirtyAt(path2); if (path) { dispose = asArray(path).flatMap((path2) => [ ...listenOn(node, validateOn, dedupeEvents(VALIDATE_EVENTS, () => validateAt(path2))), ...listenOn(node, dirtyOn, dedupeEvents(DIRTY_EVENTS, () => dirtyAt(path2))) ]); } else { if (isHTMLFormElement(node)) { node.noValidate = true; node.autocomplete = context.autocomplete; if (!node.role) node.role = "form"; dispose = [ listen(node, "submit", prevent_default(context.submit)), listen(node, "reset", prevent_default(context.reset)) ]; } dispose = [ ...listenOn(node, validateOn, dedupeEvents(VALIDATE_EVENTS, listenWith(validateAt))), ...listenOn(node, dirtyOn, dedupeEvents(DIRTY_EVENTS, listenWith(dirtyAt))) ]; } dispose.push(context.validity(node, options2).destroy); }; update(options); return {update, destroy}; } const updateToggle = (node, classes, store, toggle, path) => withPathOf(node, (path2) => toggle(node, classes, store, path2), path); const subscribeTo = (path, node, classes, store, toggle) => subscribe(store, (store2) => updateToggle(node, classes, store2, toggle, path)); const subscribeToElements = (node, classes, store, toggle, combine) => subscribe(store, (map) => combine(node, classes, query_selector_all("input,select,textarea,[contenteditable],output,object,button", node).map((element) => updateToggle(element, classes, map, toggle)))); const isFormField = (node) => isHTMLFormElement(node) || "setCustomValidity" in node; const toogleClass = (node, classes, state, key) => { const className = `${isFormField(node) ? "is" : "has"}-${key}`; toggle_class(node, classes[className] || className, state); return state; }; const setCustomValidity = (node, error) => { node.setCustomValidity?.(error?.message || ""); return error; }; const updateDirty = (node, classes, dirty) => { toogleClass(node, classes, !dirty, "pristine"); return toogleClass(node, classes, dirty, "dirty"); }; const updateSuccess = (node, classes, success) => toogleClass(node, classes, success, "success"); const updateError = (node, classes, error) => toogleClass(node, classes, error, "error"); const updateValidating = (node, classes, validating) => toogleClass(node, classes, validating, "validating"); const updateValidity = (node, classes, error) => updateError(node, classes, setCustomValidity(node, error)); const updateStoreDirty = (node, classes, store, path) => updateDirty(node, classes, store.has(path)); const updateStoreValidity = (node, classes, store, path) => updateValidity(node, classes, store.get(path)); const updateStoreSuccess = (node, classes, store, path) => updateSuccess(node, classes, store.has(path)); const updateStoreValidating = (node, classes, store, path) => updateValidating(node, classes, store.has(path)); const useFirstTo = (update) => (node, classes, results) => update(node, classes, results.find(identity)); const useEveryTo = (update) => (node, classes, results) => update(node, classes, results.every(identity)); function validity(context, node, options) { let dispose; const destroy = () => runAll(dispose); const update = (options2 = {}) => { destroy(); if (isString(options2)) options2 = {at: options2}; const {at: path = findSchemaPathForElement(node)} = options2; const classes = {...context.classes, ...options2.classes}; if (path) { dispose = asArray(path).flatMap((path2) => [ subscribeTo(path2, node, classes, context.dirty, updateStoreDirty), subscribeTo(path2, node, classes, context.invalid, updateStoreValidity), subscribeTo(path2, node, classes, context.valid, updateStoreSuccess), subscribeTo(path2, node, classes, context.validating, updateStoreValidating) ]); } else if (isHTMLFormElement(node)) { dispose = [ subscribe(context.isDirty, (dirty) => updateDirty(node, classes, dirty)), subscribe(context.isError, (error) => updateError(node, classes, error)), subscribe(context.isValidating, (validating) => updateValidating(node, classes, validating)), subscribe(context.isSubmitting, (submitting) => toogleClass(node, classes, submitting, "submitting")), subscribe(context.isSubmitted, (submitted) => toogleClass(node, classes, submitted, "submitted")) ]; } else { dispose = [ subscribeToElements(node, classes, context.dirty, updateStoreDirty, useFirstTo(updateDirty)), subscribeToElements(node, classes, context.invalid, updateStoreValidity, useFirstTo(updateValidity)), subscribeToElements(node, classes, context.valid, updateStoreSuccess, useEveryTo(updateSuccess)), subscribeToElements(node, classes, context.validating, updateStoreValidating, useFirstTo(updateValidating)) ]; } }; update(options); return {update, destroy}; } const CONTEXT_KEY = Symbol.for("svelte-formup"); const setAt = (store2, path, value) => store2.update((set) => { set[value ? "add" : "delete"](path); return set; }); const getFormupContext = () => getContext(CONTEXT_KEY); const isEmpty = ({size}) => size === 0; const negate = (value) => !value; const formup = ({ schema, onSubmit = noop, onReset = noop, getInitialValues = blank_object, validateInitialValues = false, state = blank_object(), validateOn = "change", dirtyOn = validateOn, debounce = 100, classes = {}, autocomplete = "off" }) => { const values = writable(getInitialValues()); const errors = writable(new Map()); const dirty = writable(new Set()); const validating = writable(new Set()); const valid = derived([dirty, validating, errors], ([$dirty, $validating, $errors]) => { const $valid = new Set(); for (const field of $dirty) { if (!$errors.has(field) && !$validating.has(field)) { $valid.add(field); } } return $valid; }); const invalid = derived([dirty, validating, errors], ([$dirty, $validating, $errors]) => { const $invalid = new Map(); for (const [field, error] of $errors.entries()) { if ($dirty.has(field) && !$validating.has(field)) { $invalid.set(field, error); } } return $invalid; }); const isSubmitting = writable(false); const isValidating = writable(false); const isSubmitted = writable(false); const isPristine = derived(dirty, isEmpty); const isDirty = derived(isPristine, negate); const isError = derived([isDirty, isValidating, validating, errors], ([$isDirty, $isValidating, $validating, $errors]) => $isDirty && !$isValidating && $validating.size === 0 && $errors.size > 0); const submitCount = writable(0); const setErrorAt = (path, error) => errors.update((errors2) => { if (error) { errors2.set(path, error); } else { errors2.delete(path); } return errors2; }); const setFormError = (error) => setErrorAt("", error); const setDirtyAt = (path, isDirty2 = true) => setAt(dirty, path, isDirty2); const setValidatingAt = (path, isValidating2 = true) => setAt(validating, path, isValidating2); const context = { schema, values, state: writable(state), formError: derived(errors, (errors2) => errors2.get("")), errors, dirty, validating, invalid, valid, isValidating, isSubmitting, isSubmitted, submitCount, isPristine, isDirty, isError, async submit(event) { if (get(isSubmitting)) return; isSubmitted.set(false); isSubmitting.set(true); submitCount.update((count) => count + 1); abortActiveValidateAt(); try { const data = await validate2(); if (data) { const result = await onSubmit(data, context, event); submitCount.set(0); isSubmitted.set(true); return result; } } catch (error) { setFormError(error); } finally { isSubmitting.set(false); } }, reset(event) { if (get(isSubmitting)) return; values.set(getInitialValues(event)); abortActiveValidateAt(); dirty.set(new Set()); errors.set(new Map()); isSubmitted.set(false); submitCount.set(0); if (validateInitialValues) void validate2(event); onReset(context, event); }, setFormError, setErrorAt, setDirtyAt, setValidatingAt, validateAt, validate: (node, options) => validate(context, node, options), validity: (node, options) => validity(context, node, options), validateOn: asArray(validateOn), dirtyOn: asArray(dirtyOn), debounce, classes, autocomplete }; setContext(CONTEXT_KEY, context); let currentValues; subscribe(values, (values2) => { currentValues = values2; }); const validateAtStates = new Map(); let validateAbortController; const createValidateContext = (controller, event) => ({ formup: context, signal: controller.signal, event }); if (validateInitialValues) void validate2(); return context; async function validate2(event) { validateAbortController?.abort(); validateAbortController = new AbortController(); const currentController = validateAbortController; isValidating.set(true); try { const data = await schema.validate(currentValues, { abortEarly: false, strict: false, context: createValidateContext(validateAbortController, event) }); if (currentController === validateAbortController) { errors.set(new Map()); return data; } } catch (error) { if (currentController === validateAbortController) { const newErrors = new Map(); dirty.update((dirty2) => { [].concat(error.inner?.length ? error.inner : error).forEach((error2) => { if (error2.path) { newErrors.set(error2.path, error2); dirty2.add(error2.path); } else { setFormError(error2); } }); return dirty2; }); errors.set(newErrors); } } finally { if (currentController === validateAbortController) { isValidating.set(false); } } } function abortActiveValidateAt() { validateAtStates.forEach((state2) => { clearTimeout(state2.t); if (state2.c) state2.c.abort(); state2.l = 0; state2.c = void 0; state2.t = void 0; }); validating.set(new Set()); } function validateAt(path, {debounce: debounce2 = context.debounce} = {}) { if (get(isSubmitting)) return; let state2 = validateAtStates.get(path); if (!state2) { validateAtStates.set(path, state2 = {l: 0}); } state2.c?.abort(); clearTimeout(state2.t); state2.t = setTimeout(doValidateAt, debounce2, path, state2, debounce2); } async function doValidateAt(path, state2, debounce2) { if (Date.now() - state2.l < debounce2) { return validateAt(path, {debounce: debounce2}); } const current = new AbortController(); state2.c = current; state2.l = Date.now(); setValidatingAt(path); let foundError; try { await schema.validateAt(path, currentValues, { abortEarly: true, strict: true, context: createValidateContext(current) }); } catch (error) { foundError = error; } if (state2.c === current) { state2.c = void 0; setValidatingAt(path, false); setErrorAt(path, foundError); } } }; export { formup, getFormupContext }; //# sourceMappingURL=svelte-formup.js.map