A robust component for selecting a single item from a list of options with suggestions as you type, keyboard navigation, and more.
A combo box is created by the factory function fun <T> combobox(). T is the data type of the choices to be offered,
such as a country.
When the input created via comboboxInput is focused, a dropdown with suggestions is shown and updated as you
type. When focused, the input shows the current input. Otherwise, the currently selected item is displayed.
It is mandatory to specify a data stream or a store of type T? as data binding via the value property. The component
supports two-way data binding, i.e. it reflects a selected element from the outside by a Flow<T>
but also emits the updated selection to the outside via a Handler.
A combo box may not hold a value (e.g. if initially there is no selection or the implementation lets the user
un-select his choice). Thus, the type parameter of the data-binding is nullable. It is possible to specify a
placeholder text via the vanilla placeholder attribute exposed by the comboboxInput's
input element.
Within the selection list the user can navigate using the keyboard. An item is selected via Enter, Space or a mouse click. If the combo box input loses focus or the user clicks outside the selection list, the dropdown is hidden.
As typical use cases may offer thousands of items to choose from, the component reduces and filters those in order to
support the visual recognition of a user down to a feasible size, which can be configured via maximumDisplayedItems.
As a result, the combo box constantly evaluates which items to show based on the user's query. The results are emitted
by the
results Flow available in the scope of the items brick.
The intended pattern is to iterate over the results and re-render all of them when the results change.
The selection dropdown is populated via the comboboxItems brick. Within the scope of this function, individual items
can be rendered via the comboboxItem brick.
Beware: Due to the inner workings of the combobox, rendering items via renderEach does not work and results in
undefined behavior!
Instead, apply an ordinary render on the results-flow and create the items via a forEach-call on the provided
List<T>.
// some domain type for this example, a collection to choose from, and an external store
val countries = listOf("Germany", "Netherlands", "France")
val country = storeOf<String?>(null)
combobox<String> {
items(countries)
// set up (two-way) data binding
value(country)
comboboxInput {}
comboboxItems("bg-white") {
// using a combination of 'render' and 'forEach' is the intended
// way of populating the dropdown
results.render { result ->
result.items.forEach { item ->
comboboxItem(item) {
span {
className(active.map { if (it) "underline" else "" })
+item.value
}
}
}
}
}
}
Do not forget to include a portalRoot()-call at the end of your initial RenderContext as explained
here!
A comboboxItem has two special states:
active.selected.In the scope of a comboboxItem, active and selected are available as a Flow<Boolean> to apply styling or to
render or hide certain elements based on the state:
comboboxItem(item) {
//change fore- und background-color is item is active
className(active.map {
if (it) "text-amber-900 bg-amber-100" else "text-gray-300"
})
span { +item.value }
//render checked-icon only if item is selected
selected.render {
if (it) {
svg { content(HeroIcons.check) }
}
}
}
When rendering items based on the user's query, it is often a requirement to highlight the passages of each
item's String representation that match the given query.
For this purpose, the current query is part of the results Flow.
comboboxItems {
results.render { (query, items, _) ->
items.forEach { item ->
comboboxItem(item) {
// separate the item's String representation into matching and
// non-matching segments
val segments = item.value.split(
Regex(
"(?<=($highlight))|(?=($highlight))",
RegexOption.IGNORE_CASE
)
)
for (s in segments) {
span(
joinClasses(
"underline".takeIf {
s.contentEquals(query, ignoreCase = true)
}
)
) { +s }
}
}
}
}
}
The results Flow only displays a fixed number of items at a time, configured via the maximumDisplayedItems property.
If more items match the given query than configured to be displayed, the truncated flag available in the results
Flow is set to true so an appropriate hint can be displayed.
comboboxItems {
results.render { (_, items, truncated) ->
items.forEach { item ->
comboboxItem(item) {
span { +item.value }
}
}
if (truncated) {
span {
+"Refine your query for more results"
}
}
}
}
The combo box can be supplemented with a label using comboboxLabel, e.g. for use in forms or for a screen reader:
combobox<String> {
comboboxLabel {
span { +"Select a country" }
}
}
Combobox is an OpenClose component. There are different Flows and Handlers
like opened available in its scope to control the open state of the combo box based on state changes.
Most of the time, the open state managed by the component itself should be enough for all use cases, though.
The combo box's dropdown can be configured to automatically open based on different events:
| Configuration method | Description |
|---|---|
lazily() |
The dropdown is opened when the user starts typing. |
eagerly() |
The dropdown is opened as soon as the user focuses the combo box's input element |
The configuration is done via the openDropdown hook available in the configuration scope.
Both strategies are good choices and mostly depend on subjective preferences. By default, the dropdown opens itself eagerly.
Example:
combobox {
openDropdown.lazily()
// OR
openDropdown.eagerly()
}
By default, the user needs to manually select an item for it to be accepted as the combo box's value.
It can, however, also be configured to automatically select matching items for a query via the selectionStrategy
property.
The following modes are offered:
| Configuration method | Description |
|---|---|
autoSelectMatch() |
Matching items are automatically selected |
manual() |
Matching items need to be selected manually |
combobox {
selectionStrategy.autoSelectMatch()
// OR
selectionStrategy.manual()
}
Showing and hiding the selection list can be easily animated with the help of transition:
comboboxItems {
transition(
opened,
enter = "transition duration-100 ease-out",
enterStart = "opacity-0 scale-95",
enterEnd = "opacity-100 scale-100",
leave = "transition duration-100 ease-in",
leaveStart = "opacity-100 scale-100",
leaveEnde = "opacity-0 scale-95"
)
}
comboboxItems is a PopUpPanel and therefore provides a set of
configuration options to control the position or distance of the list box from the comboboxButton
as a reference element:
comboboxItems {
placement = PlacementValues.top
addMiddleware(offset(20))
}
In practice, the comboboxInput element might be wrapped in additional elements that are considered
to be a part
of it. Since by default the dropdown is positioned relative to the comboboxInput element, the
dropdown may
appear out of place. In those cases, the outermost wrapping element can be created via the
comboboxPanelReference brick to fix the positioning.
In order to see an example of the comboboxPanelReference in action, have a look at the source code of the combobox
demo
within the headless-demo module. You can observe the consequences of removing and re-adding it there.
The anchor element of the dropdown is determined based on a number of conditions:
comboboxInput is present, the panel is placed relative to it.comboboxPanelReference is present, it is used as the reference instead.The data-binding allows the Combobox component to process the validation messages and provide its own building
block lcomboboxValidationMessages that can beused to render the messages if present. The messages are exposed within
its scope as a msgs Flow.
combobox<String> {
value(country)
comboboxValidationMessages(tag = RenderContext::ul) {
msgs.renderEach {
li { +it.message }
}
}
}
When the comboboxInput element is focused, the selection list (dropdown) is visible and can be used to select
items. The input remains focused until a selection is made, even if the user navigates to an item via keyboard.
When an item is active and Enter is pressed, it will be selected and the dropdown is closed.
When the input elements loses focus, the dropdown will be closed as well and the displayed value is reset to match the
last selection.
A click on the comboboxInput focuses the element and opens the selection dropdown as described above. A click outside
the opened selection list closes it. If the mouse is moved over an item in the open list, it is marked as active.
Clicking on an item when the list is open selects it and closes the list.
| Command | Description |
|---|---|
| ⬆ ⬇ when the combobox is open | Activates previous / next item |
| Home End when the combobox is open | Activates first / last item |
| Esc when the combobox is open | Closes the combobox |
| Enter Space when the combobox is open | Selects the active item |
The combo box component uses a specific internal pipeline to filter and display the selection items:
The debouncing is in place because the above workflow consists of two relatively expensive operations: filtering and rendering.
While typing, the query may be manipulated multiple times per second. In order for the filter function to run as few times as possible, the flow of inputs is debounced.
The same goes for the actual rendering: It is by far the most expensive operation in the workflow so it is debounced to not be executed multiple times in a row.
Adding to the above, the number of displayed items in the dropdown also has an impact on the rendering performance.
Most of the time, the default behavior should be working fine. There might be cases, however, where the implementing component has a consistently high/low amount of items or other niche scenarios. In those cases, the debouncing and other performance-related parameters can be configured via the DSL.
| Property | Type | Default | Description |
|---|---|---|---|
maximumDisplayedItems |
Int |
20 |
Maximum number of items to display |
inputDebounceMillis |
Long |
50L |
Time to wait and debounce before the filter function is invoked |
renderDebounceMillis |
Long |
50L |
Time to wait and debounce before the filter results are rendered |
See combobox for more api information.
combobox<T> {
val items: ItemsHook()
// params: List<T> / Flow<List<T>>
var itemFormat: (T) -> String
val value: DatabindingProperty<T?>
var filterBy: FilterFunctionProperty
// params: (Sequence<T>, String) -> Sequence<T> / T.() -> String
val openDropdown: DropdownOpeningHook
// methods: lazily() / eagerly()
val selectionStrategy: SelectionStrategyProperty
// methods: autoSelectMatch() / manual()
var maximumDisplayedItems: Int = 20
var inputDebounceMillis: Long = 50L
var renderDebounceMillis: Long = 50L
comboboxInput() { }
comboboxPanelReference() {
// this brick is often used with a nested
// comboboxInput() { }
}
comboboxLabel() { }
comboboxItems() {
// inherited by `PopUpPanel`
var placement: Placement
var strategy: Strategy
var flip: Boolean
var skidding: Int
var distance: int
val results: Flow<QueryResult.ItemList<T>>
// results.render {
// for each QueryResult.ItemList<T>.Item<T> {
comboboxItem(Item<T>) { }
// }
// }
}
comboboxValidationMessages() {
val msgs: Flow<List<ComponentValidationMessage>>
}
}
Parameters: classes, id, scope, tag, initialize
Default-Tag: div
| Scope property | Type | Description |
|---|---|---|
items |
Combobox<T>.ItemsHook |
Mandatory List<T> or Flow<List<T>> of items to offer (invoke) |
itemFormat |
(T) -> String |
Recommended formatting function used to display an item's String representation in the comboboxInput. |
value |
DatabindingProperty<T> |
Mandatory (tow-way) data binding for a selected item. |
filterBy |
FilterFunctionProperty |
Recommended filter function to find matching items based on the query. Accepts either a String getter (T.() -> String) or a fully custom filter function ((Sequence<T>, String) -> Sequence<T>). Mandatory for non-String items! |
openDropdown |
DropdownOpeningHook |
Optional strategy to configure when the combo box's dropdown should open (lazily or eagerly) |
selectionStrategy |
SelectionStrategyProperty |
Optional strategy to configure whether exact matches are automatically selected. Invoke either autoSelectMatch() or manual() |
maximumDisplayedItems |
Int |
Maxmimum number of items to display in the dropdown. Defaults to 20 |
inputDebounceMillis |
Long |
Time to wait and debounce before the filter function is invoked. Defaults to 50 milliseconds. |
renderDebounceMillis |
Long |
Time to wait and debounce before the filter results are rendered. Defaults to 50 milliseconds. |
openState |
DatabindingProperty<Boolean> |
Optional (two-way) data binding for opening and closing. |
opened |
Flow<Boolean> |
Data stream that provides Boolean values related to the "open" state. Quite useless within a list box, as it is always true |
close |
SimpleHandler<Unit> |
Handler to close the list box from inside. Should not be used, as the component handles this internally. |
open |
SimpleHandler<Unit> |
handler to open; does not make sense to use within a list box! |
toggle |
SimpleHandler<Unit> |
handler for switching between open and closed; does not make sense to use within a list box. |
Available in the scope of: combobox
Parameters: classes, scope, tag, initialize
Default-Tag: div
Available in the scope of: combobox, comboboxPanelReference
Parameters: classes, scope, initialize
Available in the scope of: combobox, comboboxPanelReference
Parameters: classes, scope, tag, initialize
Default-Tag: label
Available in the scope of: combobox, comboboxPanelReference
Parameters: classes, scope, tag, initialize
Default-Tag: div
| Scope property | Typ | Description |
|---|---|---|
msgs |
Flow<List<ComponentValidationMessage>> |
provides a data stream with a list of ComponentValidationMessages |
Available in the scope of: combobox
Parameters: classes, scope, tag, initialize
Default-Tag: div
| Scope property | Typ | Description |
|---|---|---|
results |
Flow<QueryResult.ItemList<T>> |
Emits the current list of items to be displayed in the selection dropdown |
Available in the scope of: comboboxItems
Parameters: item, classes, scope, tag, initialize
Default-Tag: button
| Scope property | Typ | Description |
|---|---|---|
selected |
Flow<Boolean> |
This data stream provides the selection state of the managed option: true the option is selected, false if not. |
active |
Flow<Boolean> |
This data stream indicates whether an item has focus: true the option has focus, false if not. Only one option can have focus at a time. |