PhoenixKitWeb.Components.Core.BulkSelect (phoenix_kit v1.7.138)

Copy Markdown View Source

Bulk-select toolkit for admin tables, client-side. The selection lives in the browser; the server only learns about it at action time (when the user clicks an action button) via the BulkSelectScope JS hook. This makes per-checkbox toggles feel instant — no LV round-trip on every click.

Three function components compose with <.table_default>, plus a wrapper element with the hook attached.

  • <.bulk_select_scope> — opens an inline-styled wrapper with phx-hook="BulkSelectScope". Everything inside (header cell, row cells, toolbar buttons) participates in the same selection set. Pass total_count so the hook knows when "all" is checked.

  • <.bulk_select_header_cell> — the header checkbox. Tri-state (unchecked / indeterminate / checked) is managed by the hook.

  • <.bulk_select_cell> — per-row checkbox bound to a UUID value.

  • <.bulk_actions_toolbar> — the toolbar above the table. Buttons with data-bulk-action dispatch LV events with the selected UUIDs in the {uuids: [...]} payload.

Example

<.bulk_select_scope id="projects-bulk" total_count={length(@projects)}>
  <.bulk_actions_toolbar
    on_open_reorder="open_reorder_modal"
    on_clear_selection="clear"
    noun_singular={gettext("project")}
    noun_plural={gettext("projects")}
  />

  <.table_default id="projects-list" size="sm">
    <.table_default_header>
      <.table_default_row>
        <.bulk_select_header_cell
          id="projects-select-all"
          aria_label={gettext("Select all projects")}
        />
        <.table_default_header_cell>Name</.table_default_header_cell>
        ...
      </.table_default_row>
    </.table_default_header>
    <tbody>
      <.table_default_row :for={p <- @projects}>
        <.bulk_select_cell value={p.uuid} />
        <.table_default_cell>{p.name}</.table_default_cell>
        ...
      </.table_default_row>
    </tbody>
  </.table_default>
</.bulk_select_scope>

The consumer LV handles on_open_reorder (etc.) with a payload of %{"uuids" => uuids}:

def handle_event("open_reorder_modal", %{"uuids" => uuids}, socket) do
  {:noreply, assign(socket, show_reorder_modal: true, captured_uuids: uuids)}
end

Why client-side

Server-side selection (each click is a phx-click round-trip) forces a re-render with every toggle, which feels laggy at any meaningful network latency. Bulk-select is a high-frequency UI interaction where the server only needs to know the selection at the moment it acts on it — making the client the natural owner.

Summary

Functions

Floating toolbar above the table. Built-in actions: Reorder, Delete, Clear. Each button is opt-in via flags / event-name attrs. Toolbar always renders; the count text + button labels + visibility update live as the user toggles checkboxes.

Per-row checkbox cell. The value is captured into the selection set when checked; it's the identifier the server receives in {uuids: [...]} when an action fires.

Header checkbox cell — drop-in replacement for <.table_default_header_cell> in the bulk-select column. The BulkSelectScope hook drives its checked / indeterminate state based on the current selection.

Opens a bulk-select scope. Everything inside this wrapper participates in the same selection set.

Functions

bulk_actions_toolbar(assigns)

Floating toolbar above the table. Built-in actions: Reorder, Delete, Clear. Each button is opt-in via flags / event-name attrs. Toolbar always renders; the count text + button labels + visibility update live as the user toggles checkboxes.

Reorder is mandatory (on_open_reorder is required). Delete and Clear are optional.

Attributes

  • on_open_reorder (:string) (required) - Event pushed when the Reorder button is clicked. Receives %{"uuids" => uuids}.
  • on_bulk_delete (:string) - Event pushed when Delete is clicked. Required if allow_delete is true. Defaults to nil.
  • noun_singular (:string) - Defaults to "item".
  • noun_plural (:string) - Defaults to "items".
  • allow_delete (:boolean) - Defaults to true.
  • reorder_gate (:atom) - When :always, the Reorder button is always visible — label is 'Reorder all' when 0–1 rows are selected, 'Reorder N selected' when 2+. (A one-row reorder is a no-op, so we keep the 'Reorder all' label there; the consumer LV normalises 1-uuid scopes to :all when applying.) When :multi, hidden unless count > 1 — useful when the surrounding context has no meaningful 'reorder all' interpretation (e.g. the list is currently sorted by name, not the manual position field). Defaults to :always. Must be one of :always, or :multi.
  • reorder_dialog_id (:string) - Optional id of a kept-in-DOM reorder modal (e.g. "reorder-modal"). When set, the Reorder button opens that dialog locally via the BulkSelectScope hook BEFORE pushing on_open_reorder, so the modal appears instantly without waiting for the LV round-trip. Pair with <.reorder_modal> (which sets keep_in_dom={true} on its inner <.modal>). Omit to fall back to the round-trip-open flow used by conditionally-rendered modals. Defaults to nil.

Slots

  • leading - Content rendered on the left of the toolbar before the action buttons. Common use: tuck a sort selector in here so the toolbar reads as one widget.

bulk_select_cell(assigns)

Per-row checkbox cell. The value is captured into the selection set when checked; it's the identifier the server receives in {uuids: [...]} when an action fires.

Attributes

  • value (:string) (required)
  • class (:string) - Defaults to "w-8".

bulk_select_header_cell(assigns)

Header checkbox cell — drop-in replacement for <.table_default_header_cell> in the bulk-select column. The BulkSelectScope hook drives its checked / indeterminate state based on the current selection.

Attributes

  • id (:string) (required)
  • aria_label (:string) - Defaults to "Toggle select all".
  • class (:string) - Defaults to "w-8".

bulk_select_scope(assigns)

Opens a bulk-select scope. Everything inside this wrapper participates in the same selection set.

Attributes

  • id (:string) (required)
  • total_count (:integer) (required)
  • class (:string) - Defaults to "".

Slots

  • inner_block (required)