Filterable List Components

Four components for selecting from lists. All share the same option/group model and keyboard navigation.

1. Select

A dropdown picker. Tab to focus, Enter to open, arrows to navigate.

melker --stdout examples/lists/select.melker
 Order a drink

 Size:   Choose size...   v 

 Drink:   Choose drink...  ^ 
         ┌──────────────────┐
 Order: (Coffee            
         Espresso          
         Latte             
         Cappuccino        
         Tea               
         Green Tea         
         Black Tea         
         Chai Latte        
         └──────────────────┘

View source

Options are declared as child elements. Use <group> to organize them under headers. The onChange handler fires with event.value and event.label.

select.melker
<select id="drink" placeholder="Choose drink..."
    style="width: 20"
    onChange="$app.update()">
    <group label="Coffee">
        <option value="espresso">Espresso</option>
        <option value="latte">Latte</option>
        <option value="cappuccino">Cappuccino</option>
    </group>
    <group label="Tea">
        <option value="green">Green Tea</option>
        <option value="black">Black Tea</option>
    </group>
</select>

<script type="typescript">
    export function update() {
        const drink = $melker.getElementById('drink');
        const result = $melker.getElementById('result');
        result.setValue(`Order: ${drink?.getValue()}`);
    }
</script>

Reference: Filterable list architecture

2. Combobox

A select with a text input. Type to filter the options list.

melker --stdout examples/lists/combobox.melker
 Country Picker

 Country:   ype to search...            ^
           ┌────────────────────────────┐
 Selected: Europe                     
           Sweden                     
           Norway                     
           Denmark                    
           Finland                    ░│
           Germany                    ░│
           France                     ░│
           Americas                   ░│
           └────────────────────────────┘

View source

The filter prop controls how typed text matches options. Set filter="fuzzy" for character-by-character matching (typing "sw" matches "Sweden"), or "prefix", "contains", "exact". The onSelect handler fires when an option is picked.

combobox.melker
<combobox id="country" placeholder="Type to search..."
    style="width: 30" filter="fuzzy"
    onSelect="$app.pick(event)">
    <group label="Europe">
        <option value="se">Sweden</option>
        <option value="no">Norway</option>
        <option value="dk">Denmark</option>
    </group>
    <group label="Americas">
        <option value="us">United States</option>
        <option value="ca">Canada</option>
    </group>
</combobox>

Combobox also accepts freeform text entry. If the user types something that does not match any option and presses Enter, onSelect fires with event.freeform = true.

Reference: Filterable list architecture

3. Autocomplete

A combobox with async search. Results load as you type.

melker --stdout examples/lists/autocomplete.melker
 User Search

 Find:   earch users...                   ^
        ┌─────────────────────────────────┐
 SelecteRecent                           
        Alice Anderson                   
        Bob Brown                        
        └─────────────────────────────────┘

View source

The onSearch handler runs when the user types. It receives event.query and should return an array of { value, label } objects. The debounce prop (in ms) prevents firing on every keystroke. minChars sets the minimum input length before searching.

autocomplete.melker
<autocomplete id="search" placeholder="Search users..."
    onSearch="$app.searchUsers(event.query)"
    onSelect="$app.selectUser(event)"
    minChars="1" debounce="300" width="35">
    <group label="Recent">
        <option value="alice">Alice Anderson</option>
        <option value="bob">Bob Brown</option>
    </group>
</autocomplete>

<script type="typescript">
    export async function searchUsers(query: string) {
        const res = await fetch(new URL('users.json', $melker.url));
        const users = await res.json();
        return users.filter(u =>
            u.label.toLowerCase().includes(query.toLowerCase())
        );
    }
</script>

Child <option> elements show as initial suggestions before the user types. Once onSearch returns results, they replace the dropdown contents.

Reference: Filterable list architecture

4. Command Palette

A modal overlay for commands. Fuzzy search, keyboard shortcuts, grouped actions.

melker --stdout examples/lists/command-palette.melker
 Command Palette
      ┌───────────────Command Palette────────────────┐
 Press│Search commands...                            
      ├──────────────────────────────────────────────┤
File                                          
 New File                              Ctrl+N 
 Ready│ Open File                             Ctrl+O 
 Save                                  Ctrl+S 
Edit                                          
 Undo                                  Ctrl+Z 
 Redo                                  Ctrl+Y 
View                                          
 Toggle Sidebar                               
 Zoom In                               Ctrl++ 
                                              
      └──────────────────────────────────────────────┘

View source

The palette starts closed. Open it by setting props.open = true or binding it to a keyboard shortcut. Each option can have a shortcut prop that displays right-aligned, and its own onSelect handler.

command-palette.melker
<command-palette id="palette" placeholder="Search commands...">
    <group label="File">
        <option value="new" shortcut="Ctrl+N"
            onSelect="$app.run('New file')">New File</option>
        <option value="save" shortcut="Ctrl+S"
            onSelect="$app.run('Saved')">Save</option>
    </group>
    <group label="View">
        <option value="sidebar"
            onSelect="$app.run('Toggled')">Toggle Sidebar</option>
    </group>
</command-palette>

<script type="typescript">
    export function openPalette() {
        const p = $melker.getElementById('palette');
        if (p) p.props.open = true;
    }
</script>

The palette includes built-in system commands (Exit, AI Assistant, Dev Tools) by default. Set system="false" to disable them.

Reference: Filterable list architecture

5. Options and groups

All four components use the same child elements.

Child elements
<!-- Simple option -->
<option value="id">Display Label</option>

<!-- Option with shortcut and disabled state -->
<option value="paste" shortcut="Ctrl+V" disabled="true">Paste</option>

<!-- Grouped options -->
<group label="Category">
    <option value="a">Item A</option>
    <option value="b">Item B</option>
</group>

Options can also be set programmatically with setOptions():

Dynamic options
const el = $melker.getElementById('mySelect');
el.setOptions([
    { id: 'a', label: 'Alpha', group: 'Greek' },
    { id: 'b', label: 'Beta', group: 'Greek' },
    { id: '1', label: 'One', group: 'Numbers' },
]);

Both child elements and setOptions() can be used together. Child options appear first, then programmatic ones.

Reference: Component reference

6. Filter modes

Four built-in filter algorithms. Set via the filter prop.

Filter modes
<!-- Fuzzy: characters in order, scores consecutive matches -->
<combobox filter="fuzzy" .../>     <!-- "sw" matches "Sweden" -->

<!-- Prefix: text starts with pattern -->
<combobox filter="prefix" .../>    <!-- "sw" matches "Sweden" but not "New Sweden" -->

<!-- Contains: text includes pattern -->
<combobox filter="contains" .../>  <!-- "eden" matches "Sweden" -->

<!-- Exact: text equals pattern -->
<combobox filter="exact" .../>    <!-- only "Sweden" matches "Sweden" -->

The default filter mode is fuzzy for combobox and command palette. Select does not filter (it has no text input). Autocomplete delegates filtering to the onSearch handler.

Reference: Filterable list architecture