この記事は Elixir Advent Calendar 2022 の 12 日目です。
@koyo-miyamura です!
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております。
「お仕事でも使える Elixir」をテーマに実務での実装例の紹介に励んでおりますb
以前こちらの記事で LiveView での無限スクロールデータテーブルを実装しました。
今回はこちらのコンポーネント化をご紹介します!
バージョン
Phoenix 1.6.15
LiveView 0.17.12
コンポーネントについて
LiveView にはコンポーネントの手法が大きく2つあります。
1つ目は Phoenix.Component
というステートレスなコンポーネントを作る機能です。
シンプルで強力なのでまずこちらを使うことが多いでしょう。
2つ目は Phoenix.LiveComponent
というステートフルなコンポーネントを作る機能です。
こちらは LiveView ハンドラもセットでついてくるので、状態管理したりライフサイクル管理したり細かいイベントハンドリングできたりします。
ただあまりそういうのが必要な状況も少ないので、基本的には1つ目の Phoenix.Component
を試してみて、ダメだったら検討するでいいかなと思います。
スロットを使ったコンポーネント
ではまずスロット機能を使ったコンポーネントを紹介します。
スロットとはコンポーネントに外から html を与える機能です。
React や Vue などにもありますね。
実装
ハンドラの方に alias を貼っておきます。
defmodule ScrollableTableSampleWeb.UserLive.Index do
...
alias ScrollableTableSampleWeb.Components.ScrollableDataTable
...
end
コンポーネントの方はこういう感じで実装します。
Named Slot の機能を使って簡潔に書いています。
defmodule ScrollableTableSampleWeb.Components.ScrollableDataTable do
@moduledoc """
Scrollable data table.
"""
use Phoenix.Component
def table(assigns) do
~H"""
<div style="overflow-y: scroll; max-height: 70vh;" class="infinite-scrollable-table">
<table>
<thead>
<tr>
<%= for column <- @columns do %>
<%= column.label %>
<% end %>
</tr>
</thead>
<tbody
id="rows"
phx-update="append"
phx-hook="InfiniteScroll"
data-page={@page}
data-total_pages={@total_pages}
data-el=".infinite-scrollable-table"
>
<%= for row <- @rows do %>
<tr id={"row-#{row.id}"}>
<%= for column <- @columns do %>
<%= render_slot(column, row) %>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
"""
end
end
使う側はこんな感じですね。
<ScrollableDataTable.table rows={@users} page={@page} total_pages={@total_pages}>
<:columns let={user} label="Name">
<td><%= user.name %></td>
</:columns>
<:columns let={user} label="">
<td>
<span><%= live_redirect "Show", to: Routes.user_show_path(@socket, :show, user) %></span>
<span><%= live_patch "Edit", to: Routes.user_index_path(@socket, :edit, user) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: user.id, data: [confirm: "Are you sure?"] %></span>
</td>
</:columns>
</ScrollableDataTable.table>
以下にリポジトリも置いているので気になる方はみてみてください。
コンポーネントを関数単位で細かく分ける方法
スロットを使う方法でもいいのですが、柔軟性に欠ける場合があります。
例えばスロットはコンポーネントの直下に書かないといけない制約があります。
例えば下記のようにはできません。
<ScrollableDataTable.table>
<div>
<:columns let={user} label="Name">
<td><%= user.name %></td>
</:columns>
</div>
...
</ScrollableDataTable.table>
これだと困ることがあって、例えば DataTable コンポーネントがあって、 ScrollableDataTable はそれを利用して作りたい!みたいな場合に行き詰りますw
なのでテーブルの構造をスロットで表現するのではなく、コンポーネント内の関数単位で表現する手法を紹介します。
こっちの方が記述は煩雑ですが、後から柔軟に構造を組み替えたりできるので、いいかなと思います。
実装
まずデータテーブルを以下のように作ります。
コンポーネントの中にテーブルの構造を関数単位で表現するようにしています。
defmodule ScrollableTableSampleWeb.Components.DataTable do
@moduledoc """
Data table.
"""
use Phoenix.Component
def table(assigns) do
~H"""
<table>
<%= render_slot(@inner_block) %>
</table>
"""
end
def thead(assigns) do
~H"""
<thead>
<tr>
<%= render_slot(@inner_block) %>
</tr>
</thead>
"""
end
def tbody(assigns) do
~H"""
<%= for row <- @rows do %>
<tr id={"row-#{row.id}"}>
<%= render_slot(@inner_block, row) %>
</tr>
<% end %>
"""
end
end
上記のデータテーブルコンポーネントを使って、無限スクロールデータテーブルを作ります。
細かいスタイルはデータテーブルコンポーネントに委譲しつつ、無限スクロールデータテーブルに必要な hook やスタイルを定義するようにしています。
defmodule ScrollableTableSampleWeb.Components.ScrollableDataTable do
@moduledoc """
Scrollable data table.
"""
use Phoenix.Component
alias ScrollableTableSampleWeb.Components.DataTable
def table(assigns) do
~H"""
<div style="overflow-y: scroll; max-height: 70vh;" class="infinite-scrollable-table">
<DataTable.table>
<%= render_slot(@inner_block) %>
</DataTable.table>
</div>
"""
end
def thead(assigns) do
~H"""
<DataTable.thead>
<%= render_block(@inner_block) %>
</DataTable.thead>
"""
end
def tbody(assigns) do
~H"""
<tbody
id="rows"
phx-update="append"
phx-hook="InfiniteScroll"
data-page={@page}
data-total_pages={@total_pages}
data-el=".infinite-scrollable-table"
>
<DataTable.tbody let={row} rows={@rows}>
<%= render_block(@inner_block, row) %>
</DataTable.tbody>
</tbody>
"""
end
end
使う側はこんな感じです。ちょっと記述増えますが柔軟に記述できます。
<ScrollableDataTable.table>
<ScrollableDataTable.thead>
<th>Name</th>
<th></th>
</ScrollableDataTable.thead>
<ScrollableDataTable.tbody rows={@users} let={user} page={@page} total_pages={@total_pages}>
<td><%= user.name %></td>
<td>
<span><%= live_redirect "Show", to: Routes.user_show_path(@socket, :show, user) %></span>
<span><%= live_patch "Edit", to: Routes.user_index_path(@socket, :edit, user) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: user.id, data: [confirm: "Are you sure?"] %></span>
</td>
</ScrollableDataTable.tbody>
</ScrollableDataTable.table>
こちらも以下にリポジトリも置いているので気になる方はみてみてください!
まとめ
LiveView で無限スクロールデータテーブルをコンポーネント化する手法を紹介しました。
2パターン紹介しましたが筆者的には後者の方がおすすめです。
(前者でもっといいやり方あるかもしれませんがw)
コンポーネントに抽象化することで再利用も簡単なのでぜひやっていきましょう。
ちなみに LiveView 0.18 だとまた Phoenix.Component の文法が変わります。
具体的には Slot の機能がさらに強化されますね。
0.18 ver の実装出来たらぜひ記事にしてみてくださいb
次回は
@GeekMasahiro さんの「Elixirらしいif(xx){str += xxx}はこれだ」
です!お楽しみに!