Tutorial — integrate your first form
Wire a field, observe state, and submit in a few minutes. Progressive path: Basic (this page) → Concepts → feature guides → Playground.
Previous: Overview · Next: Core concepts
Playground
Use the Validation explorer to mirror these steps without a local install. For attribute-only rules on a real <form>, open HTML constraints.
Prerequisites: Node 20+, TypeScript or JavaScript ESM project. Copy-paste install commands live on the Overview.
Learning path
| Level | Doc | Playground |
|---|---|---|
| Basic | This tutorial | Validation |
| Intermediate | Rules (when()) · Workflow / autosave | Rules · Workflow |
| Advanced | Plugins · Performance | Sandbox |
Choose your integration path
| Path | Best for | Key API |
|---|---|---|
| Native HTML | Existing forms, static pages, progressive enhancement | createForm({ target }) — schema optional; HTML constraints OK |
| Headless | Custom UI, non-React frameworks | createForm({ initialValues, validators }) + field().bind() |
| React | React apps | useForm() from @jayoncode/form-intelligence-react |
The steps below follow the headless path, then show the matching HTML / JSX structure so you can see how elements map to the API. For more adapter variants, see Adapters.
Step 1 — Install
npm install @jayoncode/form-intelligenceFor pnpm / yarn and the React adapter, see Overview → Install.
Outcome: @jayoncode/form-intelligence is available to import.
Step 2 — Create a form instance
Define initialValues and optional validators:
import { createForm, email, required } from "@jayoncode/form-intelligence";
const form = createForm({
initialValues: {
email: "",
name: "",
},
validators: {
email: [required, email],
name: [required],
},
});Outcome: form.state.values reflects initialValues. Validation runs according to validateOn (default field behavior applies on blur/submit).
Step 3 — Bind a field (and the markup)
bind() returns a headless contract for any input:
const email = form.field("email").bind();
// { name, value, onChange, onBlur, onFocus }Native HTML (progressive enhancement)
Prefer target + name attributes — no manual wiring. Constraint attributes (required, minlength, type="email", …) become FI validators on attach; you can still add schema / validators (Field > Schema > HTML).
createForm({
target: "#signup",
// schema optional when HTML already declares constraints:
schema: {
email: "email",
name: { required: true, minLength: 2 },
},
onSubmit: async (values) => api.signup(values),
});<form id="signup">
<label>
Email
<input name="email" type="email" required autocomplete="email" />
</label>
<label>
Name
<input name="name" required minlength="2" autocomplete="name" />
</label>
<button type="submit">Sign up</button>
</form>The engine discovers inputs by name, syncs values, imports Phase 1 HTML constraints, and surfaces errors with aria-invalid / live regions (novalidate on the form). Live lab: HTML constraints playground.
Headless DOM (manual bind())
<form id="signup">
<label>
Email
<input id="email" type="email" />
<span id="email-error" role="alert"></span>
</label>
<button type="submit">Sign up</button>
</form>const input = document.querySelector("#email");
const error = document.querySelector("#email-error");
const email = form.field("email").bind();
input.name = email.name;
input.value = String(email.value ?? "");
input.addEventListener("input", (event) => {
email.onChange(event.target.value);
});
input.addEventListener("blur", () => email.onBlur());
input.addEventListener("focus", () => email.onFocus());
form.subscribe(() => {
const message = form.state.errors.email;
error.textContent = message ?? "";
input.setAttribute("aria-invalid", message ? "true" : "false");
});React JSX
npm install @jayoncode/form-intelligence @jayoncode/form-intelligence-reactimport { useForm } from "@jayoncode/form-intelligence-react";
export function SignupForm() {
const form = useForm({
initialValues: { email: "", name: "" },
validators: {
email: [required, email],
name: [required],
},
onSubmit: async (values) => api.signup(values),
});
return (
<form {...form.form()}>
<label>
Email
<input {...form.field("email")} type="email" />
{form.state.errors.email ? <span role="alert">{form.state.errors.email}</span> : null}
</label>
<label>
Name
<input {...form.field("name")} />
{form.state.errors.name ? <span role="alert">{form.state.errors.name}</span> : null}
</label>
<button {...form.submit()} disabled={form.state.isSubmitting}>
{form.state.isSubmitting ? "Saving…" : "Sign up"}
</button>
</form>
);
}Outcome: User input updates form.state.values and triggers validation per configured timing.
Step 4 — Read state (no subscription required)
Inspect the form at any time:
form.state.values;
form.state.errors;
form.state.isValid;
form.isSubmitting();
form.getValues();
form.getErrors();For vanilla JS UIs that re-render on every change, use subscribe():
form.subscribe(() => {
render(form.state);
});You can also declare listeners at create time (fires once immediately, then on every change, until destroy()):
createForm({
initialValues: { email: "" },
subscribe: (form) => {
render(form.state);
},
});React developers should use useForm() instead — the adapter subscribes internally via useSyncExternalStore.
Step 5 — Submit
const form = createForm({
initialValues: { email: "", name: "" },
validators: { email: [required, email], name: [required] },
onSubmit: async (values) => {
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!res.ok) throw new Error(String(res.status));
},
});
const success = await form.submit();
// success === true → onSubmit completed
// success === false → validation failed or submit already in flightWhile onSubmit runs, form.isSubmitting() is true.
Outcome: Valid forms invoke onSubmit once per user action; invalid forms return false without calling the handler.
Recap
| Step | API | Result |
|---|---|---|
| 1 | npm install | Package on disk |
| 2 | createForm({ … }) | Instance with values + validators |
| 3 | field().bind() | Headless input wiring |
| 4 | form.state / subscribe() | Read state; createForm({ subscribe }) or form.subscribe() for manual UI |
| 5 | submit() | Validated async submission |
Continue
| Topic | Guide | Playground |
|---|---|---|
| Terminology and architecture | Core concepts | State |
| Validation timing, async rules | Validation | Validation |
| HTML attributes → validators | Adapters → Native HTML | HTML constraints |
| Submit, cancel, offline queue | Submission | Submission |
| Autosave, drafts, wizard | Workflow | Workflow |
Conditional when() rules | Rules | Rules |
| Derived fields | Calculations | Calculations |
| Snapshots, undo/redo | State | State |
Additional snippets: Examples · Integrations: Integrations
