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

@opentf/afw-form

v1.33.0

Published

Path-based reactive forms for AFW.

Readme

@opentf/afw-form

Path-based reactive forms for AFW

Docs | API | Changelog | GitHub

An Open Tech Foundation project

Fields bind straight to reactive stores, addressed by dot-path. Typing in one field re-runs only the bindings that read that field: per-keystroke work grows with the path, not with the number of fields.

import { createForm } from "@opentf/afw-form";

export default function SignupForm() {
  const form = createForm({
    initialValues: { username: "", email: "" },
    validate: (values) => {
      const errors = {};
      if (!values.username) errors.username = "Username required";
      if (!values.email.includes("@")) errors.email = "Invalid email";
      return errors;
    },
  });

  return (
    <form onsubmit={form.handleSubmit((values) => console.log(values))}>
      <input {...form.register("username")} placeholder="Username" />
      {form.errors.username && <span class="error">{form.errors.username}</span>}

      <input {...form.register("email")} placeholder="Email" />
      {form.errors.email && <span class="error">{form.errors.email}</span>}

      <button type="submit" disabled={form.isSubmitting}>Sign up</button>
    </form>
  );
}

register(path) returns everything an input needs: name, value, checked, oninput and onblur, plus the field's error and isTouched state. handleSubmit validates first and calls your handler only when the form is valid.

[!NOTE] Alpha. The API can change between minor versions; see the changelog.

Why afw-form

| Need | How afw-form meets it | | --- | --- | | 🎯 Fine-grained | form.values, form.errors, form.touched and form.changed are reactive stores; only the nodes that read a changed path update | | 🌲 Nested state and arrays | Any depth by dot-path ("user.profile.firstName", "tags.0"); arrays are mutated directly | | 🛡️ Validation your way | A plain function, an async check, or a schema library such as Zod or Valibot | | ⏱️ Validation timing | onChange, onBlur or onSubmit, with a separate mode for re-validation | | 🔌 No provider | The form is a plain object; pass it down or read it where it was created |

Install

pnpm add @opentf/afw-form

@opentf/afw is a peer dependency.

Nested state and arrays

const form = createForm({
  initialValues: { user: { name: "Ada" }, skills: ["JavaScript"] },
});

<input {...form.register("user.name")} />

{form.values.skills.map((skill, i) => (
  <input {...form.register(`skills.${i}`)} />
))}

<button type="button" onclick={() => form.values.skills.push("")}>Add skill</button>

Editing one row never re-renders the others; push and splice on form.values.skills update the list in place.

Validation

validate(values) returns an errors object keyed by field path. validator(values) returns { errors } instead, which suits schema resolvers. Either may be async; form.isValidating is true while it runs.

import { z } from "zod";

const schema = z.object({
  username: z.string().min(3, "Too short"),
  email: z.string().email("Invalid email"),
});

const zodResolver = (schema) => (values) => {
  const result = schema.safeParse(values);
  if (result.success) return { errors: {} };
  const errors = {};
  for (const issue of result.error.issues) errors[issue.path.join(".")] = issue.message;
  return { errors };
};

const form = createForm({ initialValues: { username: "", email: "" }, validator: zodResolver(schema) });

| Option | Values | Default | Validates | | --- | --- | --- | --- | | mode | "onChange", "onBlur", "onSubmit" | "onBlur" | On every edit (and on mount), on blur, or only on submit | | reValidateMode | "onChange", "onBlur" | "onChange" | Once an error exists, when to check again |

API

createForm(options) returns a stable form object. When the same component instance remounts, each createForm call gets its form back, matched by key or else by call order in the component body. Several forms in one component stay independent.

| Option | Purpose | | --- | --- | | initialValues | Starting values; also the baseline for isChanged | | validate / validator | Validation function, as above | | mode / reValidateMode | Validation timing, as above | | key | Names the form within its component, instead of its call order |

| Member | Purpose | | --- | --- | | register(path) | Props to spread onto an input | | handleSubmit(fn) | Submit handler: validates, then calls fn(values) when valid | | reset(values?) | Restore initialValues, or reset to new baseline values | | setError(path, message) | Set a field's error from outside the validator (a server refusing the submit); an empty message clears it | | values, errors, touched, changed | Reactive stores, read by path | | isValid, isChanged, isTouched | Form-wide status | | isSubmitting, isValidating, isSubmitted, submitCount | Submission status |

License

MIT — see LICENSE.