npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zoijs/forms

v0.1.1

Published

A tiny, native-forms-first helper for Zoijs: reactive values, errors, and touched state. No JSX, no build step.

Downloads

266

Readme

@zoijs/forms

A tiny, native-forms-first helper for Zoijs. Reactive values, errors, and touched state — without a form framework.

npm license

Documentation · Core package


@zoijs/forms is an optional package. Add it when a form needs a little structure — tracking values, errors, and which fields have been touched. You still write ordinary <input>s, and you still submit with @zoijs/action. Forms never touches the network.

You can learn the whole thing in about 5 minutes.

Install

npm install @zoijs/core @zoijs/forms

Or with no install, from a CDN:

import { form } from "https://esm.sh/@zoijs/[email protected]";

What form() does

form(initialValues, options?) keeps your form's values, errors, and touched state in Zoijs reactive state, with small per-field helpers:

import { form } from "@zoijs/forms";

const login = form({ email: "", password: "" });

login.all();                        // { email: "", password: "" } — all values (reactive)
login.value("email");               // one field (reactive)
login.set("email", "[email protected]");      // update one field
login.error("email");               // one field's error (reactive)
login.setError("email", "Required");
login.clearError("email");
login.isTouched("email");           // has this field been touched? (reactive)
login.touch("email");               // mark touched (e.g. on blur)
login.reset();                      // restore initial values, clear errors + touched

These reader methods (all(), value(), error(), isTouched(), …) match the rest of the ecosystem — the same shape as data(), loading(), and pending() — so once you've learned one Zoijs package, the others read the same way.

The API

| Member | What it does | |---|---| | form(initialValues, options?) | Create a form helper | | all() | Read all values (reactive) | | value(name) | Read one field's value (reactive) | | set(name, value) | Update one field | | allErrors() | Read all errors (reactive) | | error(name) | Read one field's error (reactive) | | setError(name, message) | Set one field's error | | clearError(name) | Clear one field's error | | allTouched() | Read all touched flags (reactive) | | isTouched(name) | Has one field been touched? (reactive) | | touch(name) | 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) |

Advanced: raw state access

The underlying reactive state is also exposed as values, errors, and touched (each a createState-shaped { get, set, peek }). Prefer the reader methods above; reach for the raw state only when you need direct access — e.g. login.values.peek() to read without subscribing. These remain for backward compatibility and won't be removed in 0.x.

Login form example

Use native inputs — value reads from the form, oninput writes back, onblur marks touched:

import { html, mount } from "@zoijs/core";
import { form } from "@zoijs/forms";

const login = form({ email: "", password: "" });

function App() {
  return 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)}
  `;
}

mount(App, "#app");

See examples/login/.

Contact form example

A contact form with a textarea, default rules via options.validate, and handleSubmit. See examples/contact/.

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 and call validate() with no args:

const login = form({ email: "", password: "" }, {
  validate: { email: (v) => (v.includes("@") ? null : "Enter a valid email") },
});
login.validate();

Using it with @zoijs/action

Forms holds state; @zoijs/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;
    await submitLogin.run(login.all());
  }}>
    ...
    <button disabled=${() => submitLogin.pending()}>Sign in</button>
  </form>
`;

handleSubmit is a thin convenience that prevents the default reload for you:

const onSubmit = login.handleSubmit((values) => submitLogin.run(values));
html`<form onsubmit=${onSubmit}>...</form>`;

Common mistakes

  • Reading outside a binding. Wrap reads in an arrow to make them live: 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; clearError() to remove one.
  • Reaching for field arrays / nested objects. Keep values flat. For complex, dynamic shapes, manage your own reactive state with createState.

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. It's the 90%-case helper — native forms, plus a little structure.

License

MIT © Zoijs contributors