defmodule Corex.FileUploadLive do @moduledoc ~S''' LiveView uploads wrapper that shares [`Corex.FileUpload`](Corex.FileUpload.html) layout tokens (`data-scope` / `data-part`) and styling. Use after [`allow_upload/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3). Pass `upload={@uploads.name}` and `field` matching the atom given to `allow_upload`. Renders [`live_file_input`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#live_file_input/1) and `phx-drop-target`; **no** Zag `FileUpload` hook. Do not combine this component with `<.file_upload>` on the same file control. Forms must bind [`phx-change`](https://hexdocs.pm/phoenix_live_view/uploads.html) (and typically `phx-submit`) as in the [uploads guide](https://hexdocs.pm/phoenix_live_view/uploads.html). For shared form patterns, see the [Forms](forms.html) guide. ## Anatomy ### Minimal ```heex
<.file_upload_live upload={@uploads.document} field={:document} class="file-upload"> <:close> <.heroicon name="hero-x-mark" />
``` ### With label ```heex
<.file_upload_live upload={@uploads.document} field={:document} class="file-upload"> <:label>Files <:close> <.heroicon name="hero-x-mark" />
``` ### Custom slots ```heex
<.file_upload_live upload={@uploads.document} field={:document} class="file-upload"> <:dropzone> Custom dropzone <:open> Custom trigger <:close> <.heroicon name="hero-x-mark" />
``` ### Form with submit ```heex
<.file_upload_live upload={@uploads.attachment} field={:attachment} class="file-upload"> <:label>Attachment <:close> <.heroicon name="hero-x-mark" /> <.action type="submit" class="button button--accent">Submit
``` ```elixir def mount(_params, _session, socket) do {:ok, allow_upload(socket, :attachment, accept: ~W(.jpg .jpeg .png .pdf .txt), max_entries: 3, max_file_size: 8_000_000 )} end def handle_event("validate", _params, socket), do: {:noreply, socket} def handle_event("save", _params, socket) do _results = consume_uploaded_entries(socket, :attachment, fn %{path: path}, entry -> File.rm!(path) {:ok, entry.client_name} end) {:noreply, socket} end def handle_event("file_upload_live_cancel", params, socket) do %{"ref" => ref, "upload_field" => field} = params {:noreply, cancel_upload(socket, String.to_existing_atom(field), ref)} end ``` ### LiveView setup ```elixir def mount(_params, _session, socket) do {:ok, allow_upload(socket, :document, accept: ~w(.jpg .jpeg .png .pdf), max_entries: 3, max_file_size: 8_000_000 )} end def handle_event("file_upload_live_cancel", %{"ref" => ref, "upload_field" => field}, socket) do {:noreply, cancel_upload(socket, String.to_existing_atom(field), ref)} end ``` The `field` atom must match the name passed to `allow_upload/3`. Implement `file_upload_live_cancel` so remove-entry works; optional `cancel_event` on the component overrides the event name. ''' @doc type: :component use Phoenix.Component alias Corex.FileUpload.Translation alias Phoenix.LiveView.UploadConfig alias Phoenix.LiveView.UploadEntry attr(:upload, UploadConfig, required: true, doc: "Upload config from `allow_upload/3` on the LiveView socket" ) attr(:field, :atom, required: true, doc: "Upload name passed to `allow_upload` (for cancel events)" ) attr(:id, :string, default: nil, doc: "Stable prefix for internal ids; defaults to a generated id" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "Text direction (ltr or rtl)" ) attr(:invalid, :boolean, default: false, doc: "Whether the file upload is invalid") attr(:disabled, :boolean, default: false, doc: "Whether the file upload is disabled") attr(:cancel_event, :string, default: "file_upload_live_cancel", doc: "LiveView `handle_event` name; receives ref and upload_field" ) attr(:translation, Corex.FileUpload.Translation, default: nil, doc: "Same translatable strings as `<.file_upload>`" ) attr(:rest, :global) slot(:label, required: false, doc: "Label above the dropzone") slot(:dropzone, required: false, doc: "Custom dropzone content; defaults to translation dropzone text" ) slot(:open, required: false, doc: "Custom open-picker trigger; defaults to translation open text" ) slot(:close, required: true, doc: "Remove control for each upload entry") slot(:error, required: false, doc: "Error message content; receives the message as slot argument" ) do attr(:class, :string, required: false) end def file_upload_live(assigns) do translation = Translation.resolve(Map.get(assigns, :translation)) assigns = assigns |> assign_new(:id, fn -> "file-upload-live-#{System.unique_integer([:positive])}" end) |> assign(:translation, translation) ~H"""
<.live_file_input upload={@upload} disabled={@disabled} data-scope="file-upload" data-part="hidden-input" />
<%= for err <- upload_errors(@upload) do %>
<%= if @error != [] do %> {render_slot(@error, upload_err_text(err))} <% else %> {upload_err_text(err)} <% end %>
<% end %>
""" end defp image_entry?(%UploadEntry{} = entry) do case entry.client_type do nil -> false type when is_binary(type) -> String.starts_with?(type, "image/") _ -> false end end defp format_bytes(nil), do: "" defp format_bytes(n) when is_integer(n) and n >= 1_048_576 do "#{Float.round(n / 1_048_576, 1)} MB" end defp format_bytes(n) when is_integer(n) and n >= 1024 do "#{Float.round(n / 1024, 1)} KB" end defp format_bytes(n) when is_integer(n), do: Integer.to_string(n) <> " B" defp upload_err_text(:too_many_files), do: Corex.Gettext.gettext("Too many files") defp upload_err_text(:too_large), do: Corex.Gettext.gettext("File is too large") defp upload_err_text(:not_accepted), do: Corex.Gettext.gettext("Unacceptable file type") defp upload_err_text(:external_client_failure), do: Corex.Gettext.gettext("Upload failed") defp upload_err_text({:writer_failure, _reason}), do: Corex.Gettext.gettext("Upload failed") defp upload_err_text(other), do: inspect(other) end