MobTranslate styleguide

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

PartActual contract
FormNative form, flex column / 24px gap, noValidate controlled by the installed validation strategy
FieldsRegistered Field + FieldControl or compatible installed primitives; meaningful name and visible label
ActionsExplicit submit plus named type=button recovery actions. Enter submission follows native context.
Errors / task statusField errors by registered name; visible form-level pending/failure/success/cancel text
Retained recordTask-owned confirmed data separate from editable draft; this demo retains only in page memory

Working validation and local outcomes

English-only example; entered values stay in this page.

Format validation does not prove ownership or delivery.

Nothing has been saved. This is a local form simulation.

tsx
'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

PropContract
validationModeonSubmit default; onBlur/onChange. Field overrides the form-level mode.
errorsExternal errors keyed by Field name; strings/arrays accepted by installed Form context. Editing can clear the corresponding error.
onFormSubmitRegistered values and generic event details. Native preventDefault is applied when used. No backend operation is provided.
onSubmitNative React form event after registered validation; caller owns prevention, payload, async task and recovery.
action / method / native attributesNative semantics remain available. Verify the actual installed noValidate strategy and server endpoint before relying on browser validation.
className / style / render / refInstalled composition; HTMLFormElement ref; state class callback preserved.

Validation and submission state machine

StateTask action
EditingKeep values and visible constraints. Allow native correction, paste, autofill and composition.
Invalid attemptStop the task, show connected errors and focus the invalid control or a linked summary for a large form.
PendingSnapshot the submitted values; retain read-only fields; guard duplicate submits. A spinner alone does not enforce this.
Field rejection / task failureRetain draft, connect server-returned field errors and offer retry. General network failures need form-level recovery.
CancelledReport the cancellation boundary truthfully. A stopped client wait may not cancel a remote write.
SucceededReplace retained data from the confirmed result, show the outcome and keep a route to edit.
Restore / resetDistinguish 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.