Item Renderers
Item renderers are framework-specific functions or components that the form engine invokes to render each item of a dropdown, list, or radiogroup. The engine forwards an ItemRenderContext<T> object that carries the raw item plus useful metadata (selected, disabled, focused, index).
The contract
Section titled “The contract”Each framework adapts the same ItemRenderContext<T> to its idiomatic shape:
| Framework | Type | What the engine passes |
|---|---|---|
| React | ReactItemRenderer<T> | A React.ComponentType<ItemRenderContext<T>> — the context fields arrive as props. |
| Angular | AngularItemRenderer<T> | A component class structurally matching AngularItemRenderContext<T> — one signal input() per context field. |
| Lit | LitItemRenderer<T> | A (ctx: ItemRenderContext<T>) => TemplateResult — called as a function, not a custom element. |
| Vue | VueItemRenderer<T> | A Component<Partial<ItemRenderContext<T>>> — the context fields arrive as props on the SFC. |
| Vanilla JS | (ctx) => HTMLElement | A plain function called with ItemRenderContext<T> that returns a DOM node — like Lit’s, but returning real DOM instead of a TemplateResult. |
The shared context shape comes from @golemui/core:
export interface ItemRenderContext<T extends ItemRenderItemData> { template: T; value: string | number; index: number; selected?: boolean; disabled?: boolean; focused?: boolean;}template is the raw item from the widget’s items array. The other fields reflect the live state of the option in the rendered list.
Example — country renderer
Section titled “Example — country renderer”import type { ReactItemRenderer } from '@golemui/react';
type Country = { id: string; flag: string; label: string };
export const CountryItemRenderer: ReactItemRenderer<Country> = ({ template, index, selected, disabled, focused,}) => ( <div className={`country-item ${selected ? 'country-item--selected' : ''} ${ disabled ? 'country-item--disabled' : '' } ${focused ? 'country-item--focused' : ''} ${index % 2 ? 'country-item--odd' : ''}`} > <span className="country-item__flag">{template.flag}</span> <span className="country-item__label">{template.label}</span> </div>);Props are the full ItemRenderContext<T> — destructure whichever fields you need.
import { Component, input } from '@angular/core';import { type AngularItemRenderContext } from '@golemui/angular';
type Country = { id: string; flag: string; label: string };
@Component({ standalone: true, selector: 'country-item-renderer', template: ` <div class="country-item" [class.country-item--selected]="selected()" [class.country-item--disabled]="disabled()" [class.country-item--focused]="focused()" [class.country-item--odd]="index() % 2" > <span class="country-item__flag">{{ template().flag }}</span> <span class="country-item__label">{{ template().label }}</span> </div> `,})export class CountryItemRenderer implements AngularItemRenderContext<Country> { template = input.required<Country>(); value = input.required<string | number>(); index = input.required<number>(); selected = input<boolean | undefined>(undefined); disabled = input<boolean | undefined>(undefined); focused = input<boolean | undefined>(undefined);}Declare one signal input per context field; the engine binds each one when rendering an item. Read each value by calling the signal — template().flag, selected(), etc.
implements AngularItemRenderContext<T> is the recommended contract for signal-based renderers: TypeScript enforces the full set of inputs and their signal types, so a missing or mistyped field is a compile error.
import { html, TemplateResult } from 'lit';import { classMap } from 'lit/directives/class-map.js';import { ItemRenderContext } from '@golemui/core';
type Country = { id: string; flag: string; label: string };
export const countryItemRenderer = ( ctx: ItemRenderContext<Country>,): TemplateResult => { const classes = { 'country-item': true, 'country-item--selected': !!ctx.selected, 'country-item--disabled': !!ctx.disabled, 'country-item--focused': !!ctx.focused, 'country-item--odd': ctx.index % 2 !== 0, };
return html` <div class=${classMap(classes)}> <span class="country-item__flag">${ctx.template.flag}</span> <span class="country-item__label">${ctx.template.label}</span> </div> `;};Lit renderers are functions — the engine invokes them with an ItemRenderContext<T> argument and inlines the returned template into the dropdown / list. A LitElement class with an @property() item field will not be picked up by the engine.
<script setup lang="ts">import type { ListItemRendererProps } from '@golemui/gui-vue';
type Country = { id: string; flag: string; label: string };
defineProps<ListItemRendererProps<Country>>();</script>
<template> <div :class="{ 'country-item': true, 'country-item--selected': !!selected, 'country-item--disabled': !!disabled, 'country-item--focused': !!focused, 'country-item--odd': index % 2 !== 0, }" > <span class="country-item__flag">{{ template?.flag }}</span> <span class="country-item__label">{{ template?.label }}</span> </div></template>Vue renderers are SFCs. Use optional chaining on template?.flag because the props can be undefined while items are loading (ListItemRendererProps<T> marks template and value optional for that reason).
export const countryItemRenderer = (ctx) => { const root = document.createElement('div'); root.className = 'country-item'; if (ctx.selected) root.classList.add('country-item--selected'); if (ctx.disabled) root.classList.add('country-item--disabled'); if (ctx.focused) root.classList.add('country-item--focused'); if (ctx.index % 2 !== 0) root.classList.add('country-item--odd');
const flag = document.createElement('span'); flag.className = 'country-item__flag'; flag.textContent = ctx.template.flag;
const label = document.createElement('span'); label.className = 'country-item__label'; label.textContent = ctx.template.label;
root.append(flag, label); return root;};Vanilla renderers are plain functions called with an ItemRenderContext<T> — just like Lit’s, but they return a real DOM node rather than a TemplateResult. The engine inserts the returned element directly into the dropdown / list.
Registering the renderer
Section titled “Registering the renderer”The widget references the renderer by name through its itemRenderer prop. Where the renderer map lives depends on the path:
In the Programmatic API the renderer map goes inside formConfig.itemRenderers:
import { gui } from '@golemui/gui-shared';import { CountryItemRenderer } from './country-item-renderer';
const config = { formDef: [ gui.inputs.dropdown('country', { items: [ /* … */ ], itemRenderer: 'countryItemRenderer', }), ], formConfig: { itemRenderers: { countryItemRenderer: CountryItemRenderer } },};
// <GuiForm config={config} />In the JSON path the renderer map is a top-level property of the config object — there is no formConfig wrapper for JSON forms. The JSON references the renderer by name on the widget.
{ "form": [ { "kind": "input", "type": "dropdown", "path": "country", "items": [], "itemRenderer": "countryItemRenderer" } ]}import { CountryItemRenderer } from './country-item-renderer';import formDef from './my-form.json';
const config = { formDef, itemRenderers: { countryItemRenderer: CountryItemRenderer },};
// <GuiForm config={config} />See also
Section titled “See also”- Features / Item Renderers — wiring renderers in
formConfig. - Getting Started / Custom renderer for the car list — a full step-by-step example.