Phoenix LiveView uploads
Tags: elixir, phoenix, liveview
Published: 2020-11-22 09:39:45

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

# Introduction
[Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) 0.15 supports [live uploads](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/CHANGELOG.md).
- [Phoenix LiveView 0.15 is out with the new uploads feature! I'm super excited to see what folks build with this. We put together a 30 minute deep dive that takes you step-by-step thru adding uploads to an app, including direct to S3 support](https://twitter.com/chris_mccord/status/1329836988485275652)

I try it.

I was looking forward to the live uploads.
I took part in the review.
My suggestions(only comment!) are applied.
- [Update lib/phoenix_live_view/helpers.ex](https://github.com/phoenixframework/phoenix_live_view/pull/1184/commits/1e6a856a3ccf55c17eb03190b4455ba6e1f8cb47)
- [Update lib/phoenix_live_view.ex](https://github.com/phoenixframework/phoenix_live_view/pull/1184/commits/46d870c9f0101dcdf721807053c9ca2bd0c9f766)
- [Update guides/server/uploads.md](https://github.com/phoenixframework/phoenix_live_view/pull/1184/commits/84a9c39fb0e90c5ecf81316e26e0e839bab6788a)

# Demo
- [https://elixir-is-beautiful.torifuku-kaiou.tokyo/pictures](https://elixir-is-beautiful.torifuku-kaiou.tokyo/pictures)
- I build demo site.
- Please feel free to use.
- This server specs are poor...
- One of these days I may stop.
  - Sorry...

# GitHub
- The all source code is [here](https://github.com/TORIFUKUKaiou/gallery).

# Build 🚀🚀🚀

```
$ mix phx.new gallery --live
$ cd gallery
$ mix ecto.create
```

- change `mix.exs`

```diff
       {: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},
```

- `mix deps.get`

```
$ mix deps.get
```

- `mix phx.gen.live`

```
$ mix phx.gen.live Art Picture pictures message
```

- Then I add, remove, change the code.

## priv/repo/migrations/20201122051151_create_pictures.exs

```elixir
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
```

## lib/gallery_web/live/picture_live/form_component.ex

- [allow_upload](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/upload.ex#L12)
- [cancel_upload](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/upload.ex#L71)
- [uploaded_entries](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/upload.ex#L183)
- [consume_uploaded_entries](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/upload.ex#L202)

```elixir
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
```

## lib/gallery_web/live/picture_live/form_component.html.leex
- [live_file_input](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/helpers.ex#L444)
- [live_img_preview](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/lib/phoenix_live_view/helpers.ex#L411)

```html
<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>
```

## lib/gallery/art.ex

```elixir
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
```

## lib/gallery/art/picture.ex

```elixir
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
```

## lib/gallery_web/live/picture_live/index.ex

```elixir
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
```

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

```html
<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>
```

## lib/gallery_web.ex

```diff
       # Import LiveView helpers (live_render, live_component, live_patch, etc)
       import Phoenix.LiveView.Helpers
+      import GalleryWeb.LiveHelpers
```

## lib/gallery_web/live/live_helpers.ex

```elixir
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
```

## lib/gallery_web/live/modal_component.ex

```elixir
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
```

## lib/gallery_web/router.ex 

```diff
     pipe_through :browser
 
     live "/", PageLive, :index
+    live "/pictures", PictureLive.Index, :index
+    live "/pictures/new", PictureLive.Index, :new
   end
```

## config/dev.exs

```diff
 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)$"
```

## lib/gallery_web/endpoint.ex

```diff
     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)
```

# Run!!!
```
$ mkdir priv/static/uploads
$ mix ecto.migrate
$ mix phx.server
```

Visit: http://localhost:4000/pictures

# Refrences
- [Phoenix LiveView Uploads Deep Dive](https://www.phoenixframework.org/blog/phoenix-live-view-upload-deep-dive)
- [phoenix_live_view/guides/server/uploads.md](https://github.com/phoenixframework/phoenix_live_view/blob/v0.15.0/guides/server/uploads.md)

# Wrapping up!
- Enjoy [Elixir](https://elixir-lang.org/) !!!
- Please run the below snippet on your IEx.

```elixir
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]
```

- Thanks!