← Back to Examples

🚀 Data & API

Custom objects, tuple arrays, member/getter callbacks, async search, form integration (json/csv/array), public API methods, and cascading selects.

🌐 client — runs in the browser (element JS API) 🖥️ server — LiveView round-trip

DA01 · Custom Object Structure

value_member="userId" · display_value_member="fullName" · subtitle_member="email"

Use your own data structure! Specify which properties to use for value, display, and other fields using value_member and display_value_member.

DA02 · [key, value] Tuple Arrays

Just pass tuples to options={@languages} — no value_member/display_value_member needed

Auto-detected! Simply pass an array of [key, value] tuples. Perfect for simple key-value pairs without creating objects.

DA03 · Callbacks for Complex Logic

Use callbacks when you need complex logic to extract values. Callbacks take precedence over members.

getValueCallback, getDisplayValueCallback, getSubtitleCallback and getDisabledCallback can't be serialized as HTML attributes — they're assigned to the element by id in the inline script below.

API01 · Form Integration — JSON Format

name="skills" + value_format="json" writes a hidden input the form submits

Seamless HTML form integration! Hidden inputs are automatically created and updated. Choose from 3 formats: json, csv, or array.

API02 · Form Integration — CSV Format

value_format="csv" submits a single comma-separated hidden input

Use CSV format for traditional comma-separated values. Great for legacy systems.

API03 · Form Integration — Array Format

value_format="array" emits one hidden input per value as tags[]

Use array format to create multiple hidden inputs. Standard HTML array handling with name[].

Captured params

(submit the form to see captured params)

API04 · Public API Methods

multiple={false} switches single-select mode (returns a scalar); omit it for multi-select

New properties and methods for easier value access:

  • getValue() - Returns single value or array depending on mode
  • selectedValue - Get the selected value(s)
  • selectedItem - Get the first selected item object
  • getSelected() - Get all selected item objects
Result:
Change the selection, then click a method above.
Result:
Change the selection, then click a method above.

API07 · Open / Close API

Drive the dropdown open state from your own code — wizards, "open on step", or custom trigger buttons. Calling open() from a button's own click handler works directly: the component ignores that opening click so it won't immediately re-close.

  • open() - Open the dropdown
  • close() - Close the dropdown
  • toggle() - Toggle the dropdown open/closed
  • isOpen - Boolean property; reading returns the state, assigning true/false opens/closes it
isOpen:
false
const multiselect = document.querySelector('web-multiselect');

multiselect.open();      // Open the dropdown
multiselect.close();     // Close the dropdown
multiselect.toggle();    // Toggle open/closed

multiselect.isOpen;          // → true / false
multiselect.isOpen = true;   // open by assignment
multiselect.isOpen = false;  // close by assignment

API08 · Search text — read & drive the query

Read the current search box text with searchText, set it programmatically with search(term) — it applies exactly as if the user typed (runs beforeSearchCallback / minSearchLength / async searchCallback) and does not open the dropdown — and reset it with clearSearch(). There's no separate "filter" method: what the query does is governed by searchMode — set el.searchMode = 'filter' (narrow the list) or 'navigate' (keep all options, jump focus to the match) before searching. Handy for deep-links ("open with a query pre-filled"), syncing an external field, or restoring saved state.

  • searchText - read the current query (empty string when nothing is typed)
  • search(term) - set the query and apply it (does not open the dropdown)
  • searchMode - 'filter' | 'navigate'; governs what the query does
  • clearSearch() - clear the query and restore the full list

Try an in each mode: filter narrows to Banana / Mango / Orange; navigate keeps every fruit visible and just focuses the first match.

searchText:
""
const el = document.querySelector('web-multiselect');

el.searchMode = 'filter';  // or 'navigate' — governs how the query behaves
el.search('an');           // apply (as if typed); flush() applies the mode first
el.open();                 // …call open() to see the effect
el.searchText;             // → "an"
el.clearSearch();          // → ""

API09 · Deferred initialization — defer / ready()

defer attribute holds the first render · release with el.ready() or by removing the attribute

With defer={true} the element builds nothing on upgrade — it only reserves space. Wire options, callbacks (customStylesCallback) and listeners first, then call el.ready() to build once, flash-free. This closes the classic upgrade-then-restyle flash where a component paints with its default styles for a beat before a post-upgrade callback (or a framework's shared-stylesheet adoption) restyles it — the styled result appears in one shot.

  • defer — boolean attribute; present = hold the first render
  • ready() — release the gate and build once (latched — never re-closes)
  • Removing the defer attribute also releases it — this is how the wrapper's shared-styles registry does it, server-driven, with no JS hook
  • isReady — reflected as the is-ready attribute (CSS hook :host([defer]:not([is-ready])) reserves space while held)
  • ready — event dispatched once, right after the first build
state:
held — waiting for data…
How LiveView releases it (and why you rarely call ready() yourself)
When :shadow_styles is configured the wrapper adds defer to every <web-multiselect> automatically, and its registry adopts the shared sheet then removes the attribute — themed badges paint flash-free with no per-instance JS. See the 💧 Elixir Only page. Reach for defer={true} + your own hook/ready() (as below) only when you need to hold the build for async wiring.
// <.web_multiselect id="defer-demo" defer={true} value={["js", "ts"]} />
// `defer` holds the build, so the element paints NOTHING until ready().
const el = document.querySelector('#defer-demo');

// Wire everything WHILE HELD — nothing renders yet:
el.customStylesCallback = () => `.ms__badge { background: #6d28d9; color: #fff; }`;
el.addEventListener('ready', () => console.log('built flash-free', el.isReady));

const langs = await loadLanguages();  // e.g. a 2 s fetch
el.options = langs;                   // assign options…
el.ready();                           // …then release: builds ONCE, styled, in one shot

// In LiveView you'd wire this from a phx-hook's mounted() and call el.ready() after your
// async options land. For the shared-theme case you need NONE of this — configure
// :shadow_styles and the wrapper defers + releases every select for you (see Elixir Only).

DA04 · Async Search / Lookup

min_search_length={2} · search_debounce={300} · icon_member="flag"

Load options dynamically as users type. Perfect for large datasets, API searches, or real-time filtering.

searchCallback receives an AbortSignal as its 2nd argument. Forward it to fetch so a superseded keystroke cancels the in-flight request. Pair with search_debounce to also coalesce keystroke bursts. These callbacks are assigned to each element by id in the inline script below.

User Search (GitHub API with Fallback)

Product Search (Simulated API)

1500 ms

Crank the delay up, then type quickly — each keystroke fires a request and cancels the previous in-flight one (watch the log below).

Debounced Search (search-debounce)

300 ms

The API call fires only after you pause typing for the debounce window — so a fast burst of keystrokes collapses into a single request. Set it to 0 to fire on every keystroke.

Keystrokes: 0 API calls: 0

Country Lookup with Icons

DA05 · Cascading Selects (Reactive Options)

Create dependent dropdowns that automatically update! Setting element.options = newArray triggers automatic re-rendering. Perfect for Organization → Business Unit → Department flows. This card mirrors upstream's pure-JS reactive cascade; for the server-driven (LiveView) variant see EO03 on the Elixir Only page.

Selected Path:
{
  "organization": null,
  "businessUnit": null,
  "department": null
}

💬 Badge Tooltips

enable_badge_tooltips={true} · badge_tooltip_placement="top"

Show informative tooltips when hovering over badges and remove buttons. Perfect for truncated text or additional context.

Basic Tooltips

Custom Tooltip Callback

Default content: display value + subtitle. Custom content: use getBadgeTooltipCallback (assigned in the inline script) for rich content. Remove-button tooltips show "Remove [item name]"; placement is configurable (top, bottom, left, right).

🎨 Custom Badge Display

Show different text in badges vs dropdown options. This example shows full details in dropdown ("John Doe - Senior Developer") with subtitle showing email, but just first names in badges ("John") for maximum space efficiency!

getDisplayValueCallback drives the dropdown text while getBadgeDisplayCallback drives the compact badge text — both assigned in the inline script. It falls back to the standard display value when getBadgeDisplayCallback isn't provided.

🎛️ Custom Action Buttons

Full control over dropdown action buttons! Add Select All, Clear All, and custom actions with dynamic visibility, tooltips, and custom handlers.

Built-in 'select-all' / 'clear-all' plus custom actions with onClick, dynamic visibility via getIsVisibleCallback, tooltips, and CSS classes. The actionButtons array is assigned in the inline script below.

✨ Benefits Summary

Flexible Data Handling

  • Works with any data structure
  • No need to transform your data
  • Auto-detects tuple arrays
  • Member or callback patterns
  • Full TypeScript support

Form Integration

  • Automatic hidden input creation
  • 3 format options (JSON, CSV, array)
  • Custom formatters via callback
  • Auto-updates on changes
  • Standard HTML form behavior

API05 · Event Handling

DOM events (no attribute): el.addEventListener("select" | "deselect" | "change", …) — see the inline <script>

Listen to select, deselect, and change events

Open browser console to see events
Event log will appear here...
Looking for the on* property handlers and the beforeSelect/beforeDeselect interceptors? See Events, Handlers & Interceptors.

API06 · Synchronous options + setSelected

Assign .options and call setSelected(...) on the very next line — no await. The property write coalesces onto a microtask, but the imperative call flushes it, so the pre-set selection renders immediately. Shown for the partial and compact badge modes (the compact composite badge is what previously mis-rendered when the two calls raced).

const el = document.querySelector('#api06-compact');
el.options = languages;          // reactive write (coalesces on a microtask)
el.setSelected(['js', 'ts', 'py']); // imperative call flushes it synchronously
🔌 Server round-trip
0
The LiveView server saw 0 event(s) from the wrapper.
(pick an option anywhere on this page)