@lekoala/combobox

One engine enhances free-form input+datalist and constrained select controls, wrapped declaratively in a <combo-box> custom element. The imperative surface needs no global: every example here is configured through the element itself (options/configure()) and its public .combobox engine. The original source always owns form values.

Auto mode · Force basic fallback · Actual CSS skin · Bootstrap skin · Query builder · Service options · Directory picker · Place picker · Position modes

This page always loads the generated classic build (../dist/combobox.js), so it validates the distributed product and works identically over http(s) and file://. Run bun run sync before opening it. The source ESM is exercised directly by the unit and browser test suites.

1. Free text autocomplete

Native source: input + datalist. Enhanced mode detaches the datalist so the UA picker never competes with the custom picker. search="includes" selects an accent-insensitive substring match.

The browser's own autofill is not the component's to control: this field is a genuine address field (name="city", autocomplete="address-level2"), so Chrome may still lay its saved addresses over the picker. Avoid enhancing fields a browser knows how to fill — name, address, phone, payment — or declare a token from an unrelated section, such as autocomplete="new-password", on them.

2. Filterable single select

The filter input is a separate unnamed interaction control. The select remains the constrained value owner. “Add external doctor” simulates a modal/API creating an entity and selecting it programmatically via .combobox.select().

3. Multiple select with chips

The native select multiple owns selected values. Chips are a projection only. Use Arrow Left from an empty search to enter chip keyboard navigation; Delete/Backspace removes the focused chip.

4. Creatable multiple select

Creation is validated separately from creation itself. This demo rejects tags of 2 characters or fewer and purely numeric tags. Basic fallback keeps the native select plus a small unnamed Add input.

5. Explicit selection order

Catalogue order, result order and selection order are distinct. This instance preserves selection order without physically moving source options. Focus a chip and press Alt+Arrow/Alt+Home/Alt+End to reorder; move() is the model operation; drag-and-drop is not core.

No selection

6. Remote loading contract

File-friendly async mock. Remote results are transient and do not fill the native select; only a selected result becomes a native option. The loader receives { signal, cursor, source, input, combobox }. A failed load shows a .cb-error row (overridable via render.error), never touches the selection, and is cleared by the next search.

7. Dependent-field loader

The loader reads the sibling field live from its own context. shouldLoad vetoes the request while the country is empty, and switching countries refreshes results (stale results never linger).

8. Declarative <combo-box> custom element

The wrapper owns nothing: the child select stays the form-value owner and attributes map to engine options. Registration is centralized — importing the engine never registers anything (src/define.js does). This page charges that entry via the generated dist/combobox.js bundle.

9. JS options through the element

JavaScript-only behavior such as load() is passed through configure() on the element. The official tag is fixed; an application-specific name is a native subclass of the exported ComboBoxElement and is covered by tests and documentation.

10. RTL

dir="rtl" on the wrapper: chips, remove button and selected markers mirror naturally. Logical properties (padding-inline, inline-size) keep the layout correct.

11. Async guards (confirm add/remove/clear)

guards may be sync or async. false is a voluntary refusal and mutates nothing; a rejected promise is an application error surfaced as combobox:guarderror. A cancelled confirmation resolves false, it does not reject. This widget also shows separators + create-on-blur.

Waiting for actions…

12. maxItems never mutilates server-rendered selection

The server shipped three selected options with max-items="2". All three stay selected and removable; the cap only blocks future additions (important for server-rendered content and form reset).

13. Disabled items at runtime

A selected-then-disabled option stays as a chip without a remove button. Toggling disabled and calling .combobox.refresh() re-renders.

14. maxOptions is a rendering cap only

Six catalogue options but max-options="3": at most three options ever render or take keyboard navigation. The native catalogue stays intact.

15. Rich renderer returns DOM Nodes

Labels are text by default. A renderer may return a DOM Node (here a ★ badge for favorites). Hostile markup in option data renders as text, never HTML.

16. Clear affordance is application-authored

The core never auto-injects a clear button. The application places one and calls .combobox.clear(), with a cancellable combobox:beforeclear.

17. labelField/valueField + searchFields

label-field/value-field map data objects to canonical { value, label }. Search matches any field in search-fields (config) and the renderer shows extra columns.

18. Declarative fuzzy search

search="fuzzy" enables lightweight order-preserving subsequence matching (type ghar for George Harrison); search-fields="label,email" also searches the <option data-*> metadata; tab-select commits the active row with Tab. This is pure declarative configuration — no JavaScript.

19. Form reset restores initial state

Change selections below (add/remove chips, pick a different team, clear a filter), then press Reset demo values. Every widget returns to the exact server-rendered initial state — the button resets the whole demo form, native source included, and the engine re-syncs from it.

20. GitHub-style topics

Chips + toggle-selected + autoselect-first: typing highlights the first match, Enter/click selects it, and Enter/click on a checked row deselects it again. Unknown text shows a single “Press Enter to add as new topic” row (only when nothing matches). No checkbox styling needed — the checked state is the existing aria-selected row.

21. Disabled optgroup and locked selections

A disabled <optgroup> disables every option inside: rows render with aria-disabled and cannot be selected. A server-rendered selection inside it stays as a locked chip — no remove button, clear() spares it, and remove() resolves false. Enabling the group and calling .combobox.refresh() unlocks it at runtime.

Bonus: “Starter” exists in both groups under the same value="starter". Option identity is the HTMLOptionElement, so the two chips stay distinct — remove("starter") clears the removable one only.

Legacy 2019 and Starter (2019 legacy) are locked chips.

Event / extension log

The architecture exposes cancellable “before” events, including the Open UI-inspired beforefilter with event.query.

Waiting for events…