import { createContext, createEffect, createSignal, FlowComponent, onCleanup, onMount, useContext, } from "solid-js"; import { createStore } from "solid-js/store"; const makeStore = () => createStore({ valid: true as boolean, values: {} as Record, errors: {} as Record, }); type Store = ReturnType; const ValidationContext = createContext(); export const createValidation = function () { const store = makeStore(); const isValid = createEffect(() => { store[1]("valid", Object.keys(store[0].errors).length === 0); }); const ContextProvider: FlowComponent = (props) => ( {props.children} ); const result = [ContextProvider, store[0]] as [ typeof ContextProvider, typeof store[0] ]; return result; }; const EMPTY_OBJECT = {}; export const validateInput = function ({ value: getValue, invalidate: invalidate, listen, }: { value?: () => any; invalidate?: (v: any) => string | boolean; listen?: { input?: boolean; blur?: boolean; change?: boolean; observer?: boolean; }; } = {}) { // TODO: Default listen values could be set from the ValidationContext listen = listen || { input: true, blur: true, change: false, observer: true }; const store = useContext(ValidationContext); const [validity, setValidity] = createSignal( { valid: true } as any, { equals: false } ); let name = ""; let value: any = null; let init = true; let lastBadInput = false; let isCheckbox = false; let el: HTMLInputElement = undefined!; const onChange = function () { if (!el) return; let nextValue = isCheckbox ? el.checked : el.value; const validity_ = el.validity || EMPTY_OBJECT; if ( init || nextValue !== value || // Update state on badInput (value stays unchanged on badInput), but only update the state once validity_.badInput != lastBadInput ) { if (invalidate) { const invalidateResult = invalidate(nextValue); if (invalidateResult) { el.setCustomValidity( invalidateResult === true ? "Input value is invalid." : invalidateResult ); } else { el.setCustomValidity(""); } } name = name || el.name || el.id; setValidity(validity_); lastBadInput = validity_.badInput; if (validity_.valid) { store && store[1]("errors", name, undefined as any); } else { store && store[1]("errors", name, validity_); } value = nextValue; store && store[1]("values", name, value === undefined ? null : value); } init = false; }; const directive = function (el_: HTMLInputElement) { el = el_; isCheckbox = el.type === "checkbox"; onCleanup(function () { if (name) { store && store[1]("errors", name, undefined as any); store && store[1]("values", name, undefined); } }); if (getValue) { createEffect(function () { getValue(); onChange(); }); } if (listen?.observer) { const observer = new MutationObserver(onChange); onMount(function () { observer.observe(el, { attributes: true }); }); onCleanup(function () { observer.disconnect(); }); } if (listen?.input) { el.addEventListener("input", function () { onChange(); }); } if (listen?.change) { el.addEventListener("change", function () { onChange(); }); } if (listen?.blur) { el.addEventListener("blur", function () { requestAnimationFrame(onChange); }); } }; return [directive, validity, onChange] as [ typeof directive, typeof validity, typeof onChange ]; }; declare module "solid-js" { namespace JSX { interface Directives { validate: boolean; } } }