Tutorial — integrate your first form
Wire a field, observe state, and submit in a few minutes.
Previous: Overview · Next: Core concepts
Playground
Use the Validation explorer to mirror these steps without a local install.
Prerequisites: Node 20+, TypeScript or JavaScript ESM project. Copy-paste install commands live on the Overview.
Choose your integration path
| Path | Best for | Key API |
|---|---|---|
| Native HTML | Existing forms, static pages, progressive enhancement | createForm({ target, schema, onSubmit }) |
| Headless | Custom UI, non-React frameworks | createForm({ initialValues, validators }) + field().bind() |
| React | React apps | useForm() from @jayoncode/form-intelligent-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-intelligentFor pnpm / yarn and the React adapter, see Overview → Install.
Outcome: @jayoncode/form-intelligent is available to import.
Step 2 — Create a form instance
Define initialValues and optional validators:
import { createForm, email, required } from "@jayoncode/form-intelligent";
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:
createForm({
target: "#signup",
schema: {
email: "email",
name: { required: true, minLength: 2 },
},
onSubmit: async (values) => api.signup(values),
});<form id="signup">
<label>
Email
<input name="email" type="email" autocomplete="email" />
</label>
<label>
Name
<input name="name" autocomplete="name" />
</label>
<button type="submit">Sign up</button>
</form>The engine discovers inputs by name, syncs values, and surfaces errors with aria-invalid / live regions.
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-intelligent @jayoncode/form-intelligent-reactimport { useForm } from "@jayoncode/form-intelligent-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);
});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; subscribe only for manual UI |
| 5 | submit() | Validated async submission |
Continue
| Topic | Guide | Playground |
|---|---|---|
| Terminology and architecture | Core concepts | State |
| Validation timing, async rules | Validation | Validation |
| 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
