Form
Coordinate registered validation, retained drafts and explicit task outcomes without implying a server operation.
Choose a submission boundary
Form renders a native form with installed registered-field validation and consolidated external errors. Use it for one submit boundary. A visual stack is not enough: inputs need registration or an explicit native validation/payload policy. Never nest forms.
onFormSubmit collects registered field values and prevents native submission. onSubmit receives the native event after registered checks; call preventDefault for a local task and read native FormData when that is your payload. The two objects are not interchangeable.
Anatomy and specifications
| Part | Actual contract |
|---|---|
| Form | Native form, flex column / 24px gap, noValidate controlled by the installed validation strategy |
| Fields | Registered Field + FieldControl or compatible installed primitives; meaningful name and visible label |
| Actions | Explicit submit plus named type=button recovery actions. Enter submission follows native context. |
| Errors / task status | Field errors by registered name; visible form-level pending/failure/success/cancel text |
| Retained record | Task-owned confirmed data separate from editable draft; this demo retains only in page memory |
Working validation and local outcomes
Nothing has been saved. This is a local form simulation.
'use client';
import { useRef, useState } from 'react';
import { Button, Field, FieldControl, FieldDescription, FieldError, FieldLabel, Form, Input } from '@mobtranslate/ui';
export function ValidatedFormExample() {
const [title, setTitle] = useState('');
const [email, setEmail] = useState('');
const [pending, setPending] = useState<{title: string; email: string} | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
const [result, setResult] = useState('Nothing has been saved. This is a local form simulation.');
const titleInput = useRef<HTMLInputElement>(null);
const retained = useRef<{title: string; email: string} | null>(null);
function resolve(outcome: 'success' | 'failure' | 'cancel') {
if (!pending) return;
if (outcome === 'success') { retained.current = pending; setResult(`Retained local record: ${pending.title}, ${pending.email}. No server write or email occurred.`); }
if (outcome === 'failure') { setErrors({title:'Simulated rejection. Edit the local title and retry.'}); setResult('Local submission failed. Both draft values remain available.'); }
if (outcome === 'cancel') setResult('Local submission cancelled. Both draft values remain available.');
setPending(null); titleInput.current?.focus();
}
return <div data-form-lab className="space-y-4">
<Form errors={errors} aria-busy={Boolean(pending)} onFormSubmit={values => {
if (pending) return;
setErrors({}); setPending({title:String(values.title),email:String(values.email)}); setResult('Local submission pending. Choose an explicit local outcome.');
}}>
<Field name="title" validate={value => typeof value === 'string' && value.trim().length >= 3 ? null : 'Use at least three non-space code units for the local title.'}>
<FieldLabel>Local record title (required)</FieldLabel>
<FieldControl ref={titleInput} data-form-title value={title} onValueChange={setTitle} readOnly={Boolean(pending)} required render={<Input />} />
<FieldDescription>English-only example; entered values stay in this page.</FieldDescription><FieldError />
</Field>
<Field name="email"><FieldLabel>Local contact email (required)</FieldLabel><FieldControl data-form-email type="email" required value={email} onValueChange={setEmail} readOnly={Boolean(pending)} autoComplete="email" render={<Input />} /><FieldDescription>Format validation does not prove ownership or delivery.</FieldDescription><FieldError match="valueMissing">Enter an email for this local example.</FieldError><FieldError match="typeMismatch">Use a valid email format for this local example.</FieldError></Field>
<div className="flex flex-wrap gap-3"><Button type="submit" disabled={Boolean(pending)}>Submit local record</Button><Button type="button" variant="outline" disabled={Boolean(pending)} onClick={() => { setTitle(retained.current?.title ?? ''); setEmail(retained.current?.email ?? ''); setErrors({}); setResult(retained.current ? 'Restored the retained local values.' : 'Cleared the unsaved local values.'); titleInput.current?.focus(); }}>Restore retained draft</Button></div>
</Form>
{pending && <div className="flex flex-wrap gap-3"><Button type="button" onClick={() => resolve('success')}>Complete local form</Button><Button type="button" variant="outline" onClick={() => resolve('failure')}>Fail local form</Button><Button type="button" variant="outline" onClick={() => resolve('cancel')}>Cancel local form</Button></div>}
<p role="status">{result}</p>
</div>;
}
API reference
| Prop | Contract |
|---|---|
| validationMode | onSubmit default; onBlur/onChange. Field overrides the form-level mode. |
| errors | External errors keyed by Field name; strings/arrays accepted by installed Form context. Editing can clear the corresponding error. |
| onFormSubmit | Registered values and generic event details. Native preventDefault is applied when used. No backend operation is provided. |
| onSubmit | Native React form event after registered validation; caller owns prevention, payload, async task and recovery. |
| action / method / native attributes | Native semantics remain available. Verify the actual installed noValidate strategy and server endpoint before relying on browser validation. |
| className / style / render / ref | Installed composition; HTMLFormElement ref; state class callback preserved. |
Validation and submission state machine
| State | Task action |
|---|---|
| Editing | Keep values and visible constraints. Allow native correction, paste, autofill and composition. |
| Invalid attempt | Stop the task, show connected errors and focus the invalid control or a linked summary for a large form. |
| Pending | Snapshot the submitted values; retain read-only fields; guard duplicate submits. A spinner alone does not enforce this. |
| Field rejection / task failure | Retain draft, connect server-returned field errors and offer retry. General network failures need form-level recovery. |
| Cancelled | Report the cancellation boundary truthfully. A stopped client wait may not cancel a remote write. |
| Succeeded | Replace retained data from the confirmed result, show the outcome and keep a route to edit. |
| Restore / reset | Distinguish native initial-value reset from restoring last retained data. Explicitly reset controlled state; do not discard silently. |
Accessibility and responsive behavior
- Native form, label and submit semantics enable keyboard submission. Buttons that resolve local outcomes are explicit non-submit actions.
- The example focuses the title after outcomes remove their controls, retains both draft fields, and uses a polite task status. Spoken errors/status still require assistive-technology checks.
- For long forms provide a focused linked error summary, meaningful fieldset groups and a logical single-column narrow layout. Keep labels visible during autofill and editing.
- Authentication needs password manager/paste support, appropriate autocomplete and accessible recovery. This local demo collects no password and provides no authentication service.
Failures and recovery
- An onSubmit that only preventsDefault is not a functional sign-in/save flow. Give the user an observable local result or connect and verify the real operation.
- Async Field validate does not gate onSubmit. Await necessary server checks in the task and handle stale/aborted requests without accepting an old response.
- Do not trust client validation for security. Repeat authoritative validation/permission checks at the server and preserve evidence for meaningful writes.
- Native reset does not reset React controlled values. This example offers an explicit restore to the retained page-memory record. Reload/navigation clears it.
Verification and release scope
- Verify the actual label/control connection, description/error IDs, invalid state, focus order and corrected input with pointer and keyboard. Read the submitted payload rather than inferring it from visible text.
- Test blank, populated, invalid, disabled, read-only and pending/recovery states. Check both themes, long labels, user text spacing, 320px reflow, reduced motion, forced colors and zoom.
- Run the named input-family browser checks and source/type/build checks. Inspect mobile keyboards, autofill, password managers and a named screen-reader/browser pair separately. DOM tests do not prove their behavior.
- Client validation supports correction; it does not establish authorization, consent, source authority, language quality, identity, security or a successful server write.