UNPKG

@vaadin/hilla-react-form

Version:

Hilla form utils for React

192 lines 5.65 kB
import { _fromString, _validity, BinderRoot, CHANGED, getBinderNode, getDefaultFieldStrategy, hasFromString, isFieldElement } from "@vaadin/hilla-lit-form"; import { useEffect, useMemo, useReducer, useRef } from "react"; ((feature, vaadinObj = globalThis.Vaadin ??= {}) => { vaadinObj.registrations ??= []; vaadinObj.registrations.push({ is: feature ? `@vaadin/hilla-react-form/${feature}` : "@vaadin/hilla-react-form", version: "24.8.0" }); })(); let isRendering = false; function useUpdate() { const [_, count] = useReducer((x) => x + 1, 0); return () => { if (isRendering) { return; } count(); }; } function convertFieldValue(model, fieldValue) { return typeof fieldValue === "string" && hasFromString(model) ? model[_fromString](fieldValue) : fieldValue; } function getFormPart(node) { return { addValidator: node.addValidator.bind(node), get defaultValue() { return node.defaultValue; }, dirty: node.dirty, errors: node.errors, invalid: node.invalid, model: node.model, name: node.name, ownErrors: node.ownErrors, required: node.required, setValidators(validators) { node.validators = validators; }, setValue(value) { node.value = value; }, setVisited(visited) { node.visited = visited; }, validate: node.validate.bind(node), validators: node.validators, get value() { return node.value; }, visited: node.visited }; } function useFields(node) { const update = useUpdate(); return useMemo(() => { const registry = new WeakMap(); return (model) => { isRendering = true; const n = getBinderNode(model); let fieldState = registry.get(model); if (!fieldState) { fieldState = { changeHandler() { fieldState.inputHandler(); n.validate().catch(() => {}); }, element: undefined, errorMessage: "", inputHandler() { if (fieldState.strategy) { fieldState.strategy.invalid = false; fieldState.strategy.checkValidity(); n[_validity] = fieldState.strategy.validity; n.value = convertFieldValue(model, fieldState.strategy.value); } }, invalid: false, blurHandler() { fieldState.inputHandler(); n.validate().catch(() => {}); n.visited = true; }, ref(element) { if (!element) { fieldState.element?.removeEventListener("blur", fieldState.blurHandler); fieldState.strategy?.removeEventListeners(); fieldState.element = undefined; fieldState.strategy = undefined; update(); return; } if (!isFieldElement(element)) { throw new TypeError(`Element '${element.localName}' is not a form element`); } if (fieldState.element !== element) { fieldState.element = element; fieldState.element.addEventListener("blur", fieldState.blurHandler); fieldState.strategy = getDefaultFieldStrategy(element, model); fieldState.strategy.onInput = fieldState.inputHandler; fieldState.strategy.onChange = fieldState.changeHandler; update(); } }, required: false, strategy: undefined }; registry.set(model, fieldState); } if (fieldState.strategy) { const valueFromField = convertFieldValue(model, fieldState.strategy.value); if (valueFromField !== n.value && !(Number.isNaN(n.value) && Number.isNaN(valueFromField))) { fieldState.strategy.value = Number.isNaN(n.value) ? "" : n.value; } if (fieldState.required !== n.required) { fieldState.required = n.required; fieldState.strategy.required = n.required; } const firstError = n.ownErrors.at(0); const errorMessage = firstError?.message ?? ""; if (fieldState.errorMessage !== errorMessage) { fieldState.errorMessage = errorMessage; fieldState.strategy.errorMessage = errorMessage; } fieldState.invalid = n.invalid; fieldState.strategy.invalid = n.invalid; } isRendering = false; return { name: n.name, ref: fieldState.ref }; }; }, [node]); } export function useForm(Model, config) { const configRef = useRef({}); configRef.current.onSubmit = config?.onSubmit; configRef.current.onChange = config?.onChange; const update = useUpdate(); const binder = useMemo(() => new BinderRoot(Model, configRef.current), [Model]); const field = useFields(binder); const clear = binder.clear.bind(binder); useEffect(() => { binder.addEventListener(CHANGED.type, update); clear(); return () => binder.removeEventListener(CHANGED.type, update); }, [binder]); return { ...getFormPart(binder), clear, field, read: binder.read.bind(binder), reset: binder.reset.bind(binder), setDefaultValue(defaultValue) { binder.defaultValue = defaultValue; }, setValue(value) { binder.value = value; }, submit: binder.submit.bind(binder), value: binder.value, submitting: binder.submitting, update }; } export function useFormPart(model) { isRendering = true; const binderNode = getBinderNode(model); const field = useFields(binderNode); isRendering = false; return { ...getFormPart(binderNode), field }; } /** * Hook to access an array model part of a form. It provides the same API as `useFormPart`, * but adds an `items` property that allows to iterate over the items in form of an array of models. * * @param model - The array model to access * @returns The array model part of the form */ export function useFormArrayPart(model) { isRendering = true; const binderNode = getBinderNode(model); isRendering = false; return { ...getFormPart(binderNode), items: Array.from(model, (item) => item.model) }; } //# sourceMappingURL=./index.js.map