PhxMediaLibrary.LiveUpload (PhxMediaLibrary v0.6.1)

Copy Markdown View Source

Provides upload helpers for Phoenix LiveViews.

use PhxMediaLibrary.LiveUpload injects convenience functions into your LiveView for managing media uploads backed by PhxMediaLibrary.

Usage

defmodule MyAppWeb.PostLive.Edit do
  use MyAppWeb, :live_view
  use PhxMediaLibrary.LiveUpload

  def mount(%{"id" => id}, _session, socket) do
    post = Posts.get_post!(id)

    {:ok,
     socket
     |> assign(:post, post)
     |> allow_media_upload(:images, model: post, collection: :images)
     |> stream_existing_media(:media, post, :images)}
  end

  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

  def handle_event("save_media", _params, socket) do
    case consume_media(socket, :images, socket.assigns.post, :images, notify: self()) do
      {:ok, media_items} ->
        {:noreply,
         socket
         |> stream_media_items(:media, media_items)
         |> put_flash(:info, "Uploaded #{length(media_items)} file(s)")}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Upload failed: #{inspect(reason)}")}
    end
  end

  def handle_event("delete_media", %{"id" => id}, socket) do
    case delete_media_by_id(id, notify: self()) do
      :ok -> {:noreply, stream_delete_by_dom_id(socket, :media, "media-#{id}")}
      {:error, reason} -> {:noreply, put_flash(socket, :error, inspect(reason))}
    end
  end

  def handle_event("cancel_upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :images, ref)}
  end

  # Handle media lifecycle notifications
  def handle_info({:media_added, media_items}, socket) do
    # React to newly added media (e.g. update counters, trigger side-effects)
    {:noreply, assign(socket, :media_count, length(media_items))}
  end

  def handle_info({:media_removed, _media}, socket) do
    {:noreply, socket}
  end

  def handle_info({:media_error, _reason}, socket) do
    {:noreply, socket}
  end
end

How It Works

The module provides three categories of helpers:

1. Setup Helpers

2. Consumption Helpers

3. Management Helpers

  • stream_media_items/3 — inserts newly created media items into an existing stream so the UI updates without a full reload.

  • delete_media_by_id/1 — fetches a media record by ID and deletes it along with its files.

  • media_upload_errors/2 — returns human-readable error strings for an upload's errors.

  • media_entry_errors/2 — returns human-readable error strings for a specific upload entry.

Event Notifications

Both consume_media/5 and delete_media_by_id/2 accept a :notify option. When set to a pid (e.g. self()), lifecycle messages are sent to that process:

  • {:media_added, [Media.t()]} — after successful upload consumption
  • {:media_error, reason} — when upload consumption fails
  • {:media_removed, Media.t()} — after successful media deletion

This allows parent LiveViews (or any process) to react to media lifecycle events via handle_info/2.

Summary

Functions

Allows a media upload on the socket with collection-aware defaults.

Consumes completed uploads and persists them as media via PhxMediaLibrary.

Deletes a media item by its ID and removes its files from storage.

Returns whether the upload has any entries (pending or completed).

Returns whether an upload entry is an image based on its client MIME type.

Returns human-readable error strings for a specific upload entry.

Returns human-readable error strings for an upload.

Loads existing media for a model/collection and streams them.

Inserts newly created media items into an existing stream.

Translates an upload error atom into a human-readable string.

Functions

allow_media_upload(socket, name, opts)

@spec allow_media_upload(Phoenix.LiveView.Socket.t(), atom(), keyword()) ::
  Phoenix.LiveView.Socket.t()

Allows a media upload on the socket with collection-aware defaults.

Wraps Phoenix.LiveView.allow_upload/3, automatically deriving the :accept, :max_entries, and :max_file_size options from the collection configuration defined on the model's schema.

Options

All options are passed through to allow_upload/3. The following are specific to allow_media_upload/3:

  • :model — (required) the Ecto struct that owns the media. Used to look up collection configuration.
  • :collection — (required) the collection atom to upload into.
  • :accept — override the accept list. When not provided, it is derived from the collection's :accepts MIME types. Falls back to :any when the collection has no type restrictions.
  • :max_entries — override maximum entries. Derived from the collection's :max_files or :single_file setting.
  • :max_file_size — maximum file size in bytes. Defaults to 10 MB.

Any other option (e.g. :auto_upload, :chunk_size, :progress) is forwarded to allow_upload/3 unchanged.

Examples

# Derive everything from collection config
allow_media_upload(socket, :images, model: post, collection: :images)

# Override accept and max entries
allow_media_upload(socket, :avatar,
  model: user,
  collection: :avatar,
  accept: ~w(.jpg .png),
  max_entries: 1
)

consume_media(socket, upload_name, model, collection_name, opts \\ [])

@spec consume_media(
  Phoenix.LiveView.Socket.t(),
  atom(),
  Ecto.Schema.t(),
  atom(),
  keyword()
) :: {:ok, [PhxMediaLibrary.Media.t()]} | {:error, term()}

Consumes completed uploads and persists them as media via PhxMediaLibrary.

Wraps Phoenix.LiveView.consume_uploaded_entries/3. For each completed entry, it calls PhxMediaLibrary.add/2 |> PhxMediaLibrary.to_collection/2 to store the file and create the database record.

Returns {:ok, [Media.t()]} with all successfully created media items, or {:error, reason} if any entry fails (already-consumed entries are still persisted — the error is for the first failure encountered).

Options

  • :disk — override the storage disk for these uploads.
  • :custom_properties — a map of custom properties to attach to each media item.
  • :responsive — whether to generate responsive images. Defaults to false.
  • :notify — a pid to send lifecycle messages to. When set, sends {:media_added, media_items} on success or {:media_error, reason} on failure.

Examples

{:ok, media_items} = consume_media(socket, :images, post, :images)

{:ok, media_items} =
  consume_media(socket, :images, post, :images,
    custom_properties: %{"uploaded_by" => user.id},
    responsive: true,
    notify: self()
  )

delete_media_by_id(id, opts \\ [])

@spec delete_media_by_id(
  String.t(),
  keyword()
) :: :ok | {:error, term()}

Deletes a media item by its ID and removes its files from storage.

Returns :ok on success, {:error, :not_found} if the media doesn't exist, or {:error, reason} on failure.

Options

  • :notify — a pid to send {:media_removed, media} to on success.

Examples

case delete_media_by_id(media_id) do
  :ok -> {:noreply, stream_delete_by_dom_id(socket, :media, "media-#{media_id}")}
  {:error, reason} -> {:noreply, put_flash(socket, :error, inspect(reason))}
end

# With notification
case delete_media_by_id(media_id, notify: self()) do
  :ok -> {:noreply, stream_delete_by_dom_id(socket, :media, "media-#{media_id}")}
  {:error, reason} -> {:noreply, put_flash(socket, :error, inspect(reason))}
end

has_upload_entries?(arg1)

@spec has_upload_entries?(Phoenix.LiveView.UploadConfig.t()) :: boolean()

Returns whether the upload has any entries (pending or completed).

Useful for conditionally showing/hiding UI elements.

Examples

<div :if={has_upload_entries?(@uploads.images)}>
  ...upload preview area...
</div>

image_entry?(arg1)

@spec image_entry?(Phoenix.LiveView.UploadEntry.t()) :: boolean()

Returns whether an upload entry is an image based on its client MIME type.

Useful for conditionally showing image previews vs. file icons.

Examples

<%= if image_entry?(entry) do %>
  <.live_img_preview entry={entry} />
<% else %>
  <.icon name="hero-document" />
<% end %>

media_entry_errors(upload_config, entry)

@spec media_entry_errors(
  Phoenix.LiveView.UploadConfig.t(),
  Phoenix.LiveView.UploadEntry.t()
) :: [
  String.t()
]

Returns human-readable error strings for a specific upload entry.

Translates the error atoms returned by Phoenix.Component.upload_errors/2 into user-friendly messages.

Examples

for entry <- @uploads.images.entries do
  for msg <- media_entry_errors(@uploads.images, entry) do
    ...
  end
end

media_upload_errors(arg1)

@spec media_upload_errors(Phoenix.LiveView.UploadConfig.t()) :: [String.t()]

Returns human-readable error strings for an upload.

Translates the error atoms returned by Phoenix.Component.upload_errors/1 into user-friendly messages.

Examples

for msg <- media_upload_errors(@uploads.images) do
  ...
end

stream_existing_media(socket, stream_name, model, collection_name)

@spec stream_existing_media(
  Phoenix.LiveView.Socket.t(),
  atom(),
  Ecto.Schema.t(),
  atom()
) :: Phoenix.LiveView.Socket.t()

Loads existing media for a model/collection and streams them.

This is a convenience that queries existing media and feeds them into a LiveView stream so they can be rendered alongside new uploads.

The stream is configured with a :dom_id function that prefixes each item's ID with "media-".

Examples

socket
|> stream_existing_media(:media, post, :images)

# Renders in template as @streams.media

stream_media_items(socket, stream_name, media_items)

Inserts newly created media items into an existing stream.

Use this after consume_media/5 to update the UI without reloading.

Examples

{:ok, media_items} = consume_media(socket, :images, post, :images)
socket = stream_media_items(socket, :media, media_items)

translate_upload_error(error)

@spec translate_upload_error(atom() | {atom(), term()}) :: String.t()

Translates an upload error atom into a human-readable string.

Override this function in your own module if you need custom messages or i18n support.

Examples

translate_upload_error(:too_large)
#=> "File is too large"