Rent a car: Validation errors
Every required field in the Rent a Car form needs a validator. The form’s health stays errored until every validator passes; once it goes ok, the Reserve button can fire its formSubmit event.
Adding validators
Section titled “Adding validators”A validator is a per-widget object listing rules — required, minLength, maxLength, minItems, maxItems, const, pattern, type, etc. Each rule that fails produces an error on that field.
Here’s what each field in the form needs:
gui.inputs.dropdown('car', { validator: { type: 'string', required: true }, // …items…});
gui.inputs.dropdown('collectOffice', { validator: { type: 'string', required: true }, // …items…});
gui.inputs.dropdown('returnOffice', { validator: { type: 'string', required: true }, include: { in: ['differentReturn'] }, // …items…});
gui.inputs.rangeCalendar('rentalDates', { numberOfMonths: 2, validator: { required: true, minItems: 1, maxItems: 1 },});
gui.inputs.radiogroup('rentalType', { validator: { type: 'string', required: true }, // …options…});
gui.inputs.booleanInput('driverOver25', { validator: { const: true, required: true },});
gui.inputs.textInput('discountCode', { validator: { required: true, minLength: 4 }, include: { in: ['hasDiscount'] },});{ "kind": "input", "type": "dropdown", "path": "car", "validator": { "type": "string", "required": true }}
{ "kind": "input", "type": "dropdown", "path": "collectOffice", "validator": { "type": "string", "required": true }}
{ "kind": "input", "type": "dropdown", "path": "returnOffice", "include": { "in": ["differentReturn"] }, "validator": { "type": "string", "required": true }}
{ "kind": "input", "type": "rangeCalendar", "path": "rentalDates", "props": { "numberOfMonths": 2 }, "validator": { "required": true, "minItems": 1, "maxItems": 1 }}
{ "kind": "input", "type": "radiogroup", "path": "rentalType", "validator": { "type": "string", "required": true }}
{ "kind": "input", "type": "toggle", "path": "driverOver25", "validator": { "type": "boolean", "const": true, "required": true }}
{ "kind": "input", "type": "textinput", "path": "discountCode", "include": { "in": ["hasDiscount"] }, "validator": { "required": true, "minLength": 4 }}The two driver-of-the-states toggles (differentReturn and hasDiscountCode) carry no validator — they’re optional controls.
Default messages
Section titled “Default messages”By default, GolemUI’s validators show concise, generic messages from Zod. They’re fine, but for a real product you’ll usually want copy that matches your tone and explains what the user needs to do.
Every validator’s messages property lets you override the message per rule key — required, minLength, maxLength, pattern, minItems, maxItems, const, invalid, etc.
The shape
Section titled “The shape”Each messages entry maps a rule key to either a plain string or a Localizable (i18n) descriptor:
validator: { required: true, minLength: 4, messages: { required: 'Enter your discount code', minLength: 'Discount codes are at least 4 characters', },}"validator": { "required": true, "minLength": 4, "messages": { "required": "Enter your discount code", "minLength": "Discount codes are at least 4 characters" }}If a rule fires and there’s a matching key in messages, the engine renders your copy instead of Zod’s default. Rules without a custom message keep the default.
Sweep through every validator
Section titled “Sweep through every validator”Below, every validator in the Rent a Car form gets human-friendly messages — picking the car, picking offices, the rental dates range, the rental type, the over-25 confirmation, and the discount code.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.dropdown('car', { labelField: 'label', valueField: 'id', items: [ { id: 'compact', label: 'Compact', }, { id: 'suv', label: 'SUV', }, { id: 'convertible', label: 'Convertible', }, { id: 'luxury', label: 'Luxury', }, ], label: 'Select car', validator: { type: 'string', required: true, messages: { required: 'Please pick a car model', invalid: 'Please pick a car model', }, }, }), gui.layouts.grid([ gui.inputs.dropdown('collectOffice', { labelField: 'label', valueField: 'id', items: [ { id: 'lhr', label: 'London Heathrow', }, { id: 'cdg', label: 'Paris CDG', }, { id: 'fra', label: 'Frankfurt Main', }, ], label: 'Collect from office', validator: { type: 'string', required: true, messages: { required: 'Choose where you\'ll pick up the car', invalid: 'Pick a valid pickup location', }, }, }), gui.inputs.dropdown('returnOffice', { labelField: 'label', valueField: 'id', items: [ { id: 'lhr', label: 'London Heathrow', }, { id: 'cdg', label: 'Paris CDG', }, { id: 'fra', label: 'Frankfurt Main', }, ], label: 'Return to office', validator: { type: 'string', required: true, messages: { required: 'Choose where you\'ll drop the car off', invalid: 'Pick a valid return location', }, }, include: { in: ['differentReturn'], }, }), ], { direction: 'row', autoFit: true, }), gui.inputs.booleanInput('differentReturn', { label: 'Choose a different return location', }), gui.inputs.rangeCalendar('rentalDates', { numberOfMonths: 2, label: 'Rental dates', validator: { required: true, minItems: 1, maxItems: 1, messages: { required: 'Please select your rental dates', minItems: 'Pick a rental date range', maxItems: 'Only one date range, please', invalid: 'Rental dates must be a valid date range', }, }, }), gui.inputs.radiogroup('rentalType', { options: [ { label: 'Daily', value: 'daily', }, { label: 'Weekly', value: 'weekly', }, { label: 'Monthly', value: 'monthly', }, ], label: 'Rental type', validator: { type: 'string', required: true, messages: { required: 'Choose Daily, Weekly, or Monthly', invalid: 'Pick one of the rental types above', }, }, }), gui.inputs.booleanInput('driverOver25', { label: 'Driver aged over 25', validator: { const: true, required: true, messages: { const: 'Drivers must be at least 25 years old to rent', required: 'Confirm the driver is over 25', invalid: 'Confirm the driver is over 25', }, }, }), gui.inputs.booleanInput('hasDiscountCode', { label: 'I have a discount code', }), gui.inputs.textInput('discountCode', { label: 'Discount code', validator: { required: true, minLength: 4, messages: { required: 'Enter your discount code', minLength: 'Discount codes are at least 4 characters', invalid: 'Discount codes must be plain text', }, }, include: { in: ['hasDiscount'], }, }), gui.actions.button({ label: 'Reserve', actionType: 'submit', }),];{ "states": { "differentReturn": "$form.differentReturn === true", "hasDiscount": "$form.hasDiscountCode === true" }, "form": [ { "kind": "input", "type": "dropdown", "path": "car", "label": "Select car", "validator": { "type": "string", "required": true, "messages": { "required": "Please pick a car model", "invalid": "Please pick a car model" } }, "props": { "labelField": "label", "valueField": "id", "items": [ { "id": "compact", "label": "Compact" }, { "id": "suv", "label": "SUV" }, { "id": "convertible", "label": "Convertible" }, { "id": "luxury", "label": "Luxury" } ] } }, { "kind": "layout", "type": "grid", "props": { "direction": "row", "autoFit": true }, "children": [ { "kind": "input", "type": "dropdown", "path": "collectOffice", "label": "Collect from office", "validator": { "type": "string", "required": true, "messages": { "required": "Choose where you'll pick up the car", "invalid": "Pick a valid pickup location" } }, "props": { "labelField": "label", "valueField": "id", "items": [ { "id": "lhr", "label": "London Heathrow" }, { "id": "cdg", "label": "Paris CDG" }, { "id": "fra", "label": "Frankfurt Main" } ] } }, { "kind": "input", "type": "dropdown", "path": "returnOffice", "label": "Return to office", "include": { "in": ["differentReturn"] }, "validator": { "type": "string", "required": true, "messages": { "required": "Choose where you'll drop the car off", "invalid": "Pick a valid return location" } }, "props": { "labelField": "label", "valueField": "id", "items": [ { "id": "lhr", "label": "London Heathrow" }, { "id": "cdg", "label": "Paris CDG" }, { "id": "fra", "label": "Frankfurt Main" } ] } } ] }, { "kind": "input", "type": "toggle", "path": "differentReturn", "label": "Choose a different return location" }, { "kind": "input", "type": "rangeCalendar", "path": "rentalDates", "label": "Rental dates", "props": { "numberOfMonths": 2 }, "validator": { "type": "array", "required": true, "minItems": 1, "maxItems": 1, "messages": { "required": "Please select your rental dates", "minItems": "Pick a rental date range", "maxItems": "Only one date range, please", "invalid": "Rental dates must be a valid date range" } } }, { "kind": "input", "type": "radiogroup", "path": "rentalType", "label": "Rental type", "validator": { "type": "string", "required": true, "messages": { "required": "Choose Daily, Weekly, or Monthly", "invalid": "Pick one of the rental types above" } }, "props": { "options": [ { "label": "Daily", "value": "daily" }, { "label": "Weekly", "value": "weekly" }, { "label": "Monthly", "value": "monthly" } ] } }, { "kind": "input", "type": "toggle", "path": "driverOver25", "label": "Driver aged over 25", "validator": { "type": "boolean", "const": true, "required": true, "messages": { "const": "Drivers must be at least 25 years old to rent", "required": "Confirm the driver is over 25", "invalid": "Confirm the driver is over 25" } } }, { "kind": "input", "type": "toggle", "path": "hasDiscountCode", "label": "I have a discount code" }, { "kind": "input", "type": "textinput", "path": "discountCode", "label": "Discount code", "include": { "in": ["hasDiscount"] }, "validator": { "type": "string", "required": true, "minLength": 4, "messages": { "required": "Enter your discount code", "minLength": "Discount codes are at least 4 characters", "invalid": "Discount codes must be plain text" } } }, { "kind": "action", "type": "button", "label": "Reserve", "actionType": "submit" } ]}Try clicking Reserve without filling anything in: each field shows its custom message. Try selecting more than one date range in the calendar: the maxItems message kicks in. Toggle Driver aged over 25 off and back on without confirming and the boolean const message explains what’s expected.
Disabling submit until valid
Section titled “Disabling submit until valid”GolemUI exposes a built-in $formIsInvalid form property — true whenever any validator is failing. Bind it to the button’s disabled.when to keep the button greyed out until everything passes:
gui.actions.button({ label: 'Reserve', actionType: 'submit', disabled: { when: '$formIsInvalid' },}),{ "kind": "action", "type": "button", "label": "Reserve", "actionType": "submit", "disabled": { "when": "$formIsInvalid" }}That’s the visual lock-out. The behavioural one comes from actionType: 'submit': it renders the button as <button type="submit"> and triggers form-level validation on click, so an invalid form never fires formSubmit even if the disabled binding is removed. See Submitting the form for the full submit wiring.
Localizing messages
Section titled “Localizing messages”For multi-language apps, swap any string for a Localizable:
messages: { required: { key: 'rentACar.car.required', default: 'Please pick a car model' },}"messages": { "required": { "key": "rentACar.car.required", "default": "Please pick a car model" }}The default text fires when the active translator can’t resolve the key. See Features / i18n for how to wire a translator.
See also
Section titled “See also”- Features / Validators — every validator type and its rule keys.
- Form Definition API / Reference / Inputs — every input’s
validatorprops.