LiveFileInput in Production ベース+validation

# はじめに
本記事は、プロダクションでLiveViewのLiveVileInputを使用した際にハマったポイントや実装例の備忘録になります

内容としては以下を予定しています

- ベース <- 本記事
- リサイズ処理と独自サムネイル
- GCSアップロード
- テスト

# Project作成 & phx.gen.live
最初にプロジェクトの作成と phx.gen.liveでベースとなるコードを作成します
```
mix phx.new live_upload
cd live_upload
mix ecto.create
mix phx.gen.live Blog Article articles title:string body:string eyecatch:string
mix ecto.migrate
```

```lib/live_upload_web/router.ex
defmodule LiveUploadWeb.Router do
  use LiveUploadWeb, :router

  scope "/", LiveUploadWeb do
    pipe_through :browser

    get "/", PageController, :index
    live "/articles", ArticleLive.Index, :index
    live "/articles/new", ArticleLive.Index, :new
    live "/articles/:id/edit", ArticleLive.Index, :edit

    live "/articles/:id", ArticleLive.Show, :show
    live "/articles/:id/show/edit", ArticleLive.Show, :edit
  end
end
```

# topアクセス時のリダイレクト
ちょっとした小ネタとして、ログイン時とかに / にアクセスしたときにログイン後ページとかにリダイレクト処理を入れたいときは page_controller.exに書いておくとよさそうです

```lib/live_upload_web/controllers/page_controller.ex
defmodule LiveUploadWeb.PageController do
  use LiveUploadWeb, :controller

  def index(conn, _params) do
    redirect(conn, to: Routes.article_index_path(LiveUploadWeb.Endpoint, :index))
  end
end
```

# live_file_inputを使えるようにする
では live_file_inputをシステムに組み込んでいきましょう
大体前に書いた記事の通りに進めていきます

https://qiita.com/the_haigo/items/6ad00175bb3d9c15b3ee


allow_upload/3を実行しassignsにアップロード関連のステートを追加します
```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  use LiveUploadWeb, :live_component

  alias LiveUpload.Blog

  @impl true
  def update(%{article: article} = assigns, socket) do
    changeset = Blog.change_article(article)

    {:ok,
     socket
     |> assign(assigns)
     |> allow_upload(:eyecatch, accept: :any) #追加
     |> assign(:changeset, changeset)}
  end
  ...
end
```
実際にテンプレートで使用するときは
`<%= live_file_input @uploads.eyecatch %> `
とあるように、fはいれず先程指定した値のみ指定します
もとのformのeyecatchはhidden_inputにしておきます
```leex:lib/live_upload_web/live/article_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form>
    ...  
    <%= label f, :eyecatch %>
    <%= live_file_input @uploads.eyecatch %> <!--　追加 --->
    <%= hidden_input f, :eyecatch %>　<!--　変更 --->
    <%= error_tag f, :eyecatch %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```
# サムネイルを表示する
ファイルを選択すると eyecatch.entriesにファイルのデータが入るので、for文を使って表示します
`live_img_preview`で画像を`entry.client_name`でファイル名を表示しています
```leex:lib/live_upload_web/live/article_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form>
    ...　
    <%= label f, :eyecatch %>
    <%= live_file_input @uploads.eyecatch %>
    <!-- 以下追加 -->
    <%= for entry <- @uploads.eyecatch.entries do %>
      <figure>
        <%= live_img_preview entry %>
        <figcaption><%= entry.client_name %></figcaption>
      </figure>
    <% end %>
    <!-- 追加ここまで -->
    <%= hidden_input f, :eyecatch %>
    <%= error_tag f, :eyecatch %>

    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```

# 保存する
アップロードとサムネイル表示はできたので実際にDBに保存しましょう
今回はローカル環境にアップロードします

最初に初期値 placeholderをeyecatchに指定します
```lib/live_upload/blog/article.ex
defmodule LiveUpload.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset

  schema "articles" do
    field :body, :string
    field :eyecatch, :string, default: "placeholder" # default値を追加
    field :title, :string

    timestamps()
  end
  ...
end
```
formがsubmitされたときに発火する"save"イベントで、実際に保存する値にアップロードした画像のファイルパスを追加します
主に以下の処理を行います

1. 未アップロード(entriesが空)
・ placefolderまたは空文字だった場合 Ecto.changesetのバリデーションで弾かれるようにnilで置き換える
・ editの場合はファイルパスが入っているので変更なく保存する
2 アップロードしたファイルがあった場合
・ LiveViewのconsume_uploaded_entriesでアップロードしたファイルのデータを取得する
・ ファイル名を生成する(UUID+extension)
・ ファイルをコピーし、最終的なファイルパスを返す

```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  use LiveUploadWeb, :live_component

  alias LiveUpload.Blog

  ...
  def handle_event("save", %{"article" => article_params}, socket) do
    article_params =
      case Enum.empty?(socket.assigns.uploads.eyecatch.entries) do
        true ->
          validate_required(article_params)

        false ->
          {:ok, uploaded_file_path} = consume_uploaded(socket)
          Map.put(article_params, "eyecatch", uploaded_file_path)
      end

    save_article(socket, socket.assigns.action, article_params)
  end

  def validate_required(params) do
    case params["eyecatch"] in ["placeholder", ""] do
      true -> Map.put(params, "eyecatch", nil)
      false -> params
    end
  end

  def consume_uploaded(socket) do
    image =
      consume_uploaded_entries(socket, :eyecatch, fn %{path: path}, entry ->
        upload_local(path, entry)
      end)
      |> List.first()

    {:ok, image}
  end

  def upload_local(path, entry) do
    dest = Path.join([:code.priv_dir(:live_upload), "uploads", generate_filemae(entry)])
    File.cp!(path, dest)
    "/uploads/#{Path.basename(dest)}"
  end

  def generate_filemae(entry) do
    extension = MIME.extensions(entry.client_type) |> List.first()
    entry.uuid <> ".#{extension}"
  end
  ...
end
```
## entryのデータ

```elixir
%Phoenix.LiveView.UploadEntry{
  cancelled?: false,
  client_last_modified: nil,
  client_name: "DSC_0035.jpg",
  client_size: 1693887,
  client_type: "image/jpeg",
  done?: true,
  preflighted?: true,
  progress: 100,
  ref: "0",
  upload_config: :eyecatch,
  upload_ref: "phx-FvJI9I_nGsgH5gQG",
  uuid: "382df2e4-276e-4ff0-843a-d83228afc3ff",
  valid?: true
}
```
# 表示する
アップロードした画像を実際に表示させていきましょう

最初にアップロード先のフォルダを作ります
```
mkdir priv/uploads
```

gitに含めてほしくないので ignoreに追加します
```.gitignore
# Ignore assets that are produced by build tools.
/priv/static/assets/
/priv/uploads/ # 追加
```
/uploadsでアクセスできるようにendpointを設定します
１つ目に含まないのは、live_updateが走ってしまうため、新しくendpointを作っています

```lib/live_upload_web/endpoint.ex
defmodule LiveUploadWeb.Endpoint do
  ...
  plug Plug.Static,
    at: "/",
    from: :live_upload,
    gzip: false,
    only: ~w(assets fonts images favicon.ico robots.txt)
  # 以下追加
  plug Plug.Static,
    at: "/",
    from: {:live_upload, "priv"},
    gzip: false,
    only: ~w(uploads)
 ...
end
```
live_helpersに表示する関数や表示フラグの関数を実装しておけば
use LiveUploadWeb, :live_viewが記述してある箇所で使うことができます


```lib/live_upload_web/live/live_helpers.ex
defmodule LiveUploadWeb.LiveHelpers do
  import Phoenix.LiveView
  import Phoenix.LiveView.Helpers

  alias Phoenix.LiveView.JS
  alias LiveUploadWeb.Router.Helpers, as: Routes #追加
  ...

  def img_url(socket, path) do
    Routes.static_path(socket, path)
  end

  def show_uploaded_image?(data, entries) do
    data != "placeholder" && Enum.empty?(entries)
  end
end
```
さっき実装したimg_urlで表示します
```leex:lib/live_upload_web/live/article_live/index.html.heex
<h1>Listing Articles</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th>Eyecatch</th>

      <th></th>
    </tr>
  </thead>
  <tbody id="articles">
    <%= for article <- @articles do %>
      <tr id={"article-#{article.id}"}>
        <!--- 変更 --->
        <td><img src={img_url(@socket, article.eyecatch)} width="150" height="80"/></td>
        <td><%= article.title %></td>
        <td><%= article.body %></td>

        <td>
          <span><%= live_redirect "Show", to: Routes.article_show_path(@socket, :show, article) %></span>
          <span><%= live_patch "Edit", to: Routes.article_index_path(@socket, :edit, article) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: article.id, data: [confirm: "Are you sure?"] %></span>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<span><%= live_patch "New Article", to: Routes.article_index_path(@socket, :new) %></span>
```

![スクリーンショット 2022-05-25 16.53.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/10112cfd-f45a-bf3d-c465-dc453fa87d76.png)
無事表示できました！

ヘルパーに追加した表示フラグはedit時に使用します
差し替える画像がアップロードされたら非表示になるようしています

```leex:lib/live_upload_web/live/article_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form>
    ...
    <%= live_file_input @uploads.eyecatch %>
    <%= for entry <- @uploads.eyecatch.entries do %>
      <figure>
        <%= live_img_preview entry %>
        <figcaption><%= entry.client_name %></figcaption>
      </figure>
    <% end %>
    <!--- 以下追加 --->
    <%= if show_uploaded_image?(f.data.eyecatch, @uploads.eyecatch.entries) do %>
      <figure>
        <img src={img_url(@socket, f.data.eyecatch)}>      
        <figcaption><%= Path.basename(f.data.eyecatch) %></figcaption>
      </figure>
    <% end %>
    <!--- 追加ここまで --->
    <%= hidden_input f, :eyecatch %>
    <%= error_tag f, :eyecatch %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```
## fの中身

```elixir
%Phoenix.HTML.Form{
  action: "#",
  data: %LiveUpload.Blog.Article{
    __meta__: #Ecto.Schema.Metadata<:loaded, "articles">,
    body: "test",
    eyecatch: "/uploads/live_view_upload-1653463454-562604693383223-3",
    id: 1,
    inserted_at: ~N[2022-05-25 07:24:15],
    title: "test",
    updated_at: ~N[2022-05-25 07:24:15]
  },
  errors: [], 
  hidden: [id: 1],
  id: "article-form",
  impl: Phoenix.HTML.FormData.Ecto.Changeset,
  index: nil,
  name: "article",
  options: [
    method: "put",
    id: "article-form", 
    "phx-change": "validate",
    "phx-submit": "save",
    "phx-target": %Phoenix.LiveComponent.CID{cid: 1}
  ],
  params: %{},
  source: #Ecto.Changeset<action: nil, changes: %{}, errors: [],
   data: #LiveUpload.Blog.Article<>, valid?: true>
}

```

![f09961e277dedfdfa163d1d83bcfbbf4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/120dab76-f323-d77d-35e0-fa974f4181e1.gif)

これで　ファイルアップロード、保存・更新ができるようになりました

# バリデーション
次にバリデーション周りを実装していきましょう
live_file_inputでは ファイル数、ファイル容量、accept属性のバリデーションがデフォルトで実装されています

今回はファイル容量とaccept属性を行います

## ファイル容量を制限する
最初にファイル容量を制限してみましょう
defaultは8MBですので1MBに変更してみましょう
```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  ...
  @impl true
  def update(%{article: article} = assigns, socket) do
    changeset = Blog.change_article(article)

    {:ok,
     socket
     |> assign(assigns)
     |> allow_upload(
       :eyecatch,
       accept: :any,
       max_file_size: 1_000_000 #追加
     )
     |> assign(:changeset, changeset)}
  end
  ...
end
```

## アップロードを許可するファイルを制限する
次にacceptをanyから jpg,pngに変更します
```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  ...
  @impl true
  def update(%{article: article} = assigns, socket) do
    changeset = Blog.change_article(article)

    {:ok,
     socket
     |> assign(assigns)
     |> allow_upload(
       :eyecatch,
       accept: ~w(.jpeg .jpg .png), #変更
       max_file_size: 1_000_000
     )
     |> assign(:changeset, changeset)}
  end
  ...
end
```

## エラーメッセージを表示させる
エラーメッセージを表示する際にはドキュメントに記載されている`upload_errors`でエラーを取得できるのでそちらをfor文で表示していきます

https://hexdocs.pm/phoenix_live_view/uploads.html#upload-entries
```leex:lib/live_upload_web/live/article_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form>
    ...
    <label>
      <p class="button button-outline">ファイルを選択</p>
      <span style="visibility:hidden;">
        <%= live_file_input @uploads.eyecatch %>
      </span>
    </label>
    <%= for entry <- @uploads.eyecatch.entries do %>
      <%= if entry.valid? do %> <!--- エラーの場合は表示しないように変更 --->
        <figure>
          <%= live_img_preview entry %>
          <figcaption><%= entry.client_name %></figcaption>
        </figure>
      <% end %>
      <!--- 以下追加 --->
      <%= for err <- upload_errors(@uploads.eyecatch, entry) do %>
        <span class="invalid-feedback"><%= error_to_string(err) %></span>
      <% end %>
      <!--- 追加ここまで --->
    <% end %>
    <%= if show_uploaded_image?(f.data.eyecatch, @uploads.eyecatch.entries) do %>
      <figure>
        <img src={img_url(@socket, f.data.eyecatch)}>      
        <figcaption><%= Path.basename(f.data.eyecatch) %></figcaption>
      </figure>
    <% end %>

    <%= hidden_input f, :eyecatch %>
    <%= error_tag f, :eyecatch %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```
注意しないといけないのがエラーはatomで表示できないので、atomをエラーメッセージに変換する関数をerror_helpersに実装します

```lib/live_upload_web/views/error_helpers.ex
defmodule LiveUploadWeb.ErrorHelpers do
  ...  
  def error_to_string(:too_large), do: "Too large"
  def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
end
```
![スクリーンショット 2022-05-26 0.41.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/fccda310-dee3-e5d3-772b-c4a65c6a343d.png)
実際に１MBを超えるファイルをアップロードするとエラーメッセージが表示されました

これで完成！

## ファイルアップロード失敗しても saveイベント発火させたい
とおもったらこのような要望が・・・
「ファイルアップロード失敗したらsubmitできないのはUI上良くないのでなんとかして(意訳)」
まぁそうですよね

どうやら uploads.hogehoge.errrosが空じゃないとsubmitできないようで
cancel_uploadで初期化する必要があるらしい
だがそうするとエラー内容まで消えてしまう

ということで以下のようにしました
- エラーの格納するステートを追加 - ①
- validateイベント時に以下を行う
  - errorがなかったら①にnilをアサイン
  - errorがあったらcancel_uploadを行いエラー内容を①にアサイン

```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  use LiveUploadWeb, :live_component

  alias LiveUpload.Blog

  @impl true
  def update(%{article: article} = assigns, socket) do
    changeset = Blog.change_article(article)

    {:ok,
     socket
     |> assign(assigns)
     |> allow_upload(
       :eyecatch,
       accept: ~w(.jpeg .jpg .png),
       max_file_size: 1_000_000
     )
     |> assign(:upload_error, nil) # エラーメッセージ格納用
     |> assign(:changeset, changeset)}
  end

  @impl true
  def handle_event("validate", %{"article" => article_params}, socket) do
    changeset =
      socket.assigns.article
      |> Blog.change_article(article_params)
      |> Map.put(:action, :validate)

    # 以下追加
    socket =
      socket
      |> assign_upload_error(socket.assigns.uploads) 
      |> assign(:changeset, changeset)

    {:noreply, socket}
    # 追加ここまで
  end

  ... 
  # エラーのアサインと初期化処理実装
  def assign_upload_error(socket, %{eyecatch: %{errors: errors}}) do
    case Enum.empty?(errors) do
      true ->
        assign(socket, :upload_error, nil)

      false ->
        Enum.reduce(errors, socket, fn {ref, error}, acc ->
          acc
          |> cancel_upload(:eyecatch, ref)
          |> assign(:upload_error, error)
        end)
    end
  end
end
```
エラーメッセージを表示させるで追加した部分を消して、forの外側にエラーを表示する処理と追加します
おまけで
デフォルトの`input type="file"`が嫌だと言う場合はlabelタグで囲んでhiddenで非表示し、button以外のタグを置けば好きなデザインにできます

```leex:lib/live_upload_web/live/article_live/form_component.html.heex
<div>
  <h2><%= @title %></h2>

  <.form>
    ...
    <!--- 以下変更 --->
    <label>
      <p class="button button-outline">ファイルを選択</p>
      <span style="visibility:hidden;"><%= live_file_input @uploads.eyecatch %></span>
    </label>
    <!--- 変更ここまで --->
    <%= for entry <- @uploads.eyecatch.entries do %>
      <%= if entry.valid? do %>
      <figure>
        <%= live_img_preview entry %>
        <figcaption><%= entry.client_name %></figcaption>
      </figure>
      <% end %>
    <% end %>
    <!--- 以下追加 --->
    <%= if !is_nil(@upload_error) do %>
      <span class="invalid-feedback"><%= error_to_string(@upload_error) %></span>
    <% end %>
    <!--- 追加ここまで --->
    <%= if show_uploaded_image?(f.data.eyecatch, @uploads.eyecatch.entries) do %>
      <figure>
        <img src={img_url(@socket, f.data.eyecatch)}>      
        <figcaption><%= Path.basename(f.data.eyecatch) %></figcaption>
      </figure>
    <% end %>

    <%= hidden_input f, :eyecatch %>
    <%= error_tag f, :eyecatch %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>
```

## saveが発火したときにはファイルアップロードのエラーは消えてほしい
これもそうですね、
changesetのエラーとuploadのエラー両方が表示されてるのは良くないので、手を加えます
主に新規登録時に起こりうるので、`save_article(socket, :new, article_params)`
に実装します

save実行時に upload_errorがあったらnilに書き換えるようにすればokです

```lib/live_upload_web/live/article_live/form_component.ex
defmodule LiveUploadWeb.ArticleLive.FormComponent do
  ...
  defp save_article(socket, :new, article_params) do
    # 以下追加
    socket =
      case is_nil(socket.assigns.upload_error) do
        true -> socket
        false -> assign(socket, :upload_error, nil)
      end
    # 追加ここまで
    case Blog.create_article(article_params) do
      {:ok, _article} ->
        {:noreply,
         socket
         |> put_flash(:info, "Article created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
end
```
最終的にこんな感じになりました
![5002364b86b84f253d5892163887c715.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/b0bc71d9-b57d-ad30-e424-eb69b9f59a63.gif)


# 最後に
いかがでしたでしょうか？
くっそめんどくさいですね！
実際にはこれにいくつか別の処理も入っている
＋アップロードするカラムも複数なのでもっと面倒でした _(┐「ε:)_
carrier_waveは偉大だ・・・・

# コード
https://github.com/thehaigo/live_upload
https://github.com/thehaigo/live_upload/tree/base

# 参考ページ
https://hexdocs.pm/phoenix_live_view/uploads.html
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#cancel_upload/3
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#consume_uploaded_entries/3
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#live_file_input/2
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#live_img_preview/2
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#upload_errors/2
https://qiita.com/the_haigo/items/6ad00175bb3d9c15b3ee