PhoenixKitCatalogue.Web.Components.ItemPicker (PhoenixKitCatalogue v0.21.0)

Copy Markdown View Source

Combobox LiveComponent for picking a single item from the catalogue via server-side search.

Drop one into any LiveView — typically many, one per row in a picker table. Each instance owns its own search state; the parent LV only reacts to two messages:

{:item_picker_select, id, %Item{}}  # user chose an item
{:item_picker_clear,  id}           # user cleared the selection

A third message fires only when the caller opts into a clickable photo thumbnail (photo_clickable), so consumers that don't set it never receive it:

{:item_picker_photo_click, id, %Item{}}  # user clicked the thumbnail

Clicking the thumbnail ALSO opens a self-contained product card (ProductCard) — no host wiring is needed for that. The :item_picker_photo_click message is only an extra hook for a host that wants to react (analytics, its own navigation). A host that sets photo_clickable MUST provide a matching handle_info/2 clause (or a catch-all), or the message crashes its LiveView.

API

<.item_picker
  id="row-42-picker"
  category_uuids={[category.uuid]}
  selected_item={@chosen_item}
  excluded_uuids={@already_used_uuids}
  locale="en"
/>

Attrs:

  • :id (required) — unique DOM/component id. The :item_picker_* messages echo this back so a parent with N pickers knows which fired.
  • :category_uuids — scope search to these categories. nil or [] means "all categories + uncategorized" (matches Catalogue.search_items/2).
  • :catalogue_uuids — scope search to these catalogues. Composes with :category_uuids (AND).
  • :include_descendants — when true (default), :category_uuids is expanded through the V103 tree; pass false for literal set semantics.
  • :only:uncategorized_only restricts results to items without a category; :categorized_only restricts to items in some category; nil (default) is unrestricted. Forwards to Catalogue.search_items/2's :only opt.
  • :statuses — list of item statuses to include ("active", "inactive", "discontinued"); nil or [] = all non-deleted. Forwards to Catalogue.search_items/2's :statuses opt — the same scope vocabulary ItemSelectorModal accepts.
  • Changing any scope attr (:category_uuids, :catalogue_uuids, :include_descendants, :only, :statuses) from the parent invalidates the current option list and closes the dropdown — a result set fetched under the old scope is never left selectable.
  • :selected_item — the %Item{} currently chosen (or nil). Drives the input text and the aria-selected / primary-border styling in the dropdown. When the chosen item carries a featured photo (data["featured_image_uuid"]), a small thumbnail of it is rendered to the left of the input; items without a photo render as before (input only).
  • :excluded_uuids — items in this list are rendered dim + aria-disabled and cannot be clicked; the select handler refuses them server-side too (a click can race the re-render that excluded its target). Use for "already picked in another row" state.
  • :locale (required) — locale string for translated display names ("en", "es", etc.). Resolved via Catalogue.get_translation/2.
  • :placeholder — input placeholder. Defaults to "Search items…".
  • :empty_query_limit — how many items to show when the query is empty (the "just focused" state). Defaults to 10.
  • :page_size — max results fetched per query. Defaults to 20. When the unbounded count exceeds this the dropdown shows a "Type to refine…" sentinel row so the user knows there's more.
  • :disabled — disables the input and hides the clear button.
  • :format_price — 1-arity function taking an %Item{} (with :catalogue preloaded — the search always does this) and returning a display string or nil. Defaults to a Decimal stringifier of item_pricing(item).final_price. Return nil to omit the price column entirely.
  • :show_unit — when true, renders the item's measurement unit (via :format_unit) as a small muted label next to the price in each dropdown row. Defaults to false (no unit) so existing consumers are unaffected.
  • :format_unit — 1-arity function taking the item's unit string and returning a display label ("" to omit). Only used when :show_unit is true. Defaults to a built-in mapping of common abbreviations (piecepc, setset, pairpair, sheetsheet, m2, running_meterrm; unknown strings pass through). Supply your own to use a different unit vocabulary.
  • :show_sku — when true, renders the item's :sku as its own column between the name/breadcrumb block and the price/unit block on each dropdown row (as an em dash when the item has no SKU on file, so a blank catalogue field doesn't read as a rendering bug). Defaults to false so existing consumers are unaffected.
  • :highlight_selected — when true (default), the input gets the input-primary border while an item is selected. Pass false to suppress that highlight. Default preserves existing behaviour.
  • :photo_clickable — when true, the main-image thumbnail rendered to the left of the input becomes a button that echoes {:item_picker_photo_click, id, %Item{}} upward (the navigation hook the product-card feature builds on). Defaults to false: the thumbnail still renders when the item has a photo, but as an inert image, so consumers without a handler are unaffected.
  • :photo_placeholder — when true (and :photo_clickable is also true), a selected item WITHOUT a photo renders a clickable placeholder (a muted photo icon) in the thumbnail's place instead of nothing, so its product card can still be opened. Defaults to false: unchanged — no element renders for a photo-less item, exactly as before. Has no effect when :photo_clickable is false, since there is then nothing to click.
  • :photo_size — Tailwind size classes (e.g. "w-8 h-8") applied to the thumbnail/placeholder image. Defaults to "w-8 h-8", the previously hardcoded size, so existing consumers render unchanged.
  • :initial_query — optional seed string for the search input. When provided (and nothing is selected and the user hasn't typed), the input is prefilled with this string and the dropdown opens with matching results on first render. Fires once; subsequent updates leave the query alone. Defaults to nil (no seeding).

Keyboard / a11y

Handled client-side by the colocated ItemPicker hook:

  • ArrowDown / ArrowUp cycle through enabled options (announced via aria-activedescendant; DOM focus stays on the input).
  • Home / End jump to first / last enabled option.
  • Enter activates the focused option (simulates a click so the normal select event fires).
  • Escape closes the dropdown and keeps focus on the input.
  • Clicking outside the picker closes it (phx-click-away).

The dropdown is absolutely positioned and elevated with z-50; the parent container must allow overflow (overflow: visible or just don't set overflow: hidden on an ancestor that clips it).