Patterns
Forms
One pattern for every create and edit form: Add Project, Data Sharing Agreement and Data Licence request all render the same page.
Anatomy
A form is a FormPage (app/pages/_shared/form-page.tsx) with its fields laid out in FormRows. The page owns the header, the scrolling body and the footer, and the section list fills column 2, so none of them can differ between forms. Try it: press Continue with the name empty.
Live example
The rules
This is contract section 4.1 in CONTRACTS.md, enforced by npm run check:contracts: a screen that uses FormRow without FormPage fails.
- Header. An optional eyebrow, the title (with a status badge for an existing record), a one-line subtitle, and one close button (X) at the top right, the way out of the form. The header describes the step and holds no other action.
- Sections live in column 2. A form with more than two or three field groups is split into sections, listed in the contextual sidebar as a
FormSectionList, built from the vertical Progress steps: a progress bar, the sections grouped by step, and a state for each (current, complete, needs attention, not started). Any section can be opened at any time. Never tabs, never a forward-only stepper. Related fields share a screen; do not ask one question per screen. - Rows. Label and help on the left, fields on the right. Required fields are marked with an asterisk.
- Mandatory details block progress. Continue does not move on, and jumping forward through column 2 is refused, while the current section has mandatory fields missing. The form shows a Details missing error alert above the fields naming exactly what is missing, and each empty field turns red with its own message. Nothing alerts while a person is still on their first answer, and going back is always allowed. Column 2 marks the sections that still need attention, with a count.
- Footer. Back a step (secondary button, left arrow icon) on the left, hidden on the first section. On the right Save draft (secondary), then the primary action: Continue(right arrow icon) to move on, or the final action - Submit, Create project, Save changes - on the last section. Save draft is left out when a draft makes no sense, such as editing a live record. Every action sits here, where the task ends, and toasts appear above the footer, never on it.
- Leaving. The close button asks before discarding unsaved changes, and says what will be lost.
- Controls. Real DEW components only:
Input,TextArea,Select,MultiSelect,RadioGroup,Checkbox,Accordion. No bespoke tiles. If a control does not exist, mark it withGap(the?marker). - Draft. A draft needs only what identifies the record; submit validates the rest.
Usage
import { FormPage } from "@/app/pages/_shared/form-page";
import { FormRow } from "@/app/pages/_shared/form-row";
<FormPage
eyebrow="Data sharing agreement"
title="New agreement"
subtitle="Fields marked * are required to submit."
onCancel={goBack} // the X; ask before discarding
onSaveDraft={saveDraft} // omit when editing a live record
onBack={index > 0 ? previous : undefined}
problems={missing.length ? { items: missing } : undefined} // "Details missing" alert
primaryLabel={isLast ? "Submit" : "Continue"}
primaryIsContinue={!isLast}
onPrimary={isLast ? submit : next}
>
<FormRow title="Partner" required description="Who the agreement is with.">
<Input label="Organisation" isRequired />
</FormRow>
</FormPage>Where it is used
- Add Project, option 2:
/pages/project-registration/option-2, sections listed in column 2. - Data Sharing Agreement:
/pages/dsa/newand/pages/dsa/<id>/edit, sections listed in column 2. - Data Licence request:
/pages/dla/newand/pages/dla/<id>/edit, sections listed in column 2. - Exempt: Add Project option 1 (
/pages/project-registration), a one-question-per-card option kept for comparison.