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.
Step 1 — A simple service stub
Section titled “Step 1 — A simple service stub”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:
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); });}Step 2 — Wire onLoad and onFilter
Section titled “Step 2 — Wire onLoad and onFilter”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.
Step 3 — Handle the events
Section titled “Step 3 — Handle the events”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):
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} />;}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { FormComponent } from '@golemui/gui-angular';import type { FormEvent } from '@golemui/core';import { searchCars } from './car.service';import rentACarForm from './rent-a-car.form.json';import { CarItemRenderer } from './car-item-renderer.component';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-rent-a-car', template: ` <gui-form [config]="config" (formEvent)="handleFormEvent($event)" ></gui-form> `,})export class RentACarPage { protected config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters… };
async 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 }, }); } }}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-lit';import type { FormEvent } from '@golemui/core';import { searchCars } from './car.service';import { carItemRenderer } from './car-item-renderer';import rentACarForm from './rent-a-car.form.json';
@customElement('rent-a-car-page')export class RentACarPage extends LitElement { config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters… };
async 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 }, }); } }
override createRenderRoot() { return this; }
override render() { return html` <gui-form .config=${this.config} @formEvent=${(e: CustomEvent<FormEvent>) => this.handleFormEvent(e.detail)} ></gui-form> `; }}<script setup lang="ts">import { GuiForm } from '@golemui/gui-vue';import type { FormEvent } from '@golemui/core';import { searchCars } from './car.service';import rentACarForm from './rent-a-car.form.json';import CarItemRenderer from './CarItemRenderer.vue';
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 }, }); }}</script>
<template> <GuiForm :config="config" @form-event="handleFormEvent" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import { searchCars } from './car.service';import { carItemRenderer } from './car-item-renderer';import rentACarForm from './rent-a-car.form.json';
const config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters…};
async function handleFormEvent(event) { 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 }, }); }}
const form = document.getElementById('app-form');form.config = config;form.addEventListener('formEvent', (e) => handleFormEvent(e.detail));Pass handleFormEvent to <gui-form> via the formEvent callback (or, in vanilla JS, via addEventListener('formEvent', …)).
Result
Section titled “Result”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', }),];{ "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.
See also
Section titled “See also”- Features / Form Events — every event hook.
- Widgets Reference / Dropdown — every dropdown prop.