@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/forms

Or 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 + touched

API#

MemberWhat it does
form(initialValues, options?)Create a form helper
valuesReactive 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 set

You 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")}, not value=${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; call validate() 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.