@vaadin/hilla-react-form
Version:
Hilla form utils for React
203 lines • 5.9 kB
JavaScript
import { _fromString, _validity, BinderRoot, CHANGED, getBinderNode, getDefaultFieldStrategy, hasFromString, isFieldElement, getStringConverter } from "@vaadin/hilla-lit-form";
import { Model } from "@vaadin/hilla-models";
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: "25.1.8"
});
})();
let isRendering = false;
function useUpdate() {
const [_, count] = useReducer((x) => x + 1, 0);
return () => {
if (isRendering) {
return;
}
count();
};
}
function convertFieldValue(model, fieldValue) {
if (typeof fieldValue !== "string") {
return fieldValue;
}
const stringConverter = getStringConverter(model);
if (stringConverter) {
return stringConverter.fromString(fieldValue);
}
if (hasFromString(model)) {
return model[_fromString](fieldValue);
}
return 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(modelClass, config) {
const configRef = useRef({});
configRef.current.onSubmit = config?.onSubmit;
configRef.current.onChange = config?.onChange;
const update = useUpdate();
const binder = useMemo(() => new BinderRoot(modelClass, 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