Skip to content
Components

Form

A real <form>, with GdField's error slot driven by validation rules rather than by hand.

Anatomy

As it appears on the title — L14 RP80432

m²
m

Checked against the site area — a cross-field rule

A real form with real rules. Submit it empty: three messages appear AND focus moves to the first offending field. Type into one and the message clears as you fix it — but nothing is flagged before you have finished a field for the first time.
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

PropTypeDefault
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

default

The 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

PropTypeDefault
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

PropTypeDefault
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

PropTypeDefault
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.

Navigate

Esc