Usage
@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.
SectionedForm + FormSection).SettingsSection + SettingsRow).Form* wrappers over manual register + error wiring.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.
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.
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.
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.
Input or Combobox from @prepared911/ui-core directly.CoreText or table cells, not disabled form fields masquerading as labels.useZodForm instances or a single schema with step gating; don't force SectionedForm to behave like a wizard stepper without explicit step state.register and FormInput on the same field.A typical feature form stacks four layers:
z.object({ … }) with localized message keys; prefer emailSchema(), phoneSchema(), and siblings from @prepared911/ui-forms for common field types.useZodForm(schema, { defaultValues }).StandardForm, SectionedForm, ModalForm, DrawerForm, or raw Form when you need a custom footer.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.formatMessage when labels are dynamic.message values (e.g. "profile.email.required"); useFormLocalizedErrors formats them at render time.submitLabel / confirmLabel on shells when the default "Save" is too generic.FormActionIntent.Destructive with explicit copy ("Delete permanently", "Revoke access").useZodForm defaults to mode: "onBlur". Override with mode: "onChange" only when immediate feedback is required (e.g., password strength or live duplicate checks).
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.
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.
SettingsRow and useDebouncedFieldSubmit debounce field changes (default 500 ms), show saving state, and roll back on rejected promises.
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.
Do
FormPhoneInput, FormEmailInput) with matching schema builders.ButtonType.Secondary cancel.userEvent.type, blur to trigger validation, assert localized error text.Don't
register and FormInput on the same field.StandardForm / DrawerForm flows—operators should see why nothing changed. (ModalForm may confirm while pristine by design.)StandardForm inside another <form> element.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.
FormInput.FormSelect.ModalForm.DrawerForm.On this page