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.

Pretend you have a service that returns cars matching a query string. We’ll fake it with a setTimeout to emulate network latency:

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 },
];
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 event handlers:

import { gui } from '@golemui/gui-shared';
gui.inputs.dropdown('car', {
label: 'Select car',
labelField: 'label',
valueField: 'id',
itemRenderer: 'carItemRenderer',
inputDebounce: 300,
on: { load: 'loadCars', filter: 'filterCars' },
});

The inputDebounce: 300 prop tells the dropdown to wait 300ms after the user stops typing before firing onFilter.

In your formEvent callback, populate the dropdown by emitting an update for the dropdown’s items:

async function handleFormEvent(event) {
if (event.name === 'loadCars' || event.name === 'filterCars') {
const query = event.detail ?? '';
const cars = await searchCars(query);
event.update({ path: 'car', items: cars });
}
}

Pass handleFormEvent to <gui-form> via the formEvent callback.

04-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,
},
onLoad: 'loadCars',
onFilter: 'filterCars',
}),
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,
},
}),
gui.inputs.booleanInput('differentReturn', {
label: 'Choose a different return 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,
},
}),
gui.inputs.rangeCalendar('rentalDates', {
label: 'Rental dates',
validator: {
type: 'array',
required: true,
minItems: 2,
maxItems: 2,
},
}),
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,
},
}),
gui.inputs.booleanInput('driverOver25', {
label: 'Driver aged over 25',
validator: {
type: 'boolean',
const: true,
required: true,
},
}),
gui.inputs.booleanInput('hasDiscountCode', {
label: 'I have a discount code',
}),
gui.inputs.textInput('discountCode', {
label: 'Discount code',
validator: {
type: 'string',
required: true,
minLength: 4,
},
}),
gui.actions.button({
label: 'Reserve',
uid: 'submit',
onClick: 'submit',
}),
];

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

Submitting the form →