Skip to content

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.

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.

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.

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.

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.

03-messages.ts
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',
}),
];
03-messages.json
{
"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.

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.

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.

A custom item renderer →