Prepared Docs

Prepared Docs

⌘K

    Loading navigation…

User

  1. Blocks
  2. Forms
  3. Usage

Forms

Usage

Overview

@prepared911/ui-forms is the system layer for collecting and validating operator input. It wraps react-hook-form and Zod around @prepared911/ui-core primitives so every field shares the same error localization, label association, and submit contract. The mental model is schema-first, shell-second: define a Zod schema (often with exported schema builders), call useZodForm, drop fields into a form shell (StandardForm, SectionedForm, ModalForm, DrawerForm, or raw Form for settings), and let the package wire Controller plumbing for you.

Dispatch forms lean on onBlur validation so operators aren't interrupted mid-field; StandardForm and DrawerForm disable Save when pristine (!isDirty) or submitting; and destructive commits receive an explicit FormActionIntent.Destructive treatment. ModalForm does not gate confirm on dirty—only on submitting / confirmDisabled. The package does not ship business schemas: you own those per feature and can share them with GraphQL resolvers.

When to use

  • Create and edit flows where operators commit structured data: user drawers, workflow triggers, agency settings, or contact forms.
  • Masked or typed inputs (i.e., email, phone, zip, currency, URL, date) that need consistent parsing and validation messages.
  • Modal or drawer edits where Save/Cancel, unsaved-change guards, and focus trapping must match the product chrome.
  • Long forms that benefit from accordion sections with per-section error dots (SectionedForm + FormSection).
  • Settings pages with debounced per-row autosave (SettingsSection + SettingsRow).
  • Any new form in Prepared frontends—prefer Form* wrappers over manual register + error wiring.

Standard page and panel forms

Use StandardForm when the form is the main content of a drawer body, modal body, or settings panel. It renders Save/Cancel in a trailing footer, disables Save when pristine or submitting, and localizes default button labels.

Modal and drawer commits

Use ModalForm for short, focused edits that should close on successful submit. Use DrawerForm for multi-field workflows that need a side panel, unsaved-change confirmation, and a persistent footer.

Sectioned configuration

Use SectionedForm when a single submit applies across multiple logical groups (profile, roles, authentication). FormSection shows a status dot derived from field errors so operators know which accordion panel needs attention.

Settings with autosave

Use SettingsSection and SettingsRow when each control saves independently on debounced change—typical for agency configuration toggles and selects. Wrap them in Form + useZodForm: SettingsRow calls useFormContext and will throw outside a form provider.

When not to use

  • Single uncontrolled inputs outside a form context (e.g., search boxes, ephemeral filters)—use the underlying Input or Combobox from @prepared911/ui-core directly.
  • Read-only display of values—use CoreText or table cells, not disabled form fields masquerading as labels.
  • Complex multi-step wizards with divergent schemas per step—compose multiple useZodForm instances or a single schema with step gating; don't force SectionedForm to behave like a wizard stepper without explicit step state.
  • Server-only validation with no client fields—skip the package; validate in the resolver and return GraphQL errors.
  • Legacy forms mid-migration—migrate one call site at a time; don't mix manual register and FormInput on the same field.

Composition

A typical feature form stacks four layers:

  1. Schema: z.object({ … }) with localized message keys; prefer emailSchema(), phoneSchema(), and siblings from @prepared911/ui-forms for common field types.
  2. Form instance: useZodForm(schema, { defaultValues }).
  3. Shell: StandardForm, SectionedForm, ModalForm, DrawerForm, or raw Form when you need a custom footer.
  4. Fields: FormInput, FormPhoneInput, FormSelect, etc.; each binds name to a typed field path.

Optional layers:

  • ModalContextProvider + useModal(Component) when opening registered modals imperatively (show / hide).
  • useDebouncedFieldSubmit for single-field autosave outside SettingsRow.
  • useFormLocalizedErrors when building custom FormField children.

Content guidelines

  • Labels: Title case, noun phrases ("Prepared Name", "Primary Role"); localize with formatMessage when labels are dynamic.
  • Helper text: Explain format expectations ("E.164 with optional extension") rather than repeating the label.
  • Validation messages: Store i18n keys in Zod message values (e.g. "profile.email.required"); useFormLocalizedErrors formats them at render time.
  • Submit labels: Active verbs ("Save User", "Create Workflow"); pass submitLabel / confirmLabel on shells when the default "Save" is too generic.
  • Destructive commits: Pair FormActionIntent.Destructive with explicit copy ("Delete permanently", "Revoke access").

Behavior and states

Validation timing

useZodForm defaults to mode: "onBlur". Override with mode: "onChange" only when immediate feedback is required (e.g., password strength or live duplicate checks).

Dirty and submitting

StandardForm and DrawerForm disable the primary action when !form.formState.isDirty or while isSubmitting. Pass isSubmitting explicitly when the mutation lives outside RHF. ModalForm only disables confirm while submitting or when confirmDisabled is set.

Async and server validation

Run server checks in onSubmit; on failure call form.setError("fieldName", { type: "manual", message: "…" }). Don't rely on browser constraint validation—Form sets noValidate by default.

Debounced autosave

SettingsRow and useDebouncedFieldSubmit debounce field changes (default 500 ms), show saving state, and roll back on rejected promises.

Unsaved changes

DrawerForm intercepts close when form.formState.isDirty and opens UnsavedChangesModal. Wire onOpenChange from the parent drawer controller, and mount ui-core ModalProvider / ModalPortal alongside DrawerPortal so the discard dialog can open.

Best practices

Do

  • Share Zod schemas between client forms and server parsers where possible.
  • Use typed masked inputs (FormPhoneInput, FormEmailInput) with matching schema builders.
  • Keep one primary submit per form region; secondary actions are ButtonType.Secondary cancel.
  • Test with RTL: userEvent.type, blur to trigger validation, assert localized error text.

Don't

  • Wire register and FormInput on the same field.
  • Hard-code English validation strings in Zod without i18n keys.
  • Enable Save on pristine StandardForm / DrawerForm flows—operators should see why nothing changed. (ModalForm may confirm while pristine by design.)
  • Nest StandardForm inside another <form> element.

Accessibility

Label association, error announcements, focus on first invalid field, and modal/drawer focus contracts are documented in Accessibility. The package wires aria-invalid and helper text ids through ui-core primitives; shells must not trap focus incorrectly on failed submit.

Related Components

  • Input: underlying text control for FormInput.
  • Select: underlying select for FormSelect.
  • Modal: modal chrome used by ModalForm.
  • Drawer: drawer chrome used by DrawerForm.
  • Data table: list views that often open drawer forms for row edit.

Previous

Filters / Accessibility

Next

Forms / API and Development

On this page

Overview
When to use
Standard page and panel forms
Modal and drawer commits
Sectioned configuration
Settings with autosave
When not to use
Composition
Content guidelines
Behavior and states
Validation timing
Dirty and submitting
Async and server validation
Debounced autosave
Unsaved changes
Best practices
Accessibility
Related Components