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!