Form
Form state, validation, and submit handling for Corn UI inputs.
import { ... } from '@corndev/ui' HTML
<form class="corn-form">
<div class="corn-form-field">
<label class="corn-label" for="email">Email</label>
<input id="email" name="email" class="corn-input" />
<span class="corn-form-error" data-error-for="email"></span>
</div>
</form>
Classes
| Class |
|---|
.corn-form |
.corn-label |
.corn-form-field |
.corn-form-description |
.corn-form-error |
Attributes
| On | Attribute | Values |
|---|---|---|
.corn-form-field | data-layout | inline |
.corn-form-error | data-visible | present or absent |
Vanilla JS
Required for behaviour — adds reads and validates field values, shows messages in the [data-error-for] slots, and links each field to its message with aria-describedby.
import { createForm } from '@corndev/ui';
import '@corndev/ui/theme';
const formEl = document.querySelector('#target');
const instance = createForm(formEl, { /* options */ });
Options
| Option | Type | Default | Description |
|---|---|---|---|
initialValues | Object.<string, any> | {} | Initial value for each field, keyed by name. |
validate | (values: Object.<string, any>) => FormErrors | undefined | Returns a { field: message } map for invalid fields (empty object = valid). |
onSubmit | (values: Object.<string, any>) => void | undefined | Called with the current values when the form submits and validation passes. |
validateOnBlur | boolean | true | Re-run validation when a field loses focus. |
validateOnChange | boolean | false | Re-run validation on every field change. |
React
Form, useForm, useField, FieldError — from @corndev/ui-react.
children may be a render function receiving the form context. useField(name, { describedBy }) returns { name, value, error, onChange, onBlur, aria-invalid, aria-describedby }; spread it onto an input. While a field has an error it is marked aria-invalid, and if a <FieldError name> is rendered, aria-describedby points at that message. Pass your own description ids as describedBy rather than an aria-describedby prop, which the spread would replace. useForm() returns the form context (values, errors, touched, setFieldValue, setFieldTouched, errorId, …) and throws if called outside a <Form>.
The props are the same as the options above, plus className.
<FieldError> props
Shows a field’s validation message and links it to the field with aria-describedby.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Name of the field whose error this shows. |
className | string | undefined | Merged with the Corn UI classes. |
Form tracks field values, runs validation, and handles submission so you don’t
need a separate forms library. Fields are bound by name. In React, use the
useField(name) hook to wire a field’s value, onChange, onBlur, and
error; reach the whole form state with useForm().
React
import { Form, useField, Field, Input, Button } from '@corndev/ui-react';
function EmailField() {
const { error, ...input } = useField('email');
return (
<Field label="Email" error={error} required>
<Input type="email" {...input} />
</Field>
);
}
function SignIn() {
return (
<Form
initialValues={{ email: '' }}
validate={(v) => (v.email ? {} : { email: 'Required' })}
onSubmit={(values) => console.log(values)}
>
<EmailField />
<Button type="submit">Sign in</Button>
</Form>
);
}
Field draws the label, description and error, and wires id,
aria-describedby and aria-invalid for you, so pass it the error and spread
the rest onto the control.
Without Field, useField still marks the control aria-invalid while it has an error. <FieldError name> shows the message, and while it’s rendered the field’s aria-describedby points at it, so screen readers announce why the field is invalid. If the field has its own description, such as a hint, pass that id as useField(name, { describedBy: 'email-hint' }) rather than as an aria-describedby prop, which the spread would replace.
Vanilla JS
Discovers fields by name. Add an element with data-error-for="{name}" to
display a field’s error message. While the error shows, the field is marked
aria-invalid and linked to that element with aria-describedby.
<form id="signin">
<label for="email">Email</label>
<input id="email" name="email" />
<span data-error-for="email" class="corn-form-error"></span>
<button type="submit">Sign in</button>
</form>
import { createForm } from '@corndev/ui';
const form = createForm(document.querySelector('#signin'), {
initialValues: { email: '' },
validate: (values) => (values.email ? {} : { email: 'Required' }),
onSubmit: (values) => console.log(values),
});
Field — the label, the control and the wiring between them
A form is mostly fields, and a field is mostly wiring: the control needs an
id, the label a matching htmlFor, and the description and error have to be
named in aria-describedby.
Getting that wrong is silent. An unassociated label renders exactly like an
associated one — nothing looks broken, the field simply has no name when a
screen reader reaches it. Field owns that wiring, so each call site cannot get
it wrong separately.
import { Field, Input } from '@corndev/ui-react';
<Field label="Client secret" description="Kept in the keychain" error={errors.secret}>
<Input type="password" value={secret} onChange={onChange} />
</Field>
The control is a child, not a prop
Deliberately. A Field that rendered the control itself would have to forward
value, onChange, onBlur, name, defaultValue, placeholder and
everything after them — and would need a variant per control type. As a child,
the same Field works for anything:
<Field label="Format"><Select options={formats} /></Field>
<Field label="Notes"><Textarea rows={4} /></Field>
<Field label="Include examples"><Checkbox /></Field>
It is presentational
Field does not own value or validation state, so it suits a field driven by
useForm and one driven by a parsed document equally — and in a document editor
most fields are the second kind. The two compose:
const field = useField('host');
<Field label="Host" error={field.error}>
<Input {...field} />
</Field>
Layout
It renders one wrapper, .corn-form-field: a flex column with a small gap,
carrying min-inline-size: 0 — so a field inside a Fieldset inside a flex
column shrinks rather than pushing out of the box.
Vanilla
createField wires markup you already have rather than building any:
<div>
<label>Client secret</label>
<input type="password" />
<p data-field-description>Kept in the keychain</p>
<p data-field-error></p>
</div>
import { createField } from '@corndev/ui';
const field = createField(el);
field.update({ error: 'Required' });