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!