11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Phoenix LiveView で CSV エクスポート機能を作る

Last updated at Posted at 2022-06-05

@koyo-miyamura です!
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております

CSV ダウンロード

業務システムを作っているとよく「CSVエクスポート」を作ることありますよね。
さてこれを Phoenix LiveView でどう実装するんだ・・・?と思って色々調べて参考実装を作ったので紹介します。

完成画面

まず完成イメージから

エクスポート用のボタンを生やします
image.png

ボタンを押すとファイルダウンロード( content-disposition attachment の挙動)
image.png

CSVファイルの生成
2022-06-05_23h28_12.png

要件

※前回のページネーションの実装をベースにします

  • エクスポートボタンを押すとすべての User データがダウンロードされる
  • CSVのヘッダは id, name, inserted_at, updated_at である
  • 文字コードは UTF-8
  • 改行コードは CRLF

実装

実装方針としては「CSVダウンロード用のエンドポイントを生やして LiveView の画面からリクエストできるようにする」とします。
ここでCSVダウンロード用のエンドポイントは LiveView ではなく単なる Phoenix コントローラーの実装であることに注意です!

まず下記モジュールを追加します。マップを CSV に変換してくれる便利モジュールです!
https://hexdocs.pm/csv/CSV.html

mix.exs
{:csv, "~> 2.4"}

ExportUserController を作る想定でエンドポイントを追加します

router.ex
  scope "/", PaginateSampleWeb do
...
    post "/export", ExportUserController, :create
...
  end

ボタンをユーザ一覧画面に置きます

index.html.heex
...
  </.modal>
<% end %>

<div>
  <%= button to: Routes.export_user_path(@socket, :create), method: :post do %>
    Export
  <% end %>
</div>

<table>
  <thead>
...

ExportUserController の実装

ここまでいけばあとはエクスポートの実装ができればOKです!
コントローラーの実装は以下のようになります

lib/paginate_sample_web/controllers/export_user_controller.ex
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 さんからコメントいただいてより簡素な実装になりました!

lib/paginate_sample_web/controllers/export_user_controller.ex
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 が対応しているので見てみてください。

リポジトリはこちらです:pray:
https://github.com/koyo-miyamura/paginate_sample_ex

参考にした記事

下記の英語記事を参考にしつつ、順番が保証されないところなどを修正して記事にしました!
https://fullstackphoenix.com/tutorials/csv-export

11
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?