@zoijs/forms
A tiny, native-forms-first helper. It keeps a form's values, errors, and touched state in Zoijs reactive state — you still write ordinary <input>s and submit with @zoijs/action.
npm install @zoijs/core @zoijs/formsOr with no install, from a CDN with an import map:
<script type=class="tok-string">"importmap">
{
"imports": {
"@zoijs/core": "https://esm.sh/@zoijs/core@1.0.0",
"@zoijs/action": "https://esm.sh/@zoijs/action@0.1.0",
"@zoijs/forms": "https://esm.sh/@zoijs/forms@0.1.0"
}
}
</script>What it does#
Add it when a form needs a little structure — tracking values, validation errors, and which fields have been touched. Forms never touches the network; it just holds state.
const login = form({ email: "", password: "" });
login.values.get(); // { email: "", password: "" }
login.value("email"); // one field (reactive)
login.set("email", "a@b.com"); // update one field
login.error("email"); // one field's error (reactive)
login.touch("email"); // mark touched (e.g. on blur)
login.reset(); // restore initial values, clear errors + touchedAPI#
| Member | What it does |
|---|---|
form(initialValues, options?) | Create a form helper |
values | Reactive state of all values — values.get() returns the object |
value(name) | Read one field's value (reactive) |
set(name, value) | Update one field |
errors / error(name) | All errors / one field's error (reactive) |
setError(name, msg) / clearError(name) | Set / clear one field's error |
touched / touch(name) | Touched fields / mark a field touched |
reset() | Restore initial values; clear errors + touched |
validate(rules?) | Run rules, set errors, return whether valid |
handleSubmit(fn) | Wrap a submit handler: prevents reload, calls fn(values) |
Native form example#
value reads from the form, oninput writes back, onblur marks touched — plain HTML the whole way:
html`
<input
name="email"
value=${() => login.value("email")}
oninput=${(e) => login.set("email", e.target.value)}
onblur=${() => login.touch("email")}
/>
${() => (login.error("email") ? html`<span class="err">${login.error("email")}</span>` : null)}
`;Login & contact examples#
The repo ships two runnable examples: a login form (validation + submit + reset) and a contact form (a textarea, options.validate, and handleSubmit).
Validation#
Validation is just a map of field → function. A rule returns a message when invalid, or a falsy value when valid. No schemas, no dependencies.
const valid = login.validate({
email: (value) => (value.includes("@") ? null : "Enter a valid email"),
password: (value) => (value.length >= 8 ? null : "Minimum 8 characters"),
});
// valid === false, and login.error("email") is now setYou can also pass the rules once via options.validate and call validate() with no arguments.
Using it with @zoijs/action#
Forms holds state; action does the request. Validate, then submit:
import { action } from "@zoijs/action";
const submitLogin = action(async (values) => {
await api.login(values);
});
html`
<form onsubmit=${async (e) => {
e.preventDefault();
if (!login.validate(rules)) return; // forms validates
await submitLogin.run(login.values.get()); // action does the request
}}>
...
<button disabled=${() => submitLogin.pending()}>Sign in</button>
</form>
`;handleSubmit is a thin convenience that prevents the default reload for you — the network call stays yours.What this package intentionally does not do#
By design, to stay tiny: no form provider/context, no field registration, no field arrays, no schema validation, no resolver system, no async-validation engine, no controlled-component framework, and no third-party validation dependency.
Common mistakes#
- Reading outside a binding. Wrap reads in an arrow:
value=${() => login.value("email")}, notvalue=${login.value("email")}. - Expecting forms to submit for you. It doesn't — pair it with @zoijs/action.
- Expecting auto-validation.
set()only updates the value; callvalidate()on submit (or blur) when you want errors. - Validating-then-clearing on blur right before a click. Removing an error on blur can shift layout mid-click and swallow the submit — prefer validating on submit, or reserve space for the message.
Known limitations#
- Flat values — no nested objects or field arrays.
- Validation is manual and synchronous — no schemas, resolvers, or async rules.