@koyo-miyamura です!
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております
CSV ダウンロード
業務システムを作っているとよく「CSVエクスポート」を作ることありますよね。
さてこれを Phoenix LiveView でどう実装するんだ・・・?と思って色々調べて参考実装を作ったので紹介します。
完成画面
まず完成イメージから
ボタンを押すとファイルダウンロード( content-disposition
attachment
の挙動)
要件
※前回のページネーションの実装をベースにします
- エクスポートボタンを押すとすべての User データがダウンロードされる
- CSVのヘッダは
id, name, inserted_at, updated_at
である - 文字コードは UTF-8
- 改行コードは CRLF
実装
実装方針としては「CSVダウンロード用のエンドポイントを生やして LiveView の画面からリクエストできるようにする」とします。
ここでCSVダウンロード用のエンドポイントは LiveView ではなく単なる Phoenix コントローラーの実装であることに注意です!
まず下記モジュールを追加します。マップを CSV に変換してくれる便利モジュールです!
https://hexdocs.pm/csv/CSV.html
{:csv, "~> 2.4"}
ExportUserController
を作る想定でエンドポイントを追加します
scope "/", PaginateSampleWeb do
...
post "/export", ExportUserController, :create
...
end
ボタンをユーザ一覧画面に置きます
...
</.modal>
<% end %>
<div>
<%= button to: Routes.export_user_path(@socket, :create), method: :post do %>
Export
<% end %>
</div>
<table>
<thead>
...
ExportUserController の実装
ここまでいけばあとはエクスポートの実装ができればOKです!
コントローラーの実装は以下のようになります
defmodule PaginateSampleWeb.ExportUserController do
use PaginateSampleWeb, :controller
alias PaginateSample.Users
def create(conn, _params) do
send_download(conn, {:binary, csv_data()}, filename: "export.csv")
end
defp csv_data do
csv_content(users(), fields())
end
defp users do
Users.list_all_users()
end
defp fields do
[:id, :name, :inserted_at, :updated_at]
end
defp csv_content(records, fields) do
csv_content_list(records, fields)
|> CSV.encode()
|> Enum.to_list()
|> to_string()
end
defp csv_content_list(records, fields) do
[fields] ++ csv_data_list(records, fields)
end
defp csv_data_list(records, fields) do
records |> Enum.map(&fetch_by_fields_order(&1, fields))
end
defp fetch_by_fields_order(record, fields) do
fields |> Enum.map(fn field -> Map.fetch!(record, field) end)
end
end
自前でヘッダとか組み立てて send_resp
してもいいんですが send_download/3
という関数を使えば、いい感じにレスポンスを組み立てて返してくれるので便利です!
https://hexdocs.pm/phoenix/Phoenix.Controller.html#send_download/3
また Map は順序を保証しないので、ちゃんと fields で定義した順番に取り出せるように注意して実装します。
(単に Map.values()
などとすると、順序が壊れます)
追記: より簡単な実装
@koga1020 さんからコメントいただいてより簡素な実装になりました!
defmodule PaginateSampleWeb.ExportUserController do
use PaginateSampleWeb, :controller
alias PaginateSample.Users
def create(conn, _params) do
send_download(conn, {:binary, csv_data()}, filename: "export.csv")
end
defp csv_data do
users()
|> CSV.encode(headers: fields())
|> Enum.join()
end
defp users do
Users.list_all_users()
end
defp fields do
[:id, :name, :inserted_at, :updated_at]
end
end
まとめ
これで完成!です!
Elixir で書くとパイプでシンプルな実装になってよいですね...美しい!
CSVヘッダーの増減は fields
関数を修正すればいいし、 対象のデータが変わったら users
関数の修正をすればよいです。
また、改行コードを変えたい場合は CSV.encode/2
が対応しているので見てみてください。
リポジトリはこちらです
https://github.com/koyo-miyamura/paginate_sample_ex
参考にした記事
下記の英語記事を参考にしつつ、順番が保証されないところなどを修正して記事にしました!
https://fullstackphoenix.com/tutorials/csv-export