PhoenixKitWeb.Components.MediaGallery (phoenix_kit v3.5.0)

Copy Markdown View Source

A general-purpose LiveComponent for selecting, ordering, previewing and removing a set of images.

Generalizes the gallery + picker pattern so any PhoenixKit consumer can embed it instead of re-implementing the plumbing. The component manages an ordered list of file UUIDs and reports changes to the parent LiveView.

Usage

<.live_component
  module={PhoenixKitWeb.Components.MediaGallery}
  id="my-gallery"
  selected={@image_uuids}
  phoenix_kit_current_user={@current_user}
/>

# Receive changes in parent LiveView
def handle_info({PhoenixKitWeb.Components.MediaGallery, "my-gallery", {:changed, uuids}}, socket) do
  {:noreply, assign(socket, :image_uuids, uuids)}
end

Attrs

  • id — required; used for element IDs and change notifications
  • title — optional heading above the gallery
  • selected — ordered list of file UUIDs (current selection); default []
  • mode — :single or :multiple (default :multiple)
  • cols — grid columns for the thumbnail layout (default 4). Either an integer 1..6, or a string of Tailwind grid-column classes for a responsive grid (e.g. "grid-cols-4 lg:grid-cols-6 2xl:grid-cols-8"). Plumbed straight through to <.draggable_list>.
  • featured_first — when true, the first item in :selected renders a "Featured" badge in the bottom-left corner. Matches the phoenix_kit_posts post-creation convention where the first image is the featured one and drag-reordering changes the feature. Default false so existing consumers aren't surprised.
  • scope_folder_id — folder scope passed to the picker
  • group_by_folder — passed to the picker: true lists a scope that holds subfolders grouped by folder, under one heading each (see MediaSelectorModal); default false
  • folder_labels — passed to the picker: %{folder_uuid => name} shown in its folder headings instead of the stored folder names; default %{}
  • picker_size — the picker's size: :default or :full (the whole viewport, more files a page); default :default
  • phoenix_kit_current_user — required for upload in the picker
  • readonly — when true, hides the pick button, the tile menu, and DnD; preview (lightbox) still works; the server refuses pick, remove, trash and reorder events too; default false
  • removal_guard — the module asked before "Move to trash" (see "Tile actions"); nil takes the configured one, false goes without; default nil
  • phoenix_kit_current_scope — the scope of the person acting, handed to the removal guard; optional. A passed scope is used as given when the gallery acts, so the host keeps it current; without it one is built from phoenix_kit_current_user at the moment of each action
  • external_picker — when true, the "Add" tile asks the host to pick instead of opening the built-in MediaSelectorModal (which is then never rendered); see "External picker" below. Default false
  • max_count — integer upper bound on the number of selected images for :multiple mode; nil means unlimited. For :single mode the limit is always 1 (implied by mode). When the limit is reached, the "Add" tile is hidden entirely (not just disabled) and apply_selection refuses to exceed it.

Tile actions

Each tile has the preview (eye) button in its top-left corner and a ⋮ menu in its top-right corner. Both are shown on hover on a mouse screen and always on a touch screen (pointer-coarse), and on keyboard focus. The menu holds:

  • Remove from selection — takes the file out of the list (the file itself stays in the library) and reports {:changed, uuids}.
  • Move to trash — sends the file to the media trash, and takes it out of the list the same way. Shown only to a person who may change that file (Libraries.can?(scope, file, :edit): its uploader, an Owner/Admin, a media.view_all holder), and only for a file that is in the selection.

A readonly gallery shows the eye and no menu, and refuses both events.

How "Move to trash" reuses the media browser's removal guard

The gallery does not copy the browser's removal flow and does not make the host write one. It keeps its own small version of it, built from the same parts the browser uses: Storage.removal_plan/2 says whether the file would disappear, MediaBrowser.Removal asks the host's PhoenixKit.Modules.Storage.RemovalGuard module (the configured one, or the removal_guard attribute) where the file is in use, and the browser's RemovalModal draws the dialog inside the gallery, so it looks and reads the same (and Escape closes it). The dialog always asks first, as the menu sits next to the harmless "Remove from selection":

  • not in use — "Move 1 file to trash?" with the name; the confirmation trashes the file;
  • in use — the places are listed, nothing is removed, and when the guard offers a release (the "unpin everywhere" button) pressing it releases the file and asks again;
  • the guard failed — "Could not check the files", nothing is removed.

The confirmation re-reads the file, the plan and the guard's answer and trashes only what the dialog showed. On success the file is dropped from the selection and {:changed, uuids} is sent, so the host stores the shorter list as for any remove. The host needs no extra wiring for the dialog.

The host must persist that {:changed, uuids} (as for every change): a trashed file is gone from the gallery's list, but the host's stored list still names it until the host saves the shorter one. A guard that reads only saved state does not see the gallery's current selection as a use, and an abandoned form leaves a reference to a trashed file. Store the list when the message arrives, or make the guard count the unsaved selection as a use.

Change notifications — required host wiring (silent failure otherwise)

This is a LiveComponent, so it has no handle_info of its own: after any change (pick / remove / reorder) it sends a process message to the host LiveView via send/2:

{PhoenixKitWeb.Components.MediaGallery, id, {:changed, ordered_uuids}}

The host MUST handle this (see the Usage example above) and persist ordered_uuids — it is a controlled component: it renders whatever the host passes back as :selected. Forget the handler and the user's picks/reorders are silently dropped (no crash, no warning). Each host stores the selection differently (its own field/assoc), so there is intentionally no use ...Embed macro — the handling is yours to write.

External picker

With external_picker={true} the host supplies its own picker. Clicking "Add" sends the host LiveView (via send/2, like {:changed, …}):

{PhoenixKitWeb.Components.MediaGallery, id,
 {:open_picker, %{selected: uuids, mode: :single | :multiple, max_count: integer | nil}}}

The host hands the choice back with send_update/2. media_selected is the complete new selection, not an addition: it replaces selected, is limited by mode and max_count the same way as the built-in picker's, and the gallery then reports it as {:changed, uuids} like any other change. Not answering (the person cancelled) leaves the gallery as it was. Thumbnails, reordering, removal and the lightbox work as without the option; uploads are the host picker's business. A readonly gallery shows no "Add" tile and refuses open_picker on the server, so the host is never asked.

  • The host is not asked while the "Add" tile is hidden either — the selection is at max_count, or a :single gallery already holds a file — and a forged open_picker then is refused on the server.

  • Make the host's handle_info idempotent: every click sends a request, so a double click delivers two {:open_picker, …} messages.

  • Pass media_selected uuids without repeats; the gallery does not deduplicate the list.

  • Return image files only. The built-in picker is locked to images; the gallery does not filter what media_selected names, and a PDF or a video renders as a broken thumbnail.

  • Build the reply from the selection the host holds now, not from request.selected: that is a snapshot taken at the click, and a remove or reorder made while a non-modal host picker was open would be reverted by a reply built from it.

    <.live_component

    module={PhoenixKitWeb.Components.MediaGallery}
    id="my-gallery"
    selected={@image_uuids}
    external_picker={true}

    />

    def handle_info(

        {PhoenixKitWeb.Components.MediaGallery, "my-gallery", {:open_picker, request}},
        socket
      ) do
    # Open the host's picker; `request.selected` is the selection at the click.
    {:noreply, assign(socket, :picker_request, request)}

    end

    # Later, when the person confirms in the host's picker: send_update(PhoenixKitWeb.Components.MediaGallery, id: "my-gallery", media_selected: uuids)

Reorder event contract

The thumbnail grid uses the canonical <.draggable_list> primitive (PhoenixKitWeb.Components.Core.DraggableList), which fires the "reorder_images" event with payload %{"ordered_ids" => uuids}.

Because MediaGallery is a LiveComponent, the grid is rendered with target={"##{@id}"} so the SortableGrid hook routes the event via pushEventTo to this component's own handle_event/3 — not the host LiveView. Each gallery's id is unique, so multiple galleries on the same page never cross-deliver reorder events.

Summary

Functions

render(assigns)

Callback implementation for Phoenix.LiveComponent.render/1.