Skip to content

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

LevelDocPlayground
BasicThis tutorialValidation
IntermediateRules (when()) · Workflow / autosaveRules · Workflow
AdvancedPlugins · PerformanceSandbox

Choose your integration path

PathBest forKey API
Native HTMLExisting forms, static pages, progressive enhancementcreateForm({ target }) — schema optional; HTML constraints OK
HeadlessCustom UI, non-React frameworkscreateForm({ initialValues, validators }) + field().bind()
ReactReact appsuseForm() 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

bash
npm install @jayoncode/form-intelligence

For 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:

ts
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:

ts
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).

ts
createForm({
  target: "#signup",
  // schema optional when HTML already declares constraints:
  schema: {
    email: "email",
    name: { required: true, minLength: 2 },
  },
  onSubmit: async (values) => api.signup(values),
});
html
<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())

html
<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>
ts
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

bash
npm install @jayoncode/form-intelligence @jayoncode/form-intelligence-react
tsx
import { 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:

ts
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():

ts
form.subscribe(() => {
  render(form.state);
});

You can also declare listeners at create time (fires once immediately, then on every change, until destroy()):

ts
createForm({
  initialValues: { email: "" },
  subscribe: (form) => {
    render(form.state);
  },
});

React developers should use useForm() instead — the adapter subscribes internally via useSyncExternalStore.


Step 5 — Submit

ts
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 flight

While onSubmit runs, form.isSubmitting() is true.

Outcome: Valid forms invoke onSubmit once per user action; invalid forms return false without calling the handler.


Recap

StepAPIResult
1npm installPackage on disk
2createForm({ … })Instance with values + validators
3field().bind()Headless input wiring
4form.state / subscribe()Read state; createForm({ subscribe }) or form.subscribe() for manual UI
5submit()Validated async submission

Continue

TopicGuidePlayground
Terminology and architectureCore conceptsState
Validation timing, async rulesValidationValidation
HTML attributes → validatorsAdapters → Native HTMLHTML constraints
Submit, cancel, offline queueSubmissionSubmission
Autosave, drafts, wizardWorkflowWorkflow
Conditional when() rulesRulesRules
Derived fieldsCalculationsCalculations
Snapshots, undo/redoStateState

Additional snippets: Examples · Integrations: Integrations

JOC — headless @jayoncode/* TypeScript libraries. Docs sync from package source via TypeDoc and sync scripts.