Skip to content

Rent a car: A custom item renderer

The default dropdown item template is just a label. Real applications usually want richer items — a thumbnail, the title, secondary metadata. GolemUI lets you supply a custom item renderer for that.

The car items now carry an emoji thumbnail and a price:

items: [
{ 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 },
],

Item renderers are framework-specific.

CarItemRenderer.tsx
import type { ReactItemRenderer } from '@golemui/react';
type Car = { id: string; label: string; img: string; price: number };
export const CarItemRenderer: ReactItemRenderer<Car> = ({ template, selected }) => (
<div className={`car-renderer${selected ? ' selected' : ''}`}>
<span className="car-renderer__img">{template?.img}</span>
<span className="car-renderer__label">{template?.label}</span>
<span className="car-renderer__price">${template?.price}/day</span>
</div>
);

template is the per-item data — typed as Car | undefined because the list can render placeholder rows while items load, so use optional chaining (template?.foo).

Drop the following into a global stylesheet:

.car-renderer {
display: flex;
align-items: center;
padding: var(--gui-space-2);
gap: var(--gui-space-3);
cursor: pointer;
&:hover {
background-color: var(--gui-intent-primary-hover);
color: var(--gui-text-inverse);
.car-renderer__price {
color: var(--gui-text-inverse);
}
}
}
.car-renderer.selected {
background-color: var(--gui-intent-primary-active);
color: var(--gui-text-inverse);
.car-renderer__price {
color: var(--gui-text-inverse);
}
}
.car-renderer__img {
font-size: var(--gui-font-xl);
}
.car-renderer__label {
flex: 1;
}
.car-renderer__price {
color: var(--gui-text-secondary);
}

itemRenderers is a top-level key on the same config object you’ve been wiring since the intro chapter — it maps a renderer name to the framework component. Unlike states, it always lives on the component (the form file can only carry a string name, never a real component reference):

In the programmatic API, itemRenderers sits next to formDef and the formConfig we set up for states:

rent-a-car.page.tsx
const config = {
formDef: rentACarForm,
formConfig: {
states: {
differentReturn: '$form.differentReturn === true',
hasDiscount: '$form.hasDiscountCode === true',
},
},
itemRenderers: {
carItemRenderer: CarItemRenderer,
},
};

The dropdown’s itemRenderer prop carries the name you registered:

04-renderer.ts
import { gui } from '@golemui/gui-shared';
export default [
gui.inputs.dropdown('car', {
labelField: 'label',
valueField: 'id',
itemRenderer: 'carItemRenderer',
items: [
{
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,
},
],
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',
},
},
}),
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',
}),
];

Search as you type →