Skip to content

Rent a car: Search as you type

Hard-coding the car list works for a demo but real applications fetch it from a service. The dropdown widget has two events that hook into async data:

  • onLoad — fired when the dropdown is opened with no items loaded yet.
  • onFilter — fired as the user types in the search input.

Wire both to event names, and handle them in your application’s formEvent callback.

Create a new file alongside your page component — car.service.ts is a common convention (Angular users may prefer wrapping this in an @Injectable() class; the rest of the chapter uses the plain exported function for brevity). It returns cars matching a query string, faking network latency with setTimeout:

car.service.ts
type Car = { id: string; label: string; img: string; price: number };
const ALL_CARS: Car[] = [
{ id: 'compact', label: 'Compact', img: '🚗', price: 35 },
{ id: 'suv', label: 'SUV', img: '🚙', price: 75 },
{ id: 'convertible', label: 'Convertible', img: '🏎️', price: 110 },
{ id: 'luxury', label: 'Luxury', img: '🚘', price: 180 },
{ id: 'minivan', label: 'Minivan', img: '🚐', price: 95 },
{ id: 'pickup', label: 'Pickup', img: '🛻', price: 85 },
];
export function searchCars(query: string): Promise<Car[]> {
return new Promise((resolve) => {
setTimeout(() => {
const q = query.toLowerCase();
resolve(ALL_CARS.filter((c) => c.label.toLowerCase().includes(q)));
}, 250);
});
}

In the form definition, attach the event names to the dropdown’s load and filter event hooks:

gui.inputs.dropdown('car', {
labelField: 'label',
valueField: 'id',
itemRenderer: 'carItemRenderer',
inputDebounce: 300,
onLoad: 'loadCars',
onFilter: 'filterCars',
items: [],
label: 'Select car',
validator: {
type: 'string',
required: true,
messages: {
required: 'Please pick a car model',
invalid: 'Please pick a car model',
},
},
}),
{
"kind": "input",
"type": "dropdown",
"path": "car",
"label": "Select car",
"on": { "load": "loadCars", "filter": "filterCars" },
"props": {
"labelField": "label",
"valueField": "id",
"itemRenderer": "carItemRenderer",
"inputDebounce": 300,
"items": []
},
"validator": {
"type": "string",
"required": true,
"messages": {
"required": "Please pick a car model",
"invalid": "Please pick a car model"
}
}
}

Starting with an empty items list makes the intent explicit — no hard-coded list; the load event populates the dropdown the first time it opens and the filter event re-populates it as the user types. The inputDebounce: 300 prop tells the dropdown to wait 300ms after the user stops typing before firing the filter event.

Define a handleFormEvent handler that matches on the event name, calls searchCars, and dispatches an OVERRIDE_WIDGET_PROP action to update the dropdown’s items prop. Then wire it to the form component’s formEvent output — each framework spells that wiring differently. The snippets below import the JSON form file (./rent-a-car.form.json); if you’re using the programmatic API, swap the import path for ./rent-a-car.form (the .ts file):

rent-a-car.page.tsx
import { GuiForm } from '@golemui/gui-react';
import type { FormEvent } from '@golemui/core';
import { searchCars } from './car.service';
import rentACarForm from './rent-a-car.form.json';
import { CarItemRenderer } from './CarItemRenderer';
const config = {
formDef: rentACarForm,
// …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters…
};
async function handleFormEvent(event: FormEvent) {
if (event.name === 'loadCars' || event.name === 'filterCars') {
const query = event.detail ?? '';
const cars = await searchCars(query);
event.callback({
type: 'OVERRIDE_WIDGET_PROP',
payload: { path: 'car', prop: 'items', value: cars },
});
}
}
export function RentACarPage() {
return <GuiForm config={config} formEvent={handleFormEvent} />;
}

Pass handleFormEvent to <gui-form> via the formEvent callback (or, in vanilla JS, via addEventListener('formEvent', …)).

05-search.ts
import { gui } from '@golemui/gui-shared';
export default [
gui.inputs.dropdown('car', {
labelField: 'label',
valueField: 'id',
itemRenderer: 'carItemRenderer',
inputDebounce: 300,
label: 'Select car',
validator: {
type: 'string',
required: true,
messages: {
required: 'Please pick a car model',
invalid: 'Please pick a car model',
},
},
onLoad: 'loadCars',
onFilter: 'filterCars',
}),
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',
},
},
}),
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',
},
},
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',
},
},
}),
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',
},
},
}),
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',
},
},
}),
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',
},
},
include: {
in: ['hasDiscount'],
},
}),
gui.actions.button({
label: 'Reserve',
actionType: 'submit',
}),
];
05-search.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"
}
},
"on": { "load": "loadCars", "filter": "filterCars" },
"props": {
"labelField": "label",
"valueField": "id",
"itemRenderer": "carItemRenderer",
"inputDebounce": 300
}
},
{
"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" }
},
"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" }
},
"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"
}
}
},
{
"kind": "input",
"type": "radiogroup",
"path": "rentalType",
"label": "Rental type",
"validator": {
"type": "string",
"required": true,
"messages": { "required": "Choose Daily, Weekly, or Monthly" }
},
"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"
}
}
},
{
"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"
}
}
},
{
"kind": "action",
"type": "button",
"label": "Reserve",
"actionType": "submit"
}
]
}

Type in the dropdown — the list filters server-side after a 300ms pause.

Submitting the form →