Form
A real <form>, with GdField's error slot driven by validation rules rather than by hand.
Anatomy
Code
const form = useGdForm({
initial: { lotPlan: '', area: '' },
rules: {
lotPlan: [required('Enter the lot and plan'), pattern(/^L\\d+\\s?[A-Z]{2}\\d+$/i, 'Format is like L14 RP80432')],
area: [required('Enter the site area'), range(1, 100000)]
},
onSubmit: (values) => save(values)
})
<GdForm :form="form">
<GdField label="Lot / plan" name="lotPlan" required :error="form.errors.lotPlan" v-slot="f">
<GdInput v-bind="f" v-model="form.values.lotPlan" @blur="form.blur('lotPlan')" />
</GdField>
<GdButton type="submit">Check this lot</GdButton>
</GdForm>When it validates
The timing is what decides whether a form feels hostile, so it is fixed rather than configurable.
- A field is checked when it is BLURRED, and on every change after that. Nobody is told their lot/plan is invalid while typing the third character, and nobody has to blur twice to watch a message go away.
- Submit checks everything and marks everything touched — at that point the user has asserted they are finished.
- Focus moves to the first invalid field. A submit that paints three fields red and does nothing else leaves a keyboard user standing on the button with no idea anything happened. It is the same omission as a tablist with no arrow keys: the visible half shipped, the announced half did not.
- First failure wins per field. Three messages under one control is a wall, and the second is usually a consequence of the first.
- A pending submit ignores further submits. A double-submitted lodgement is a real thing that happens.
Rules are functions
A rule is (value, values) => string | undefined. Return a message to fail; return nothing to pass. There is no schema library, and that is a decision rather than an omission.
- No dependency to add or age. A function needs none, and is typed by the values object it reads.
- A rule can see its siblings, which is what planning validation actually needs — "a rear setback deeper than the lot" is not expressible one field at a time. The specimen above does exactly this.
- A schema library solves a different problem. It buys parsing and coercion of untrusted input; this validates a form a person is typing into, where the control did the coercion and the parsing already happened. Server-side payload parsing is a separate problem and may well want a different tool.
- Planning rules belong to the app. What a lot/plan looks like depends on the jurisdiction, so the system ships only the rules that are about FORMS.
API
GdForm
Props
formReturnType<typeof useGdForm>—A `useGdForm(...)` instance. Passing it wires submission, the busy state and focus-on-failure; without it this is a plain `<form novalidate>` and the caller owns `@submit`.
labelstring—The accessible name, for a form not already under a heading.
Slots
defaultThe fields and the actions.
Events
submit[]Submitted and VALID. The invalid case never reaches here.
invalid[field: string]Submitted and rejected, naming the first offending field. Focus has already moved there.
useGdForm(options)
Props
initialrequiredT—The starting values, and what `reset()` returns to.
rulesGdRules<T>—One rule or a list per field. A field with no entry is never invalid.
onSubmit(values: T) => void | Promise<void>—Called by `submit()` only when everything passes. While it is pending, `submitting` is true and further submits are ignored — a double-submitted lodgement is a real thing that happens.
Returns
Props
valuesT—Reactive. Bind controls straight to it.
errorsPartial<Record<keyof T, string>>—Current messages, by field. A field passes by being absent.
touchedPartial<Record<keyof T, boolean>>—Which fields have been blurred, or marked by a submit attempt.
submittingRef<boolean>—An async `onSubmit` is in flight.
submittedRef<boolean>—A submit has been attempted — for "please fix the below" copy.
validComputedRef<boolean>—No errors right now. Not the same as "has been validated".
submit() => Promise<keyof T | undefined>—Validate everything, run `onSubmit` if it passes. Returns the first invalid field's name, or undefined on success.
blur(name: keyof T) => void—Mark touched and check — what a control's blur should call.
change(name: keyof T) => void—Re-check, but only if already touched or submitted.
reset() => void—Back to `initial`, errors and touched cleared.
field(name: keyof T) => object—Everything one field needs in one spread — the escape hatch for a control GdField cannot drive.
Shipped rules
Props
required(message?) => GdRule—Present. Rejects empty strings, empty arrays, null and undefined — but NOT `0` or `false`, which are answers.
minLength(n, message?) => GdRule—At least n characters, ignoring surrounding space. Passes on empty — pair it with `required`.
range(min, max, message?) => GdRule—Within a numeric range, inclusive. Passes on empty.
pattern(re, message) => GdRule—Matches. The message is required, because "Invalid format" tells nobody anything.