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!