[1.0.0-rc.2] - 2026-07-02
Documentation completeness pass over the component's attribute table.
Changed
- Every
web_multiselect/1attribute now carries adoc:description. rc.1 documented the 24 non-obvious attrs (hook,search_event, the option-tooltip family, the placeholder overrides, etc.) but left 46 — the "obvious from the name" behaviour / badge / search / member / virtual-scroll attrs — with no inline docs, so they rendered in the generated attributes table with an empty Description column. All 46 now have concise one-line descriptions (noting the relevant upstream default, e.g.multipledefaults totrue), plus a short "Attribute defaults" preamble in theweb_multiselect/1@docexplaining thenil-means-omit convention. No code or behaviour change — hexdocs only.
Fixed
- README install snippet pinned the wrong major. It still read
{:keen_web_multiselect, "~> 0.1"}from before the jump to1.0, which resolves to nothing now that only1.0.0-rc.*is published — a copy-pastemix deps.getwould fail. Corrected to~> 1.0, with a note on requiring~> 1.0.0-rcto opt into the current release candidate (a plain~> 1.0skips pre-releases).
[1.0.0-rc.1] - 2026-07-02 [PUBLISHED]
Tracks @keenmate/web-multiselect v1.12.0-rc05 (option hover tooltips, action-button positioning/alignment, smart built-in action defaults, beforeSelect/beforeDeselect veto interceptors, and the breaking *Callback→on* notification rename — on top of rc04's placeholder ergonomics for non-searchable pickers and cascade multiselects, batch setAttributes, debounced async search, and AbortSignal in searchCallback), plus two wrapper-level extensions to support patterns the core phx-update="ignore" semantics can't reach: a server→client update channel for option/selection mutation, and a declarative server-side searchCallback. A subsequent code-level audit against upstream's FEATURES.md produced a wave of typed-attr completions, two enum bug fixes, an unconditional canonical-member-default change in OptionHelpers, and a major e2e expansion targeting wrapper-only behaviour. A later visual-parity pass then walked every /examples/* page against its upstream examples-*.html mirror (diffing the pages changed since rc04): it ported the rc05 demos that hadn't been mirrored (action-button positioning/rows/alignment, the classic→events cross-link), added one more typed attr (show_select_all) surfaced by the diff, and shipped the document-level Font Awesome <link> the icon demo's own instructions require.
Added
mix keen_web_multiselect.install— a one-command installer for esbuild Phoenix apps. Idempotently wires the assets: importsmultiselect.js+ the hook intoassets/js/app.js, registersKeenWebMultiselectHookon theLiveSocket(adds ahooks:key to a default socket, or merges into an existing object-literal one), and importsmultiselect.cssintoassets/css/app.css. Dependency-free (a plainMix.Task, no Igniter — the friction here is JS/CSS text, not Elixir AST). Conservative by design: a non-object-literalhooks:, a missingapp.js(importmap apps), or an unrecognizedLiveSocketshape is left untouched and printed as a manual step rather than risking a bad edit. Supports--dry-run. The text transforms are pure functions with unit coverage for the default-socket, existing-hooks, non-literal-hooks, no-socket, and idempotent re-run cases.Keenmate.WebMultiselect.push_update/3— a first-class helper for the server→client update channel. Wraps the rawpush_event(socket, "web_multiselect:update", %{id: ..., options: ..., value: ...})so consumers no longer hand-write the magic event name or payload shape.push_update(socket, "region", options: opts, value: [])swaps options and clears selection; only the keys you pass are sent (value: []clears without touching options;options: optsleaves the selection to the component). The underlying event and hook behavior are unchanged — purely an ergonomic surface over the existing channel. Documented with cascade and server-authoritative-rule examples in the README and demonstrated on the events-callbacks page.hook={true}shorthand on<.web_multiselect>. The:hookattr now accepts a boolean as well as a string:hook={true}resolves to the bundled"KeenWebMultiselectHook", a string still names a custom hook, andfalse/nilrender no hook. Removes the stringly-typed footgun where a typo in the hook name was a silent no-op. Attr type widened:string→:any; resolution happens in the component body (resolve_hook/1).- Bundled upstream bumped to
@keenmate/web-multiselectv1.12.0-rc05 —priv/static/multiselect.js/multiselect.css/multiselect.d.tsresync'd from the upstream dist.Keenmate.WebMultiselect.upstream_version/0now returns"1.12.0-rc05". rc05 changes summary: hover tooltips on dropdown options (enable-option-tooltips+option-tooltip-placement/-follow-cursor/-delay/-offset,getOptionTooltipCallback,--ms-option-tooltip-*vars); action-button positioning (actions-positiontop/bottom,actions-align, multi-rowActionButton.row); smart built-in action defaults (select-all/clear-allauto-disable when they'd be no-ops, unless the consumer set an explicitisDisabled/getIsDisabledCallback);beforeSelectCallback/beforeDeselectCallbackveto interceptors; a new--ms-state-min-heightthemable var; and two bug fixes (single-select stale-filter on reopen, dropdown height swap between empty/loading states). The DOM event names (select/deselect/change) are unchanged, soKeenWebMultiselectHookneeds no changes. - Five new typed
attr/3declarations inKeenmate.WebMultiselect.Components.web_multiselect/1for the rc05 option-tooltip surface:enable_option_tooltips(boolean),option_tooltip_placement(enum, 12 Floating-UI placements, defaulttop-start),option_tooltip_follow_cursor(boolean),option_tooltip_delay(integer),option_tooltip_offset(integer); plusactions_position(enumtop/bottom) andactions_align(enumstretch/left/right/center/space-between) for the action-button layout. Snake→kebab mapping unchanged; samenil-default-means-omit semantics as every other attr. show_select_alltypedattr/3inKeenmate.WebMultiselect.Components.web_multiselect/1— exposes upstream's built-in Select All action button (show-select-all→ internalisSelectAllShown). Boolean, samenil-default-means-omit semantics as the rest; pairs naturally withshow_checkboxes. Previously reachable via neither the typed surface nor the:restglobal —show_select_allisn't a valid HTML global attribute, so Phoenix dropped it silently (it landed in neither the top-level assigns nor@rest, soOptionHelpersnever kebab-cased it and nothing reached the element). Surfaced by the visual-parity pass: the performance page's 15k-option filter demo was missing the button upstream shows.- Two new mirrored example pages (
/examples/tooltips,/examples/events-callbacks) — card-for-card mirrors of upstream's newexamples-tooltips.htmlandexamples-events-callbacks.html, plus hub-index cards and router entries. The tooltips page covers default/custom option tooltips, virtual-scroll support, full-width placement/follow-cursor, narrow-control side placement, independent--ms-option-tooltip-*styling, and badge tooltips. The events page demonstrates the DOM events, theon*property twin, and thebeforeSelect/beforeDeselectveto interceptors with live veto demos. It also carries a wrapper-specific Server-side binding (LiveView) section — threehook="KeenWebMultiselectHook"pickers whoseselect/deselect/changeevents drivehandle_event/3: live server-derived state (price + total computed from a catalog the browser never sees), a server-stamped event log, and a server-authoritative "max 3" rule enforced by pushingweb_multiselect:updateback to correct the element — the practical answer to "you can't veto over the wire" (allow optimistically, then correct). - Bundled upstream rc04 baseline (carried forward) — Upstream changes summary:
select-placeholderandno-data-placeholderattributes for non-searchable / empty-list states,searchCallbacknow receives anAbortSignal(in-flight requests are cancelled when superseded),search-debounceattribute collapses keystroke bursts into a single request, batchedsetAttributes(attrs)method, and the search-disabled default placeholder changed from"Search..."to"Pick an option...". All additive at the wrapper boundary — existing consumers see no surface change. - Three new typed
attr/3declarations inKeenmate.WebMultiselect.Components.web_multiselect/1covering the rc04 additions:select_placeholder(string),no_data_placeholder(string),search_debounce(integer). Snake→kebab mapping unchanged; samenil-default-means-omit semantics as every other attr. search_eventattr — declarative server-sidesearchCallbackover the LV channel —<.web_multiselect search_event="github_search" hook="KeenWebMultiselectHook" />rendersdata-search-event="github_search"on the element. Onmounted/0the hook reads that attribute and installsel.searchCallback = (query, signal) => pushEventTo(el, "github_search", %{id, query}, replyCallback), resolving the promise withreply.resultsfrom the server's{:reply, %{results: [...]}, socket}. Consumers write zero JS — the entire async-search pattern reduces to ahandle_event/3clause. Honors the rc04AbortSignal: late replies for superseded queries are dropped before reaching the dropdown.web_multiselect:updateserver→client event in the hook —push_event(socket, "web_multiselect:update", %{id: "cascade-unit", options: new_options, value: []})mutates element state from the LV process. Filters bypayload.idso the broadcast can target a specific multiselect on a page that has several. Necessary because the wrapper's auto-emittedphx-update="ignore"(which protects the component's internally-managed children from morphdom) also blocks LV from morphing attribute changes likedata-options— the hook channel is the canonical way to mutate options/selection from the server. Implementation:payload.options→el.options = ...;payload.value→el.setSelected([...])(orel.value =fallback), wrapping single values into arrays so the same call shape covers single- and multi-select.- Five more typed
attr/3declarations —checkbox_align(enumtop|center|bottom),dropdown_max_width(string),remove_button_tooltip_text(string; supports{0}interpolation),badge_height(integer; popover virtual-scroll), andshow_debug_info(boolean; flips upstream's.ms__debug-infostats panel reactively without component reinit). All were already in upstream's attribute table or out-of-table special-case map; previously reachable only via:restglobal passthrough, now first-class with the samenil-default-means-omit semantics as the rest. Surfaced by an audit against upstream'sFEATURES.md. OptionHelperscanonical member-attr defaults are now unconditional and cover all six members. Previously: the wrapper emittedvalue-member="value"+display-value-member="label"only when:optionswas set in HEEx. That left async-search demos (searchCallback-driven, no:options=at mount) and JS-sideel.options = [...]assignments without any member configuration, so upstream'svalueMemberstayedundefinedand every row fell through to[N/A]. Now: the wrapper emits all six canonical attrs (value-member,display-value-member,icon-member,subtitle-member,group-member,disabled-member) on every render. Safe across all option-loading paths — upstream's declarative<option>parser produces objects with the same key names, so the explicit defaults are no-ops there. Explicit*_memberoverrides still win (verified by unit tests).search-value-memberis intentionally not defaulted — defaulting it would change which text the search matches against, with no obvious canonical key name to point at.FEATURES.mdat the repo root — full feature inventory cross-referenced with the wrapper's surface and e2e coverage. Mirrors upstream's section structure plus the new §15 Developer tooling — debug & logging coveringshow-debug-info, the bundledenableLogging/disableLogging/setLogLevel/setCategoryLevelexports,LOGGING_CATEGORIES, and thewindow.components['web-multiselect']global. Adds a "Wrapper-only features" section for surface that doesn't exist upstream (hookattr,phx-update="ignore"auto-emission,data-ready=""pre-emission,.formpolyfill,search_event,web_multiselect:update,FormFieldintegration, tuple option normalization, canonical-member-default emission).
Changed
- Elixir module namespace renamed
KeenWebMultiselect→Keenmate.WebMultiselect. All four modules move under theKeenmate.umbrella (Keenmate.WebMultiselect,.Components,.OptionHelpers,.FormHelpers) and their source/test files move to the conventionallib/keenmate/web_multiselect/path. Unchanged: the OTP app / hex package name (:keen_web_multiselect), thepriv/staticasset filenames, and the JS hook identifier (KeenWebMultiselectHook) plus its"web_multiselect:*"event names — so asset wiring, importmaps, andapp.jsneed no changes. Consumers only update theirimport Keenmate.WebMultiselect.Componentsline (and any direct module references). Pre-1.0, no deprecation shim. - rc05 upstream callback renames propagated through the demo site (breaking upstream, no aliases). Two JS-side rename waves landed in rc05: the fire-and-forget notifications
selectCallback/deselectCallback/changeCallbackbecame theon*eventsonSelect/onDeselect/onChange, and theActionButtonpredicatesisVisibleCallback/isDisabledCallbackbecamegetIsVisibleCallback/getIsDisabledCallback. These are JS-side config, not HTML attributes, soKeenmate.WebMultiselect.Components(typedattr/3) and the LV hook (which uses the unchanged DOM event names + the unchangedsearchCallback) are unaffected. The only wrapper-repo impact was the demotest_appexample pages whose inline scripts setactionButtons:action_buttons_live.exandnew_api_live.exwere updated to thegetIs*Callbackshape (the predicate callbacks would otherwise silently stop firing under rc05), along with their prose/code snippets and the callback-priority list.
Fixed
badges_display_modeenum had the wrong values. Declared as["pills", "count", "compact", "partial", "none"]. Upstream accepts["badges", "count", "compact", "partial", "none"](default"badges") and silently falls back to default when given"pills"— sobadges_display_mode="pills"looked like it worked while doing nothing, andbadges_display_mode="badges"(the only correct value) was rejected by the wrapper at compile time. Fix is one line incomponents.ex; downstream callsites intest_app/lib/test_app_web/live/fixtures/attributes_live.ex,e2e/attributes.spec.ts, and the badge-mode demo card intest_app/lib/test_app_web/live/examples/classic_live.ex(prose saidpills (default)) all updated to match.badge_tooltip_placementenum was incomplete. Was the four cardinal sides; upstream accepts 12 Floating-UI placements — the four sides plus-start/-endvariants for each. Now declares the full set.
Internal — e2e expansion
The wrapper's e2e suite went from 12 specs to 41 across 10 files, focused on what only the wrapper can break plus the headline upstream behaviors the audit flagged as unverified.
/test/search-eventfixture +e2e/search_event.spec.ts(4 tests) — declarative server-side search viasearch_event="...". Fixture has a toy fruit catalog and aslow_nexttoggle. Spec provesdata-search-eventreaches the element, typed queries arrive at the LV'shandle_event/3with the right payload shape, fresh queries replace previous results, and (the wrapper-only one) the rc04 AbortSignal contract is honored: a slow stale reply doesn't overwrite a newer fast result./test/push-updatefixture +e2e/push_update.spec.ts(3 tests) —web_multiselect:updatechannel. Fixture has parent/child/sibling triple plus a manual preselect button. Spec proves the parent-on-change-cascade pushes new options to child, sibling is untouched by id-filtered updates (snapshot-compared, not just length), and a value-onlypush_eventpreserves the child's option list while changing its selection./test/declarativefixture +e2e/declarative.spec.ts(5 tests) — bare<option>children via:inner_block,<optgroup>,selected,disabledround-trip. Asserts the wrapper does not emitdata-optionsfor the declarative path. After the unconditional-defaults change, also asserts all six canonical member attrs ARE emitted (the previous "must be absent" assertion was the proximate cause of the silently-failing async-search demos).e2e/selection.spec.tsextended with two regression guards:phx-update="ignore"is auto-emitted on every element that has an:id, anddata-ready=""is pre-emitted on first render so the placeholder doesn't flash on WS connect.e2e/form.spec.tsextended — a prefilledFormField(value["banana", "cherry"]) renders the badges post-mount, not just shipping the value in form params. Catchesinitial-valuesshape regressions that would let upstream silently drop the pre-selection.- Unit tests for the new
OptionHelpersmember-default behavior: the six canonical attrs emit regardless of:options, explicit overrides win against all six, andsearch-value-memberis never defaulted. /test/virtual-scrollfixture +e2e/virtual_scroll.spec.ts(3 tests) — proves virtual scrolling actually windows the DOM, not just that the attrs render kebab-cased. A 500-option picker renders.ms__options--virtualwith fewer than 100 materialized.ms__optionnodes whileel.picker.allOptions.length === 500; scrolling to the bottom recycles early rows out and late rows in; a non-virtual control with identical data renders all 500./test/badges-popoverfixture +e2e/badges_popover.spec.ts(3 tests) —partialmode withbadges_max_visible=2and four preselected values renders exactly two badges plus a+2 moreoverflow badge; clicking it opens the selected-items popover (.ms__selected-popover--visible) listing all four; the close button dismisses it./test/add-newfixture +e2e/add_new.spec.ts(3 tests) —allow_add_new+ a JSaddNewCallback: typing an unknown term and pressing Enter creates the option, selects it, renders the badge with the original-cased label, clears the input, and the new option joinsel.picker.allOptionsfor re-selection.e2e/declarative.spec.tsextended — per-optiondata-icon/data-subtitleflow through the:inner_blocklight-DOM children and render as.ms__option-icon/.ms__option-subtitle.e2e/attributes.spec.tsextended — the four audit-added typed attrs (checkbox_align,dropdown_max_width,remove_button_tooltip_text,badge_height) render kebab-cased, andshow_debug_info={true}both lands asshow-debug-info="true"and renders the upstream.ms__debug-infopanel. Regression guard so a future enum/typo incomponents.exfails CI.e2e/events.spec.tsextended — a single click firesselectandchangeexactly once each (no double-fire) with zerodeselect, andselectfires beforechange.- Two pre-existing spec bugs caught and fixed while landing the above.
push_update.spec.tsreadel.allOptions—undefinedon the custom element (the option array lives on the internal picker), so(el.allOptions || [])silently returned[], the first assertion passed trivially, and the real assertions timed out; switched toel.picker.allOptions.form.spec.tsmatchedpage.locator('web-multiselect'), which became a Playwright strict-mode violation once the#prefilledform was added to that page; scoped to#basket_fruits.
Internal — demo site (test_app/)
test_app/is now a deployable demo gallery, not just an e2e harness. Two router pipelines split the surface::demos_browser(gallery at/, ten example LiveViews under/examples/<name>) uses a newdemo_rootlayout that loadsexamples-shared.css(ported verbatim from upstream'sexamples-shared.css);:fixtures_browser(existing/test/*Playwright fixtures) keeps the minimalrootlayout and narrowapp.css. The two stylesheets don't conflict because they're never loaded together.IndexLiverenders the card grid withwrapper v#{Application.spec(:keen_web_multiselect, :vsn)}+upstream v#{Keenmate.WebMultiselect.upstream_version()}badges so deployments show both versions inline.- Ten demo LiveViews under
test_app/lib/test_app_web/live/examples/—classic,new_api,performance,templating,action_buttons,sizes,base_variables,theming,logging,positioning. Pages with elaborate callbacks (templating render callbacks, performance metrics, action-button visibility/text/class callbacks, positioning drift detection, logging API surface) wire JS via inline<script type="module">blocks that import from the importmap — same approach upstream'sexamples-*.htmlpages take.new_apiis the showcase for the wrapper's two LV-specific patterns: a three-tier cascading select driven bypush_eventfor option mutation, and side-by-side async-search cards (browser → GitHub vs browser → LiveView → GitHub) demonstrating when to route through the server (auth tokens, response transformation, CORS-restricted backends). - Shared HEEx primitives in
TestAppWeb.Examples.SharedComponents—<.example_page>,<.card>,<.note>,<.code_block>,<.output_panel>,<.form_group>,<.grid>. Match upstream'sexamples-shared.cssclass names so the same prose shapes work across both repos. - Port allocation
xxx0main /xxx1debug — endpoint moved from12_300to4_060;live_debuggerpinned to4_061viaconfig :live_debugger, port: 4061intest_app/config/dev.exs. Makefiledevtarget,playwright.config.ts(baseURL+webServer.url), and the README/CHANGELOG narrative all updated to match. - Dev-mode code reloader enabled —
config :test_app, TestAppWeb.Endpoint, code_reloader: true, reloadable_apps: [:test_app, :keen_web_multiselect]intest_app/config/dev.exs, plugPhoenix.CodeReloaderin the endpoint,listeners: [Phoenix.CodeReloader]intest_app/mix.exs(silences the Mix-listener warning introduced in Phoenix 1.8.8).code_reloader: falsestays the implied default for the:testenv so Playwright runs stay deterministic. :inets, :ssladded toextra_applicationsso the LV-tunneled GitHub search demo can call:httpc.request/4for the outbound HTTPS. Used byTestAppWeb.Examples.NewApiLive.github_search_users/1— the demo's own helper, not part of the wrapper.- Demo bug fixes from the audit pass — these were all
test_app/lib/test_app_web/live/examples/*issues that surfaced once the demo gallery went live and someone clicked through every page:- Templating — Single-select with compact closed display.
renderSelectedContentCallbackfor single-select returns a plain string that goes straight into<input>.valueupstream; the demos were returning HTML<span>...</span>strings and rendering them literally as<span>Jane</span>. Fixed by returning plain strings; card prose now states the contract explicitly. - Templating — Badge rendering. No tooltips were showing because the demo never set
enable_badge_tooltips. Now opt-in viaenable_badge_tooltips={true}plusgetBadgeTooltipCallbackfor richer content than the defaultdisplayValue. The "options also need tooltips" gap is now called out in the card prose (upstream has no per-option tooltip API — would needtitle=insiderenderOptionContentCallback). - Templating + Performance — Rich Content priority badges.
customStylesCallbackwas targeting.ms__badgebut upstream's default badge background lives on the inner.ms__badge-text+.ms__badge-removeelements via CSS variables, so the per-priority colors were silently covered. Selectors now target both inner parts (and setborder-colorto match). - Action Buttons — Built-in actions. Passing bare strings (
['select-all', 'clear-all']) made the buttons render"undefined"labels because upstream has no name lookup for the action — every entry needs an explicittext. Same fix applied to the Static properties and Dynamic visibility / disabled cards. - Action Buttons — Dynamic callbacks. Three cards (cards 3, 4, 5) were reading
ctx.options.length(the picker's config object, not its options array) andctx.selectedValues.length(the selectedValues is aSet, no.length). Card 3's Select All button was disabled when empty instead of when full becauseundefined === undefinedistrue. Fixed to usectx.allOptions(the actual array) andctx.selectedValues.size/.has(String(v)). - Action Buttons — Layout nowrap vs wrap. Uniform
Action 1..Action 8labels collapsed to a tidy single row even in wrap mode, hiding the layout difference. Replaced with mixed-width labels (Pick 3 random/Even-indexed/Odd/First half/Last/A–M only/Invert selection/ built-inClear) that actually drivesetSelectedso the picker visibly responds to each. - Performance — Virtual Scrolling stats panel. The stats divs (
Init / First render / Last searchms) flashed values then reset to—because they're inside the LV template and got morphed back on every WS patch. Wrapped the stats container inphx-update="ignore"so the inline<script>updates survive the connected re-render. Also added explicitvalue_member="value"+display_value_member="label"toperf-15kso the JS-loaded 15k options didn't render as[N/A]— this was before the unconditional-defaults change above and is now redundant but harmless. - Classic — Rich Content. The
:icon/:subtitle/:groupkeys on data were silently dropped because the wrapper only auto-defaultedvalue-memberanddisplay-value-member. Was the symptom that motivated the unconditional-defaults change. Card prose now lists all six defaulted members. - New API — async-search cards. Removed the three explicit
value_member/display_value_member/subtitle_memberattrs from bothsearch-jsandsearch-lv— now redundant after the unconditional-defaults change. Cards read as cleaner showcases of "just setoptions=and it works".
- Templating — Single-select with compact closed display.
- Visual-parity pass — completing the rc05 demo mirror. After the two new example pages landed, a page-by-page diff of every
/examples/*LiveView against its upstreamexamples-*.html(scoped to the five pages upstream touched since rc04) closed the remaining gaps:- Action Buttons — §13 "Positioning, Rows & Alignment" ported. Upstream's largest rc05 demo addition (four pickers in a
grid-2) had never been mirrored — the page jumped from §12 straight to the Summary. Added it:pos-top-rows/pos-bottom-rowsshow multi-row action layout (per-buttonrow: 1|2) withactions_position="top"|"bottom", andalign-right/align-betweenshowactions_align. Uses the rc05 typed attrs (actions_position/actions_align) declaratively; JS wires therowButtons(withFirst 3/Invertcustom actions) plus a code sample and the "Row ordering" note. Verified headless: 2 rows / 4 buttons and the expectedms__actions--top/--bottom/--align-right/--align-space-betweenclasses, no console errors. - Action Buttons — §11 Font Awesome now actually renders. The demo (and its own note) says font icons in a Shadow DOM component need the
@font-faceregistered at the document level — but the shareddemo_rootlayout never shipped that<link>, so the glyphs rendered as empty[]boxes despite the shadow-injectedcustomStylesCallback@import. Added a conditional Font Awesome<link>to the layout<head>, gated on anassigns[:font_awesome]flag (same per-page-assign mechanism the layout uses forpage_title) thataction_buttons_live.exsets inmount. Only that page loads the CDN stylesheet; verified the FA::beforenow resolves tofont-family: "Font Awesome 6 Free"with a real glyph. - Performance — Select All button. Added
show_select_all={true}to the 15k-option filter demo to match upstream (see the new typed attr above). The upstream page's secondshow-select-allis inside a<!-- Temporarily hidden -->block, so this is correctly a single-picker change. - Classic — cross-link to the events page. Mirrored the small rc05
<small>note at the foot of the Event Handling card pointing at theon*handlers /beforeSelect/beforeDeselectinterceptors, using<.link navigate="/examples/events-callbacks">(the LiveView equivalent of upstream's<a href>). - Hub blurb.
IndexLive's Action Buttons card description now mentions position/rows/alignment. - No drift on the other pages. The diff confirmed the remaining rc04→rc05 example changes were already handled:
new_api_live.ex'sisVisibleCallback/isDisabledCallback→getIs*Callbackrenames (done in the rc05 pass, 0 old names remain), and the two wholly-new pages. The randomized-vs-deterministic option data on the performance page (upstream usesMath.random(), so it can never byte-match) and the wrapper-only cascade "extras" section on new-api were left as intentional, non-mirror differences.
- Action Buttons — §13 "Positioning, Rows & Alignment" ported. Upstream's largest rc05 demo addition (four pickers in a
[0.1.0] - 2026-06-23
Initial release. Wraps @keenmate/web-multiselect v1.12.0-rc03 as a Phoenix LiveView
function component.
Added
Keenmate.WebMultiselect.Components.web_multiselect/1—<.web_multiselect>function component with typedattr/3declarations for every documented upstream attribute (booleans, enums, integers, JSON-encoded option lists). Snake_case in HEEx maps to kebab-case on the rendered element.Keenmate.WebMultiselect.OptionHelpers— option-list normalization, JSON encoding ofoptions/initial-values, explicit"true"/"false"rendering for boolean attributes (the upstream component reads explicit strings, not bare HTML attribute presence). Phoenix.Component bookkeeping atoms (:__given__,:__changed__) are filtered before key encoding so they never reach the rendered element as--given--="..."attributes. Option lists are emitted asdata-options(upstream readsdata-options; theoptionsproperty is reserved for JS-side assignment), withvalue-member="value"/display-value-member="label"defaulted in the same step so[%{value: ..., label: ...}]works out-of-the-box — upstream's declarative member defaults only apply when parsing<option>children, not when consumingdata-optionsJSON.Keenmate.WebMultiselect.FormHelpers—Phoenix.HTML.FormFieldintegration;<.web_multiselect field={@form[:tags]} />fills inid,name, and feeds the field value into the upstreaminitial-valuesattribute. Explicit:id/:nameassigns win over the field defaults.- LiveView morph compatibility — the wrapper renders
phx-update="ignore"(whenever:idis set) so morphdom leaves the upstream component's internally-managed children alone on subsequent renders, plus pre-emitsdata-ready=""on the element itself so LV'smergeAttrs(which stripsdata-*attributes onignore-marked elements that aren't in the server-rendered HTML) doesn't tear down the placeholder visibility flag upstream sets viarequestAnimationFrameduringconnectedCallback. Without this, the placeholder text "Search..." flashes for one frame after upgrade then disappears on every LV render, because the CSS rule:host([data-ready]) .ms__input::placeholder { opacity: ... }(controls.css:52) only matches whiledata-readyis present. - Bundled upstream JS + CSS in
priv/static/multiselect.jsandpriv/static/multiselect.css. Nonpm installrequired by consumers. - Optional LiveView hook in
priv/static/keen_web_multiselect_hook.js. Sethook="KeenWebMultiselectHook"on the component and the hook forwardsselect/deselect/changeCustomEvents to the server as"web_multiselect:select"/":deselect"/":change"events with{id, value, values}payloads. The hook module also polyfills a.formgetter onto<web-multiselect>(returningthis.internals?.form); upstream callsattachInternals()for form-association but never exposes.form, and Phoenix LV'sphx-changedelegation drops CustomEvents whosee.target.form === undefined. The polyfill is installed once at module load viacustomElements.whenDefined("web-multiselect")so it covers consumers who never opt into the hook itself. Keenmate.WebMultiselect.upstream_version/0— runtime accessor that returns the bundled@keenmate/web-multiselectversion (currently"1.12.0-rc03").Keenmate.WebMultiselect.asset_path/1— absolute on-disk path to a file inpriv/static/, for setup tasks that copy assets into a host application'sassets/folder.
Internal
Makefilemirroring the upstream@keenmate/web-multiselectMakefile verb pattern:make publish-rc/make publish/make publish-dry/make build/make test/make clean/make help. The rc/release split is enforced by checking the@versionshape inmix.exs—make publish-rcrefuses unless the version matchesX.Y.Z-rc.N;make publishrefuses if the version is an rc. Plusmake current-versionandmake last-publishedfor state inspection..claude/commands/publish.md—/publishslash command following the canonical Bliss Frameworkweb-components/publish-command.mdstructure, adapted for Hex (mix hex.info/mix hex.build/mix hex.publishinstead of the npm equivalents). Sections marked[canonical]are byte-identical (modulo npm→Hex substitutions) across all KeenMate component repos.- ExUnit test suite covering
OptionHelpersattribute encoding,FormHelpersfield binding, andComponents.web_multiselect/1rendering (23 tests). - Playwright e2e harness —
test_app/is a minimal Phoenix 1.8 + LiveView 1.2 host (Bandit on127.0.0.1:4060, path dep on the parent, shareddeps/andmix.lock). Four fixture LiveViews undertest_app/lib/test_app_web/live/fixtures/mirror upstream's test pages but assert what only the wrapper can break:field={@form[:fruits]}round-tripping throughphx-change, the LV hook forwardingselect/deselect/changetohandle_event/3with{id, value, values}, snake_case→kebab-case attribute mapping reaching the rendered element. Asset delivery uses an<script type="importmap">block in the root layout pointing atphoenix.mjs,phoenix_live_view.esm.js, and the wrapper's bundledmultiselect.jsserved via threePlug.Staticblocks — no esbuild step intest_app/. Specs ine2e/*.spec.ts, driven byplaywright.config.tsat the repo root (single chromium project,webServer = mix phx.serverwithcwd: test_app). Makefile targets:make test-e2e-install/make test-e2e/make test-e2e-ui/make test-e2e-headed. mix.exspackage metadata: MIT license, Hex docs config viaex_doc, source URL, package:fileswhitelist scoped tolib/,priv/,mix.exs,README.md,CHANGELOG.md,LICENSE,.formatter.exs.