Form
formCoordinates named fields with structured submission and field errors.
Usage
Basic usage
Base UI Form container, you get the structured values (preventDefault) when submitting.
<Form className="w-72" onFormSubmit={(v) => console.log(v)}>
<Field label="Email" name="email">
<Input name="email" type="email" placeholder="you@example.com" required />
</Field>
<Field label="Username" name="username">
<Input name="username" placeholder="Hulian user" required />
</Field>
<Button type="submit" size="sm">Submit</Button>
</Form>Server error
errors According to Field name mapping, the external/server verification results are echoed to the corresponding fields.
<Form
className="w-72"
errors={{ email: "This email address has been registered" }}
onFormSubmit={(v) => console.log(v)}
>
<Field label="Email" name="email">
<Input name="email" type="email" defaultValue="taken@example.com" />
</Field>
<Button type="submit" size="sm">Submit</Button>
</Form>Verification rules + field linkage (useForm)
useForm controller: register declares rules, dependencies does field linkage, and submit packages are submitted.
const form = useForm({ initialValues: { email: "", pwd: "", confirm: "" } });
const email = form.register("email", {
rules: [
{ required: true, message: "Please fill in your email address" },
{ pattern: /^[^@]+@[^@]+$/, message: "The email format is incorrect" },
],
});
const pwd = form.register("pwd", { rules: [{ required: true, min: 6, message: "At least 6 digits" }] });
const confirm = form.register("confirm", {
dependencies: ["pwd"],
rules: [{ validator: (v, values) => { if (v !== values.pwd) throw new Error("Two passwords are inconsistent"); } }],
When
});
<form onSubmit={form.submit(onFinish, onFinishFailed)} noValidate>
<Field label="Email" error={email.error}>
<Input value={email.value} onChange={email.onChange} onBlur={email.onBlur} />
</Field>
<Field label="Password" error={pwd.error}>
<Input type="password" value={pwd.value} onChange={pwd.onChange} onBlur={pwd.onBlur} />
</Field>
<Field label="Confirm password" error={confirm.error}>
<Input type="password" value={confirm.value} onChange={confirm.onChange} onBlur={confirm.onBlur} />
</Field>
<Button type="submit" size="sm">Submit</Button>
</form>Dynamic list (FormList)
FormList manages repeating field rows that can be added, deleted, and moved.
<FormList<{ name: string }> value={rows} onChange={setRows}>
{(fields, ops, value) => (
<>
{fields.map((f) => (
<div key={f.key} className="flex items-center gap-2">
<Input value={value[f.name]?.name ?? ""} onChange={...} />
<Button onClick={() => ops.remove(f.name)}>Delete</Button>
</div>
))}
<Button onClick={() => ops.add({ name: "" })}>+ Add Contact</Button>
</>
)}
</FormList>When to use
Use Form for a group of fields that submits structured values and owns its validation flow. Form collects values by native field name, useForm adds registration, rules, dependencies, and controlled submission, and FormList manages dynamic rows. Use ModalForm / DrawerForm for overlays, ProForm for an inline grid with a standard footer, or SearchForm for list filters.
Import
import { Form, useForm, validateValue, FormList } from "@hulianui/ui"Props
Form:
| Name | Type | Default | Description |
|---|---|---|---|
| validationMode | "onSubmit" | "onBlur" | "onChange" | "onSubmit" | Point at which validation runs. |
| errors | Record<string, string | string[]> | - | External or server errors keyed by <Field name>. The matching Field must render Field.Error. |
| className | string | - | Additional class name for the form container. |
Events
| Event | Type | Description |
|---|---|---|
| onFormSubmit | (formValues: Record<string, unknown>) => void | Called with structured values after the component prevents native form submission. |
Slots
| Slot | Type | Description |
|---|---|---|
| children | ReactNode | Fields and form actions. |
useForm controller usage (see examples for details): form.register(name, { rules, dependencies }), form.submit(onValid, onInvalid), form.resetFields(), form.isDirty() (whether the current values differ from the baseline, which drives close confirmations, navigation guards and disabling submit while nothing changed; it compares values rather than references, so editing a field and putting it back is not dirty), and form.markPristine() (pin the current values as the new baseline).
An edit form filled in asynchronously has to pin its baseline (#345)
The dirty baseline defaults to the initialValues of the first render. An edit dialog usually renders that first frame with an empty shell while the record is still in flight, so once the data lands every field differs from that shell and isDirty() stays true forever. Combined with confirmOnClose on ModalForm it turns into "asks to discard even though nothing was touched".
Declare the incoming values as the initial state while writing them:
const form = useForm({ initialValues: { title: "", type: "1" } });
useEffect(() => {
fetchDetail(id).then((d) => {
// These values are the initial state, not an edit made by the user
form.setFieldsValue({ title: d.title, type: String(d.type) }, { markPristine: true });
});
}, [id]);When the values arrive by another route, such as per-field setFieldValue calls or defaults written by the controls themselves, follow up with form.markPristine(). Avoid the two-step form of calling setFieldsValue and then markPristine(): a render sits between them, and any isDirty() during that window reads as edited.
resetFields() also restores the baseline, using the current initialValues (since 0.63.0; before that a closure pinned it to the first render, so replacing the initial values and resetting went back to the old ones).
register() returns { name, value, onChange, onBlur, error, required }. The required flag is derived from whether the rules contain required: true; forward it to the required prop of `Field` so the requirement is visible before submitting (asterisk plus aria-required), while the rules remain the only source of validation.
How the binding reports emptiness (null and undefined are not the same)
register(name).value mirrors `form.values[name]` as-is, with one exception: a field that never received an initial value (undefined) reports "", because handing undefined to a controlled control makes React treat it as uncontrolled and the first keystroke then triggers the "uncontrolled to controlled" warning.
null passes through (#220). It is the business value "explicitly cleared / left blank", a step the user picked just like 0 or "", so three-state fields work:
// null inherits the global setting - 0 is an explicit zero - a positive integer overrides
const form = useForm({ initialValues: { bonus: null as number | null } });
const bonus = form.register("bonus");
<NumberField
value={bonus.value as number | null}
onValueChange={bonus.onChange}
aria-label="Points"
/>
<Button variant="link" size="xs" muted onClick={() => bonus.onChange(null)}>Clear (inherit global)</Button>That was not true in 0.37.0 and earlier: the binding read values[name] ?? "", so within one render form.values.bonus was null while bonus.value was "" - two answers to the same question. Patching it downstream with ?? null does not help either (?? only fires on null and undefined, an empty string sails right through), so the control ends up with an out-of-signature "" - and `NumberField` renders that as 0, while "left blank" and "explicitly zero" are opposite business conclusions.
One boundary: when spreading the binding onto a native <input> or <textarea>, write value={v ?? ""} yourself, otherwise React logs "value prop should not be null". Hulian's `Input` and `Textarea` already fold null into an empty string, so they can take it directly.
Usage guidelines
- A
validatorreports failure withthrow new Error("..."), notfalse. Declare linked fields independenciesso validation reruns when they change. - To display external or server
errors, renderField.Errorin the matching Field and make each error key exactly match<Field name>. - With
useForm, addnoValidateto the native<form>so browser validation does not preempt the rule engine.
Related
ModalForm / DrawerForm · ProForm · StepsForm · LoginForm · Field · SearchForm