@rakit-ui/form-core
v1.1.0
Published
A tiny, framework-agnostic headless form engine with a simple API.
Downloads
14
Maintainers
Readme
@rakit-ui/form-core
A tiny, framework-agnostic headless form engine with a simple API.
- Headless: logic only, no UI opinions.
- Framework-agnostic: works in React, Vue, Angular, or vanilla JS.
- Tiny: ~7 KB ESM, zero dependencies.
- Simple API: values, errors, touched, dirty.
Installation
pnpm add @rakit-ui/form-core
# or
npm install @rakit-ui/form-coreRequires Node.js 20+ and a modern bundler. ESM-only.
Basic usage
import { createForm } from "@rakit-ui/form-core";
const form = createForm({
initialValues: {
email: "",
password: "",
profile: { name: "" },
},
validate(values) {
const errors: Record<string, string> = {};
if (!values.email) errors.email = "Email is required";
if (!values.password) errors.password = "Password is required";
if (!values.profile.name) errors["profile.name"] = "Name is required";
return errors;
},
async onSubmit(values) {
console.log("submit", values);
},
});
form.setValue("email", "[email protected]");
form.setTouched("email", true);
const result = await form.submit();
// result: { ok: true, errors: {} } | { ok: false, errors: {...} }Core API
createForm(options)— create a form instance.setValue(path, value)/getValue(path)— read/write values via dotted paths (profile.name,addresses.0.city).setValues(values)— replace all values (clearsdirty).setError(path, error?)/setErrors(errors)— manage errors.setTouched(path, bool)/setDirty(path, bool)— mutate metadata.getField(path)— ergonomic helper withonChange,onBlur,setValue,setTouched.validate()— run validator, returns latest errors. Supports sync and async.submit()— returnsPromise<{ ok, errors }>.reset(nextValues?)— reset to initial snapshot or a new one (wipes all interaction state).subscribe(listener)— subscribe to full state updates.destroy()— clear listeners and disable mutators.
New in v1.1
subscribeField(path, listener)— subscribe to one field. Listener fires only when that field'sFieldStatechanges (exact path match, referential diff).resetField(path)— reset one field (and its descendant subtree metadata) without touching siblings.validateField(path)— run the validator but apply onlyerrors[path]. Race-safe per path.setInitialValues(values)— swap the baseline and cleardirty, but keeperrors,touched,submitCount. Useful after server refresh.isValid()/isDirty()— sync derived helpers.
See CHANGELOG.md for the full list.
Key design decisions (v1.0.0)
- Flat error/touched/dirty records keyed by path string.
- Array index paths: numeric segments like
addresses.0.citycreate arrays, non-numeric create objects. - Concurrent
submit()is a no-op — the second call returns{ ok: false, errors }without touching state. - Parallel
validate()calls: only the latest result writes to state. destroy()disables state mutations so in-flight async tasks become no-ops.- Dirty comparison is referential (
!==) — object/array contents are not deep-compared in v1. Deep equality is on the v1.2 roadmap.
See the PRD in the repo for full rationale and roadmap.
License
MIT
