Skip to main content

C7 — Forms & Validation Patterns

Scope: Standard form patterns, validation rules, field types, and error-display conventions for all Ghasi Melmastoon surfaces (web, mobile, desktop). Covers guest booking forms, front-desk capture forms, backoffice config forms, and multi-step wizards.

Stack: React Hook Form + Zod (web/desktop renderer). React Native: React Hook Form + Zod (via @hookform/resolvers/zod). No ad-hoc form state or direct DOM manipulation.


1. Form architecture

1.1 Libraries

LayerLibraryNotes
Form statereact-hook-form v7+Mode: onBlur for most forms, onChange for search/filter forms
Schema validationzod v3+Server schema imported from shared @ghasi/schemas package
Resolver@hookform/resolvers/zodShared schemas ensure client/server parity
Field components@ghasi/ui-melmastoon form atoms (<TextField>, <Select>, <DatePicker>, etc.)Wraps Radix UI where applicable

1.2 Submit flow

User submits form
→ RHF runs Zod schema (client-side)
→ If invalid: inline field errors (see §2.2)
→ If valid: optimistic disable of form + loading state
→ BFF request
→ On 422 (VALIDATION_FAILED): merge BFF errors[] into RHF state (per §2.3)
→ On success: navigate or show success toast
→ On other error: look up C2 error-to-UI matrix

2. Field types and conventions

2.1 Text fields

<TextField
name="guestName"
label={t("guest.name")}
required
autoComplete="name"
dir={locale.dir} // "ltr" | "rtl"
inputMode="text"
maxLength={100}
description={t("guest.name.hint")} // helper text below field
/>

Rules:

  • All labels are visible (no placeholder-only labels — fails WCAG 1.3.5).
  • autoComplete is always set where applicable (improves UX + passes WCAG 1.3.5 purpose criteria).
  • dir attribute on the field itself may differ from page direction (e.g., an English email on a Pashto page is dir="ltr").
  • Max length enforced client-side and server-side.

2.2 Numeric fields

  • Financial amounts (rates, charges, refunds): always input in Latin numerals regardless of locale. Show inputMode="decimal". Append the currency symbol after the field.
  • Non-financial counts (adult count, nights, room number): inputMode="numeric". Locale-aware display allowed.
  • Validation: z.number().positive().max(9999999) for amounts; z.number().int().min(1).max(99) for counts.

2.3 Date / calendar fields

ContextComponentCalendars shown
Guest booking (check-in/out)<DateRangePicker>Gregorian + Hijri (side by side, tenant-configurable)
Reporting (date filter)<DatePicker>Gregorian only
Tax submission date<DatePicker>Gregorian + Hijri
Staff shift scheduling<DatePicker>Gregorian only
  • Hijri / Solar Hijri calendar support uses the temporal-polyfill + @js-temporal/polyfill with Intl.DateTimeFormat for display.
  • Date values stored and transmitted as ISO 8601 (YYYY-MM-DD); calendar conversion is display-only.

2.4 Phone number fields

  • Component: <PhoneField> with country dial-code picker.
  • Default country: derived from tenant.region (AF → +93, IR → +98, TJ → +992, PK → +92).
  • Validation: zod-phone-number library with libphonenumber-js.
  • Stored in E.164 format.

2.5 Select / dropdown

  • Component: Radix UI <Select> wrapped by @ghasi/ui-melmastoon.
  • Always provide a null option ("Choose…") for optional fields.
  • For > 20 options, use <Combobox> (searchable) instead.
  • RTL: arrow icon flips; popover opens towards the logical start of the field.

2.6 File upload

  • Component: <FileUpload> with drag-drop zone + browse button.
  • Accepted types declared explicitly (e.g., accept="image/jpeg,image/png,application/pdf").
  • Max file size: 10 MiB (document), 20 MiB (photo).
  • On select: client-side MIME type + size validation before upload.
  • Upload via file-storage-service signed URL; progress shown with L-03 progress bar.
  • A11y: drop zone has role="button" + keyboard trigger.

2.7 Checkbox / radio groups

  • Always use fieldset + legend for groups.
  • <Checkbox> from @ghasi/ui-melmastoon (Radix Checkbox primitive).
  • Single checkbox (terms agreement): z.literal(true, { message: t("form.required") }).

3. Validation patterns

3.1 Client-side (Zod schemas)

All schemas live in @ghasi/schemas (shared package). Frontend imports and uses them directly:

import { GuestDetailsSchema } from "@ghasi/schemas/booking";

const form = useForm<z.infer<typeof GuestDetailsSchema>>({
resolver: zodResolver(GuestDetailsSchema),
mode: "onBlur",
});

3.2 Error display

  • Errors appear below the affected field (not at the top of the form).
  • Icon: alert-circle 14px before the message.
  • aria-invalid="true" + aria-describedby="<field>-error" on the input.
  • On form submit with errors: focus moves to the first errored field.
  • Multiple errors per field: show only the first (Zod formErrors.flatten() first message).

3.3 Server-side error merge

When the BFF returns MELMASTOON.GENERAL.VALIDATION_FAILED (422) with errors[]:

const onSubmit = async (data) => {
const result = await submitToServer(data);
if (result.error?.code === "MELMASTOON.GENERAL.VALIDATION_FAILED") {
result.error.errors.forEach(({ field, code }) => {
form.setError(field, {
type: "server",
message: t(`errors.${code.toLowerCase().replaceAll(".", "_")}`),
});
});
return;
}
};

4. Multi-step wizard patterns

Used for: booking funnel, tenant onboarding, walk-in booking capture, theme authoring.

4.1 Anatomy

[Step indicator]
Step 1 ●——— Step 2 ○——— Step 3 ○

[Step content area]
Form for current step

[Navigation bar]
← Back Next →
  • Each step is a separate React subtree, not a tab panel.
  • State shared via React context (<WizardProvider>).
  • Zod schema per step; validate on "Next" before advancing.
  • Browser "Back" navigates wizard step, not page (intercept popstate).
  • On desktop: step progress persists in local SQLite draft (outbox) so users can leave and return.

4.2 Step indicator

  • Linear progress (step number / total).
  • Each step: completed (✓), current (●), upcoming (○).
  • RTL: step sequence reads right-to-left visually.
  • A11y: aria-label="Step N of M: <title>" on each indicator item.

4.3 Dirty state / exit confirmation

  • If the user navigates away from a partially filled wizard, show an exit confirmation modal: "You have unsaved changes — leave or continue?".
  • Exception: read-only review step.

5. Form-specific patterns by context

5.1 Guest details capture (booking funnel)

Required fields:

  • Full name (guestName: string 2–100 chars)
  • Email (email: RFC 5322 email)
  • Phone (phone: E.164, optional if email provided)
  • Nationality (nationality: ISO 3166-1 alpha-2)
  • Check-in time estimate (time picker, optional)
  • Special requests (textarea, 500 char max, optional)

5.2 Walk-in booking (front desk)

Required fields:

  • Room selection (property-specific room picker)
  • Check-in / check-out dates
  • Adult count, child count
  • Guest name + phone or ID document number
  • Payment method (cash / card / mobile)
  • Cash deposit amount (if cash)
  • Rate plan

Offline note: all walk-in forms must work fully offline. Data written to local SQLite outbox immediately on submit.

5.3 Folio charge

Required fields:

  • Charge category (select from canonical list)
  • Amount (Latin numerals, currency appended)
  • Description (optional, 200 chars)
  • Applied-by staff (auto-filled from session)

5.4 Tax report filter

Required fields:

  • Date range (Gregorian + Hijri picker)
  • Report currency (select)
  • Export format (PDF / CSV)

6. Accessibility requirements

  • All form controls have visible, persistent labels (not placeholder-only).
  • Error summaries at the top of the form in addition to inline errors are required for forms > 5 fields or when errors may be off-screen.
  • Keyboard navigation: Tab, Shift+Tab, Enter to submit, Escape to cancel.
  • Focus ring: --color-focusRing token, 2px solid, 2px offset.
  • Color is never the only indicator of an error state (icon + text required).

7. Open Questions

  • Hijri calendar for check-in/out: should both calendars be shown side-by-side always, or only when tenant.locale is one of ps-AF, fa-AF, fa-IR?
  • Guest name field: should we collect separate first name + last name, or full name? Legal documents in Afghanistan often use a single name (patronymic only).
  • File upload: should tenant logo uploads go through theme-config-service directly or via BFF?

References