Item renderers
<gui-list> and
<gui-multi-list> handle the keyboard, the focus, the
selection and the scrolling, but they don’t draw their items. You render the items yourself, with
your framework’s own markup, as children of the list. That way an item can hold anything: an icon,
two lines of text, a price, a badge.
This list of 43 cities is rendered that way. The code is the same example in each framework:
import { useState } from 'react';import { GuiLabel, GuiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const cities = [ 'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava', 'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh', 'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon', 'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples', 'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville', 'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw', 'Zagreb', 'Zurich',];
export function CityField() { const [city, setCity] = useState<OptionValue>(); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
return ( <div className="gui-field"> <GuiLabel uid="city" label="City" /> <div className="gui-widget"> <GuiList id="city" uid="city" label="City" height={200} items={cities} value={city} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail)} onGuiChange={(event) => setCity(event.detail.value)} > {/* The list draws no items: it reports the ones to render, and you add them as its children. One option per item, from its id and state. A click on it picks the item. */} {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> {String(item.template)} </div> </div> ); })} </GuiList> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap<typeof GuiListEvents>[K];
@Component({ selector: 'app-city-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200" [items]="cities" [value]="city()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > <!-- One option per item, from its id and state. A click on it picks the item. --> @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > {{ item.template }} </div> </div> } </gui-list> </div> </div> `,})export class CityFieldComponent { cities = [ 'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava', 'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh', 'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon', 'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples', 'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville', 'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw', 'Zagreb', 'Zurich', ];
city = signal<OptionValue | undefined>(undefined); visibleItems = signal<GuiVisibleItem[]>([]);
// The list draws no items: it reports the ones to render, and you render // them as its children. onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.city.set((event as ListEvent<'gui-change'>).detail.value); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
const cities = [ 'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava', 'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh', 'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon', 'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples', 'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville', 'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw', 'Zagreb', 'Zurich',];
const city = ref<OptionValue>();// The list draws no items: it reports the ones to render, and you add them as its children.const visibleItems = ref<GuiVisibleItem[]>([]);</script>
<template> <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" :height="200" :items.prop="cities" :value.prop="city" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="city = $event.detail.value" > <!-- One option per item, from its id and state. A click on it picks the item. --> <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > {{ item.template }} </div> </div> </gui-list> </div> </div></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap<typeof GuiListEvents>[K];
const cities = [ 'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava', 'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh', 'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon', 'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples', 'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville', 'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw', 'Zagreb', 'Zurich',];
@customElement('city-field')export class CityField extends LitElement { @state() private city?: OptionValue; @state() private visibleItems: GuiVisibleItem[] = [];
protected override createRenderRoot() { return this; }
// The list draws no items: it reports the ones to render, and you render // them as its children. private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.city = event.detail.value; }
override render() { return html` <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200" .items=${cities} .value=${this.city} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, // One option per item, from its id and state. A click on it // picks the item. (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > ${item.template} </div> </div> `, )} </gui-list> </div> </div> `; }}<div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200"></gui-list> </div></div>
<script type="module"> const list = document.querySelector('#city'); // The list draws no items: it reports the ones to render, and you add them as its children. list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
// One option per item, from its id and state. A click on it picks the item. function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.textContent = item.template; option.append(text); return option; }
list.items = [ 'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava', 'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh', 'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon', 'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples', 'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville', 'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw', 'Zagreb', 'Zurich', ];</script><gui-dropdown> and
<gui-multi-dropdown> draw their options themselves.
To change what an option shows, give them a renderItem function: see
Dropdown options.
The items to render
Section titled “The items to render”The list tells you which items to render. Whenever they change, it fires
gui-visible-items-change with the items in event.detail, and visibleItems holds them too.
Your code renders one option per item, as the list’s children, and does nothing else: the list
keeps the active item and the selection, and picks an item on a click, on Enter and on
Space. It reports only the items in view and a few on each side, so a list of 10,000
items still renders about 15 options.
Each item has everything its option needs:
| Key | What it is |
|---|---|
template | The item as you gave it: a string, a number or your object, with all its keys to render. |
value | The item’s value, from value-field for objects. |
index | The item’s index in items. |
id | The option’s id, <uid>-item-<index>. |
selected | Whether the item is selected. |
focused | Whether it is the active item, the one the arrow keys move. |
disabled | Whether it can’t be picked: it has disabled: true, or the whole list is disabled. |
Keep your selected value in step from gui-change, as the examples do.
What each option needs
Section titled “What each option needs”Each option is a child of the list, and:
- its
idis the item’sid: the list pointsaria-activedescendantat it to tell screen readers which item is active, and finds the item a click is on from it; - it has
role="option",aria-selectedfrom the item’sselected, andaria-disabled="true"when the item isdisabled; - its height is
item-height, 40px by default, since the list places the options from it.
Give the option the gui-list__item-wrapper class and wrap its content in gui-list__item, with
gui-list__item-selected, gui-list__item-focused or gui-list__item-disabled as they apply,
and the stylesheet styles it like the other fields. The classes are in the List page’s
Styling section.
Key each option by item.value, so your framework keeps the options it already rendered as the
user scrolls instead of rewriting them.
Multi-select list
Section titled “Multi-select list”A <gui-multi-list> renders its options the same way. What changes is the value:
- its value is
values, an array, and an item isselectedwhen its value is in it; - a click, Enter or Space adds the item’s value to
values, or takes it out, and firesgui-item-togglewith the item’s value, thengui-inputandgui-changewith the newvalues.
An element with the gui-list__item-check class in the option’s text is a check box: the
stylesheet shows its icon while the option has gui-list__item-selected.
import { useState } from 'react';import { GuiLabel, GuiMultiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
export function LanguagesField() { const [values, setValues] = useState<OptionValue[]>(['Catalan']); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
return ( <> <div className="gui-field"> <GuiLabel uid="languages" label="Languages you speak" /> <div className="gui-widget"> <GuiMultiList id="languages" uid="languages" label="Languages you speak" height={200} items={languages} values={values} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail) } onGuiChange={(event) => setValues(event.detail.value ?? [])} > {/* The list draws no items: it reports the ones to render, and you add them as its children. One option per item, from its id and state. A click on it adds the item or takes it out. */} {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> <span className="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{String(item.template)}</span> </div> </div> ); })} </GuiMultiList> </div> </div> <p role="status">Selected: {values.join(', ') || 'none'}</p> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap<typeof GuiMultiListEvents>[K];
@Component({ selector: 'app-languages-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" [items]="languages" [values]="values()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > <!-- One option per item, from its id and state. A click on it adds the item or takes it out. --> @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{{ item.template }}</span> </div> </div> } </gui-multi-list> </div> </div> <p role="status">Selected: {{ values().join(', ') || 'none' }}</p> `,})export class LanguagesFieldComponent { languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese', ];
values = signal<OptionValue[]>(['Catalan']); visibleItems = signal<GuiVisibleItem[]>([]);
// The list draws no items: it reports the ones to render, and you render // them as its children. onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.values.set((event as ListEvent<'gui-change'>).detail.value ?? []); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
const values = ref<OptionValue[]>(['Catalan']);// The list draws no items: it reports the ones to render, and you add them as its children.const visibleItems = ref<GuiVisibleItem[]>([]);</script>
<template> <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" :height="200" :items.prop="languages" :values.prop="values" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="values = $event.detail.value ?? []" > <!-- One option per item, from its id and state. A click on it adds the item or takes it out. --> <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{{ item.template }}</span> </div> </div> </gui-multi-list> </div> </div> <p role="status">Selected: {{ values.join(', ') || 'none' }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap<typeof GuiMultiListEvents>[K];
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
@customElement('languages-field')export class LanguagesField extends LitElement { @state() private values: OptionValue[] = ['Catalan']; @state() private visibleItems: GuiVisibleItem[] = [];
protected override createRenderRoot() { return this; }
// The list draws no items: it reports the ones to render, and you render // them as its children. private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.values = event.detail.value ?? []; }
override render() { return html` <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" .items=${languages} .values=${this.values} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, // One option per item, from its id and state. A click on it adds // the item or takes it out. (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>${item.template}</span> </div> </div> `, )} </gui-multi-list> </div> </div> <p role="status">Selected: ${this.values.join(', ') || 'none'}</p> `; }}<div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" ></gui-multi-list> </div></div><p id="languages-status" role="status"></p>
<template id="language-item"> <div class="gui-list__item"> <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span></span> </div></template>
<script type="module"> const list = document.querySelector('#languages'); // Each item is a copy of the template: no HTML strings, so Trusted Types has nothing to check. const itemTemplate = document.querySelector('#language-item');
// The list draws no items: it reports the ones to render, and you add them as its children. list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
// One option per item, from its id and state. A click on it adds the item or takes it out. function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; option.append(itemTemplate.content.firstElementChild.cloneNode(true)); option.firstChild.classList.toggle('gui-list__item-selected', item.selected); option.firstChild.classList.toggle('gui-list__item-focused', item.focused); option.querySelector('span:last-child').textContent = item.template; return option; }
list.values = ['Catalan'];
const showStatus = () => { const values = list.values ?? []; document.querySelector('#languages-status').textContent = `Selected: ${values.join(', ') || 'none'}`; }; list.addEventListener('gui-change', showStatus); showStatus();
list.items = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese', ];</script>Dropdown options
Section titled “Dropdown options”A dropdown renders its options itself, from the same items: it builds each option’s id, role,
aria-selected and height. renderItem only returns what the option shows. It gets the item, with your item in item.template, and the option’s state:
dropdown.renderItem = (item, { html, selected, focused, disabled, index }) => html`<b>${item.template.symbol}</b> ${item.template.name}`;html is Lit’s template tag, passed in so that no framework needs to import Lit. It shows every
value in ${} as text, never as markup. The function can also return a string, which shows as
text, or a DOM node. This is the example from the
Dropdown page:
import { GuiDropdown } from '@golemui/gui-components/react';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
type Currency = { code: string; name: string; symbol: string };
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];
const searchFields = ['name', 'code'];
// The option's content: `html` is Lit's, and it shows every value as text.const renderCurrency: GuiItemRenderer<Currency> = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span>`;
const currencyStyles = ` #currency .gui-list__item-content { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }`;
export function DropdownRenderItem() { return ( <> <GuiDropdown id="currency" label="Currency" labelField="name" valueField="code" searchFields={searchFields} value="EUR" items={currencies} renderItem={renderCurrency} /> <style>{currencyStyles}</style> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation,} from '@angular/core';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type Currency = { code: string; name: string; symbol: string };
@Component({ selector: 'app-dropdown-render-item', schemas: [CUSTOM_ELEMENTS_SCHEMA], // The element renders the options itself: the styles must not be scoped. encapsulation: ViewEncapsulation.None, template: ` <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" [searchFields]="searchFields" value="EUR" [items]="currencies" [renderItem]="renderCurrency" ></gui-dropdown> `, styles: ` #currency .gui-list__item-content { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } `,})export class DropdownRenderItemComponent { searchFields = ['name', 'code']; currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' }, ];
// The option's content: `html` is Lit's, and it shows every value as text. renderCurrency: GuiItemRenderer<Currency> = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span> `;}<script setup lang="ts">import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
type Currency = { code: string; name: string; symbol: string };
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];const searchFields = ['name', 'code'];
// The option's content: `html` is Lit's, and it shows every value as text.const renderCurrency: GuiItemRenderer<Currency> = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span>`;</script>
<template> <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" value="EUR" :searchFields.prop="searchFields" :items.prop="currencies" :renderItem.prop="renderCurrency" ></gui-dropdown></template>
<style>#currency .gui-list__item-content { display: flex; align-items: center; gap: var(--gui-space-2);}
.currency__symbol { min-width: 2ch; font-weight: 600;}
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75;}</style>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
type Currency = { code: string; name: string; symbol: string };
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];
const searchFields = ['name', 'code'];
@customElement('dropdown-render-item')export class DropdownRenderItem extends LitElement { protected override createRenderRoot() { return this; }
// The option's content: `html` is Lit's, and it shows every value as text. private renderCurrency: GuiItemRenderer<Currency> = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span> `;
override render() { return html` <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" value="EUR" .searchFields=${searchFields} .items=${currencies} .renderItem=${this.renderCurrency} ></gui-dropdown>
<style> #currency .gui-list__item-content { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } </style> `; }}<gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" search-fields='["name", "code"]' value="EUR"></gui-dropdown>
<script type="module"> const dropdown = document.querySelector('#currency');
dropdown.items = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' }, ];
// The option's content: `html` is Lit's, and it shows every value as text. dropdown.renderItem = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span> `;</script>
<style> #currency .gui-list__item-content { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }</style>The dropdown calls renderItem again whenever an option’s state changes: when the user moves to
it, picks it or the search changes the list. Keep it fast and free of side effects.
Framework components in an option
Section titled “Framework components in an option”A template covers most options. When an option needs a component of your framework, for its
logic or because your design system already has one, render it into root, the element that
holds the option’s content, and return nothing. The dropdown then leaves root to you. Call
onCleanup with what destroys your component: the dropdown runs it when the option leaves the
list, because the user scrolled, the search hid it or the list closed. The list only keeps the
options in view, so without it every option the user scrolls past stays in memory.
The same option is reused while its item stays in view, so create the component once per root
and update it on the next calls. The React and Angular examples keep their components in a
WeakMap by root.
In this example, CurrencyOption is an ordinary component of each framework, with nothing in it
for the dropdown: it takes a currency and whether it’s selected, and shows a check mark when it
is. Each tab shows the component, then the field that renders it in the options:
export type Currency = { code: string; name: string; symbol: string };
type Props = { currency: Currency; selected: boolean };
export function CurrencyOption({ currency, selected }: Props) { return ( <span className="currency"> <span className="currency__symbol">{currency.symbol}</span> {currency.name} <span className="currency__code"> {selected && <span aria-hidden="true">✓ </span>} {currency.code} </span> </span> );}import { createRoot, type Root } from 'react-dom/client';import { GuiDropdown } from '@golemui/gui-components/react';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/index.css';import { CurrencyOption, type Currency } from './CurrencyOption';
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];
const searchFields = ['name', 'code'];
// Each option is a React root, created on the first call and updated on the// next ones. The dropdown runs onCleanup when the option leaves the list.const roots = new WeakMap<HTMLElement, Root>();
const renderCurrency: GuiItemRenderer<Currency> = ( item, { root, selected, onCleanup },) => { let reactRoot = roots.get(root); if (!reactRoot) { const created = createRoot(root); roots.set(root, created); onCleanup(() => { roots.delete(root); created.unmount(); }); reactRoot = created; } reactRoot.render( <CurrencyOption currency={item.template} selected={selected} />, );};
const currencyStyles = ` .currency { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }`;
export function CurrencyField() { return ( <> <GuiDropdown id="currency-field" label="Currency" labelField="name" valueField="code" searchFields={searchFields} value="EUR" items={currencies} renderItem={renderCurrency} /> <style>{currencyStyles}</style> </> );}import { Component, input } from '@angular/core';
export type Currency = { code: string; name: string; symbol: string };
@Component({ selector: 'app-currency-option', template: ` <span class="currency"> <span class="currency__symbol">{{ currency().symbol }}</span> {{ currency().name }} <span class="currency__code"> @if (selected()) { <span aria-hidden="true">✓ </span> } {{ currency().code }} </span> </span> `, styles: ` .currency { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } `,})export class CurrencyOptionComponent { currency = input.required<Currency>(); selected = input(false);}import { ApplicationRef, Component, ComponentRef, CUSTOM_ELEMENTS_SCHEMA, EnvironmentInjector, createComponent, inject,} from '@angular/core';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import { CurrencyOptionComponent, type Currency,} from './currency-option.component';
@Component({ selector: 'app-currency-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown id="currency-field" label="Currency" label-field="name" value-field="code" [searchFields]="searchFields" value="EUR" [items]="currencies" [renderItem]="renderCurrency" ></gui-dropdown> `,})export class CurrencyFieldComponent { private injector = inject(EnvironmentInjector); private appRef = inject(ApplicationRef); private refs = new WeakMap< HTMLElement, ComponentRef<CurrencyOptionComponent> >();
searchFields = ['name', 'code']; currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' }, ];
// Each option is a component, created on the first call and updated on the // next ones. The dropdown runs onCleanup when the option leaves the list. renderCurrency: GuiItemRenderer<Currency> = ( item, { root, selected, onCleanup }, ) => { let ref = this.refs.get(root); if (!ref) { const created = createComponent(CurrencyOptionComponent, { environmentInjector: this.injector, hostElement: root, }); this.appRef.attachView(created.hostView); this.refs.set(root, created); onCleanup(() => { this.refs.delete(root); created.destroy(); }); ref = created; } ref.setInput('currency', item.template); ref.setInput('selected', selected); };}<script lang="ts">export type Currency = { code: string; name: string; symbol: string };</script>
<script setup lang="ts">defineProps<{ currency: Currency; selected: boolean }>();</script>
<template> <span class="currency"> <span class="currency__symbol">{{ currency.symbol }}</span> {{ currency.name }} <span class="currency__code"> <span v-if="selected" aria-hidden="true">✓ </span> {{ currency.code }} </span> </span></template>
<style scoped>.currency { display: flex; align-items: center; gap: var(--gui-space-2);}
.currency__symbol { min-width: 2ch; font-weight: 600;}
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75;}</style><script setup lang="ts">import { h, render } from 'vue';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';import CurrencyOption, { type Currency } from './CurrencyOption.vue';
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];const searchFields = ['name', 'code'];
// Vue's render mounts the component in the option on the first call and// patches it on the next ones. The dropdown runs onCleanup when the option// leaves the list.const renderCurrency: GuiItemRenderer<Currency> = ( item, { root, selected, onCleanup },) => { render(h(CurrencyOption, { currency: item.template, selected }), root); onCleanup(() => render(null, root));};</script>
<template> <gui-dropdown id="currency-field" label="Currency" label-field="name" value-field="code" value="EUR" :searchFields.prop="searchFields" :items.prop="currencies" :renderItem.prop="renderCurrency" ></gui-dropdown></template>import { LitElement, css, html, nothing } from 'lit';import { customElement, property } from 'lit/decorators.js';
export type Currency = { code: string; name: string; symbol: string };
@customElement('currency-option')export class CurrencyOption extends LitElement { static override styles = css` :host { display: flex; align-items: center; gap: var(--gui-space-2); }
.symbol { min-width: 2ch; font-weight: 600; }
.code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } `;
@property({ attribute: false }) currency!: Currency; @property({ type: Boolean }) selected = false;
override render() { return html` <span class="symbol">${this.currency.symbol}</span> ${this.currency.name} <span class="code"> ${this.selected ? html`<span aria-hidden="true">✓ </span>` : nothing} ${this.currency.code} </span> `; }}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';import type { Currency } from './currency-option';import './currency-option';
const currencies: Currency[] = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' },];
const searchFields = ['name', 'code'];
@customElement('currency-field')export class CurrencyField extends LitElement { protected override createRenderRoot() { return this; }
// <currency-option> is a custom element, so a template renders it: Lit // creates it, updates it and removes it with the option. private renderCurrency: GuiItemRenderer<Currency> = ( item, { html, selected }, ) => html`<currency-option .currency=${item.template} ?selected=${selected} ></currency-option>`;
override render() { return html` <gui-dropdown id="currency-field" label="Currency" label-field="name" value-field="code" value="EUR" .searchFields=${searchFields} .items=${currencies} .renderItem=${this.renderCurrency} ></gui-dropdown> `; }}<gui-dropdown id="currency-field" label="Currency" label-field="name" value-field="code" search-fields='["name", "code"]' value="EUR"></gui-dropdown>
<script type="module"> const dropdown = document.querySelector('#currency-field');
dropdown.items = [ { code: 'AUD', name: 'Australian dollar', symbol: 'A$' }, { code: 'BRL', name: 'Brazilian real', symbol: 'R$' }, { code: 'CHF', name: 'Swiss franc', symbol: 'Fr' }, { code: 'EUR', name: 'Euro', symbol: '€' }, { code: 'GBP', name: 'Pound sterling', symbol: '£' }, { code: 'INR', name: 'Indian rupee', symbol: '₹' }, { code: 'JPY', name: 'Japanese yen', symbol: '¥' }, { code: 'USD', name: 'US dollar', symbol: '$' }, ];
// Plain JavaScript's take on the component: a function that builds an option's content. function currencyOption(currency, selected) { const symbol = document.createElement('span'); symbol.className = 'currency__symbol'; symbol.textContent = currency.symbol;
const code = document.createElement('span'); code.className = 'currency__code'; code.textContent = currency.code; if (selected) { const check = document.createElement('span'); check.setAttribute('aria-hidden', 'true'); check.textContent = '✓ '; code.prepend(check); }
const option = document.createElement('span'); option.className = 'currency'; option.append(symbol, currency.name, code); return option; }
// The dropdown puts the node in the option, and calls the function again when its state changes. dropdown.renderItem = (item, { selected }) => currencyOption(item.template, selected);</script>
<style> .currency { display: flex; align-items: center; gap: var(--gui-space-2); }
.currency__symbol { min-width: 2ch; font-weight: 600; }
.currency__code { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }</style>Framework notes
Section titled “Framework notes”- Vue: bind
itemsandvaluewith the.propmodifier, so Vue sets the properties instead of attributes. - Lit: set the option’s height with
styleMap, not astyleattribute, so the example keeps working under a strict Content Security Policy.