DEV Community

Cover image for Phoenix LiveView uploads
TORIFUKU Kaiou
TORIFUKU Kaiou

Posted on

Phoenix LiveView uploads

Hi!, it's nice to meet you.
This post is first one for me.

Introduction

Phoenix LiveView 0.15 supports live uploads.

I try it.

I was looking forward to the live uploads.
I took part in the review.
My suggestions(only comment!) are applied.

Demo

GitHub

  • The all source code is here.

Build 🚀🚀🚀

$ mix phx.new gallery --live
$ cd gallery
$ mix ecto.create
Enter fullscreen mode Exit fullscreen mode
  • change mix.exs
       {:phoenix_ecto, "~> 4.1"},
       {:ecto_sql, "~> 3.4"},
       {:postgrex, ">= 0.0.0"},
-      {:phoenix_live_view, "~> 0.14.6"},
+      {:phoenix_live_view, "~> 0.15.0", override: true},
       {:floki, ">= 0.27.0", only: :test},
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},
Enter fullscreen mode Exit fullscreen mode
  • mix deps.get
$ mix deps.get
Enter fullscreen mode Exit fullscreen mode
  • mix phx.gen.live
$ mix phx.gen.live Art Picture pictures message
Enter fullscreen mode Exit fullscreen mode
  • Then I add, remove, change the code.

priv/repo/migrations/20201122051151_create_pictures.exs

defmodule Gallery.Repo.Migrations.CreatePictures do
  use Ecto.Migration

  def change do
    create table(:pictures) do
      add :url, :string, null: false

      timestamps()
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/picture_live/form_component.ex

defmodule GalleryWeb.PictureLive.FormComponent do
  use GalleryWeb, :live_component

  alias Gallery.Art
  alias Gallery.Art.Picture

  @impl true
  def mount(socket) do
    {:ok, allow_upload(socket, :photo, accept: ~w(.png .jpg .jpeg))}
  end

  @impl true
  def update(%{picture: picture} = assigns, socket) do
    changeset = Art.change_picture(picture)

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:changeset, changeset)}
  end

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

  def handle_event("save", _params, socket) do
    picture = put_photo_url(socket, %Picture{})

    case Art.create_picture(picture, %{}, &consume_photo(socket, &1)) do
      {:ok, _picture} ->
        {:noreply,
         socket
         |> put_flash(:info, "Picture created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

  def handle_event("cancel-entry", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :photo, ref)}
  end

  defp ext(entry) do
    [ext | _] = MIME.extensions(entry.client_type)
    ext
  end

  defp put_photo_url(socket, %Picture{} = picture) do
    {completed, []} = uploaded_entries(socket, :photo)

    urls =
      for entry <- completed do
        Routes.static_path(socket, "/uploads/#{entry.uuid}.#{ext(entry)}")
      end

    url = Enum.at(urls, 0)

    %Picture{picture | url: url}
  end

  def consume_photo(socket, %Picture{} = picture) do
    consume_uploaded_entries(socket, :photo, fn meta, entry ->
      dest = Path.join("priv/static/uploads", "#{entry.uuid}.#{ext(entry)}")
      File.cp!(meta.path, dest)
    end)

    {:ok, picture}
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/picture_live/form_component.html.leex

<h2><%= @title %></h2>

<%= f = form_for @changeset, "#",
  id: "picture-form",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>

  <%= for {_ref, msg} <- @uploads.photo.errors do %>
    <p class="alert alert-danger"><%= Phoenix.Naming.humanize(msg) %></p>
  <% end %>

  <%= live_file_input @uploads.photo %>

  <%= for entry <- @uploads.photo.entries do %>
    <div class="row">
      <div class="column">
        <%= live_img_preview entry, height: 80 %>
      </div>
      <div class="column">
        <progress max="100" value="<%= entry.progress %>" />
      </div>
      <div class="column">
        <a href="#" phx-click="cancel-entry" phx-value-ref="<%= entry.ref %>"
           phx-target="<%= @myself %>">
          cancel
        </a>
      </div>
    </div>
  <% end %>

  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>
Enter fullscreen mode Exit fullscreen mode

lib/gallery/art.ex

defmodule Gallery.Art do
  @moduledoc """
  The Art context.
  """

  import Ecto.Query, warn: false
  alias Gallery.Repo

  alias Gallery.Art.Picture

  @doc """
  Returns the list of pictures.

  ## Examples

      iex> list_pictures()
      [%Picture{}, ...]

  """
  def list_pictures do
    Repo.all(
      from p in Picture,
        order_by: [desc: p.inserted_at]
    )
  end

  def create_picture(picture, attrs \\ %{}, after_save) do
    picture
    |> Picture.changeset(attrs)
    |> Repo.insert()
    |> after_save(after_save)
  end

  defp after_save({:ok, picture}, func) do
    {:ok, _picture} = func.(picture)
  end

  defp after_save(error, _func), do: error

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking picture changes.

  ## Examples

      iex> change_picture(picture)
      %Ecto.Changeset{data: %Picture{}}

  """
  def change_picture(%Picture{} = picture, attrs \\ %{}) do
    Picture.changeset(picture, attrs)
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery/art/picture.ex

defmodule Gallery.Art.Picture do
  use Ecto.Schema
  import Ecto.Changeset

  schema "pictures" do
    field :url, :string

    timestamps()
  end

  @doc false
  def changeset(picture, attrs) do
    picture
    |> cast(attrs, [:url])
    |> validate_required([:url])
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/picture_live/index.ex

defmodule GalleryWeb.PictureLive.Index do
  use GalleryWeb, :live_view

  alias Gallery.Art
  alias Gallery.Art.Picture

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, list_of_pictures: list_of_pictures())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Picture")
    |> assign(:picture, %Picture{})
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Pictures")
    |> assign(:picture, nil)
  end

  defp list_of_pictures do
    Art.list_pictures() |> Enum.chunk_every(3)
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/picture_live/index.html.leex

<h1>Listing Pictures</h1>

<%= if @live_action in [:new] do %>
  <%= live_modal @socket, GalleryWeb.PictureLive.FormComponent,
    id: @picture.id || :new,
    title: @page_title,
    action: @live_action,
    picture: @picture,
    return_to: Routes.picture_index_path(@socket, :index) %>
<% end %>

<span><%= live_patch "New Picture", to: Routes.picture_index_path(@socket, :new) %></span>

<table>
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody id="pictures">
    <%= for pictures <- @list_of_pictures do %>
      <tr>
        <%= for picture <- pictures do %>
          <td><img src="<%= picture.url %>" height="150" /></td>
        <% end %>
      </tr>
    <% end %>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web.ex

       # Import LiveView helpers (live_render, live_component, live_patch, etc)
       import Phoenix.LiveView.Helpers
+      import GalleryWeb.LiveHelpers
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/live_helpers.ex

defmodule GalleryWeb.LiveHelpers do
  import Phoenix.LiveView.Helpers

  @doc """
  Renders a component inside the `GalleryWeb.ModalComponent` component.

  The rendered modal receives a `:return_to` option to properly update
  the URL when the modal is closed.

  ## Examples

      <%= live_modal @socket, GalleryWeb.PictureLive.FormComponent,
        id: @picture.id || :new,
        action: @live_action,
        picture: @picture,
        return_to: Routes.picture_index_path(@socket, :index) %>
  """
  def live_modal(socket, component, opts) do
    path = Keyword.fetch!(opts, :return_to)
    modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
    live_component(socket, GalleryWeb.ModalComponent, modal_opts)
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/live/modal_component.ex

defmodule GalleryWeb.ModalComponent do
  use GalleryWeb, :live_component

  @impl true
  def render(assigns) do
    ~L"""
    <div id="<%= @id %>" class="phx-modal"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target="#<%= @id %>"
      phx-page-loading>

      <div class="phx-modal-content">
        <%= live_patch raw("&times;"), to: @return_to, class: "phx-modal-close" %>
        <%= live_component @socket, @component, @opts %>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/router.ex

     pipe_through :browser

     live "/", PageLive, :index
+    live "/pictures", PictureLive.Index, :index
+    live "/pictures/new", PictureLive.Index, :new
   end
Enter fullscreen mode Exit fullscreen mode

config/dev.exs

 config :gallery, GalleryWeb.Endpoint,
   live_reload: [
     patterns: [
-      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+      ~r"priv/static/[^uploads].*(js|css|png|jpeg|jpg|gif|svg)$",
       ~r"priv/gettext/.*(po)$",
       ~r"lib/gallery_web/(live|views)/.*(ex)$",
       ~r"lib/gallery_web/templates/.*(eex)$"
Enter fullscreen mode Exit fullscreen mode

lib/gallery_web/endpoint.ex

     at: "/",
     from: :gallery,
     gzip: false,
-    only: ~w(css fonts images js favicon.ico robots.txt)
+    only: ~w(css fonts images js favicon.ico robots.txt uploads)
Enter fullscreen mode Exit fullscreen mode

Run!!!

$ mkdir priv/static/uploads
$ mix ecto.migrate
$ mix phx.server
Enter fullscreen mode Exit fullscreen mode

Visit: http://localhost:4000/pictures

Refrences

Wrapping up!

  • Enjoy Elixir !!!
  • Please run the below snippet on your IEx.
iex> [87, 101, 32, 97, 114, 101, 32, 116, 104, 101, 32, 65, 108, 99, 104, 101, 109, 105, 115, 116, 115, 44, 32, 109, 121, 32, 102, 114, 105, 101, 110, 100, 115, 33]
Enter fullscreen mode Exit fullscreen mode
  • Thanks!

Top comments (1)

Collapse
 
miguelcoba profile image
Miguel Cobá

The config to ignore the uploads directory is what I was looking for. Thanks!