JavaScript Dropdown

@lemonadejs/dropdown · ✓ 41 contract checks · framework-agnostic · zero dependencies

<Dropdown /> — select, autocomplete and picker in one block, built ON the Modal primitive (the panel) with the datagrid’s fixed-rowheight virtualization (large option lists stay a window of DOM).

v5 parity, the full nuance set:

  • items { value, text, group, image, keywords, synonym, disabled }; strings/numbers normalize to {value,text}; {id,name} too
  • groups: sorted together, header rows injected
  • single or multiple (Done/Reset controls); value as array or ‘a;b’ string via divisor; allowempty=false pins a selection
  • autocomplete: the closed label swaps to a contenteditable search field while open; filters text/group/keywords/synonym; selected items always remain listed; FORCED on by insert/remote/searchbar
  • remote: url?q= fetch with 300ms debounce, onbeforesearch veto, results merge behind the selected items; initial url load
  • insert: + button adds the typed text (async onbeforeinsert can replace or cancel; oninsert after)
  • keyboard: closed Enter/arrows open; open: arrows wrap, Home/End, Enter selects, Space selects (no autocomplete), Escape CANCELS (restores the previous value) — closing any other way COMMITS
  • types: default (anchored panel) | picker | searchbar (sheet modes) | inline (no modal, list always visible) | auto (by viewport width at open)

Example

live
import { html } from 'lemonadejs';
import Dropdown from '@lemonadejs/dropdown';

const cities = [
    { value: 'lon', text: 'London', group: 'Europe' },
    { value: 'par', text: 'Paris', group: 'Europe' },
    { value: 'ber', text: 'Berlin', group: 'Europe' },
    { value: 'nyc', text: 'New York', group: 'Americas' },
    { value: 'sao', text: 'Sao Paulo', group: 'Americas' },
    { value: 'tok', text: 'Tokyo', group: 'Asia' },
    { value: 'sin', text: 'Singapore', group: 'Asia' },
];

const App = (props, { state }) => {
    const office = state('lon');
    const visits = state(['par', 'tok']);

    return html`<div style="display:grid;gap:12px;max-width:320px">
        <${Dropdown} data="${cities}" bind="${office}" placeholder="Home office" />
        <${Dropdown} data="${cities}" bind="${visits}" multiple autocomplete placeholder="Offices to visit" />
        <p>Based in <b>${() => (cities.find((c) => c.value === office.value) || {}).text || 'nowhere'}</b>,
            visiting <b>${() => visits.value.length}</b> other offices.</p>
    </div>`;
};

Installation

npm install @lemonadejs/dropdown
import Dropdown from '@lemonadejs/dropdown';
import '@lemonadejs/dropdown/style.css';

Three deployment forms, one component:

html`<${Dropdown} />`                       // by value (no registration)
setComponents({ Dropdown });               // then <Dropdown /> by name anywhere
createWebComponent(Dropdown);              // <lm-dropdown> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
bindstringTwo-way bound value. .set() fires onchange; plain assignment is silent. value: single, array (multiple) or ‘a;b’ string
dataarrayDropdownItem[] (or strings/numbers/{id,name})
multiplebooleanfalse
autocompletebooleanfalse
remotebooleanfalsesearch against url?q= instead of locally
urlstring''
insertbooleanfalse+ button adds the typed text
typestring''
placeholderstring''
aria-labelstring''
widthnumber0
heightnumber300panel viewport height
rowheightnumber28
divisorstring";"string-value separator (multiple)
allowemptybooleantruefalse: the last selection cannot be removed
disabledbooleanfalse

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onchange — (value)
  • onopen
  • onclose — (origin)
  • onsearch — (results) after a remote search
  • onbeforesearch — (query, http) -> false cancels
  • oninsert — (item)
  • onbeforeinsert — async (item) -> item | false
  • onload — data ready (incl. initial url load)

API

import { ref } from 'lemonadejs';
const dropdown = ref();
html`<${Dropdown} ref="${dropdown}" />`;
// dropdown.current.open(...)  ·  dropdown.current.close(...)  ·  dropdown.current.toggle(...)  ·  dropdown.current.isClosed(...)  ·  dropdown.current.getValue(...)  ·  dropdown.current.setValue(...)  ·  dropdown.current.getText(...)  ·  dropdown.current.getData(...)  ·  dropdown.current.setData(...)  ·  dropdown.current.add(...)  ·  dropdown.current.reset(...)
  • open()
  • close()
  • toggle()
  • isClosed()
  • getValue()
  • setValue()
  • getText()
  • getData()
  • setData()
  • add()
  • reset()

Styling

All classes follow the lm-dropdown-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/dropdown/contract.json';

verify.json carries the conformance proof produced by verify(Dropdown).

Looking for the v5 plugin? See the archived v5 documentation.