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.
- Update lib/phoenix_live_view/helpers.ex
- Update lib/phoenix_live_view.ex
- Update guides/server/uploads.md
Demo
- 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.
Build 🚀🚀🚀
$ mix phx.new gallery --live
$ cd gallery
$ mix ecto.create
- 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},
- 
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
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
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
<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
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
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
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
<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
       # Import LiveView helpers (live_render, live_component, live_patch, etc)
       import Phoenix.LiveView.Helpers
+      import GalleryWeb.LiveHelpers
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
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("×"), 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
     pipe_through :browser
     live "/", PageLive, :index
+    live "/pictures", PictureLive.Index, :index
+    live "/pictures/new", PictureLive.Index, :new
   end
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)$"
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)
Run!!!
$ mkdir priv/static/uploads
$ mix ecto.migrate
$ mix phx.server
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]
- Thanks!
 
 
              
 
    
Top comments (1)
The config to ignore the uploads directory is what I was looking for. Thanks!