LiveView で無限スクロールデータテーブルをコンポーネント化する

この記事は Elixir Advent Calendar 2022 の 12 日目です。

https://qiita.com/advent-calendar/2022/elixir

@koyo-miyamura です！
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております。
「お仕事でも使える Elixir」をテーマに実務での実装例の紹介に励んでおりますb

以前こちらの記事で LiveView での無限スクロールデータテーブルを実装しました。
今回はこちらのコンポーネント化をご紹介します！

https://qiita.com/koyo-miyamura/items/272b751fbf58755818f6

![edit_20221113214417.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/75723014-5ba2-2c12-6576-0a70e300dfb1.gif)

# バージョン
```
Phoenix 1.6.15
LiveView 0.17.12
```

# コンポーネントについて

LiveView にはコンポーネントの手法が大きく2つあります。

1つ目は `Phoenix.Component` というステートレスなコンポーネントを作る機能です。
シンプルで強力なのでまずこちらを使うことが多いでしょう。

https://hexdocs.pm/phoenix_live_view/0.17.12/Phoenix.Component.html

2つ目は `Phoenix.LiveComponent` というステートフルなコンポーネントを作る機能です。
こちらは LiveView ハンドラもセットでついてくるので、状態管理したりライフサイクル管理したり細かいイベントハンドリングできたりします。
ただあまりそういうのが必要な状況も少ないので、基本的には1つ目の `Phoenix.Component` を試してみて、ダメだったら検討するでいいかなと思います。

https://hexdocs.pm/phoenix_live_view/0.17.12/Phoenix.LiveComponent.html

# スロットを使ったコンポーネント
ではまずスロット機能を使ったコンポーネントを紹介します。
スロットとはコンポーネントに外から html を与える機能です。
React や Vue などにもありますね。

https://hexdocs.pm/phoenix_live_view/0.17.12/Phoenix.Component.html#module-slots

## 実装

ハンドラの方に alias を貼っておきます。

```elixir:lib/scrollable_table_sample_web/live/user_live/index.ex
defmodule ScrollableTableSampleWeb.UserLive.Index do
...

  alias ScrollableTableSampleWeb.Components.ScrollableDataTable
...
end
```

コンポーネントの方はこういう感じで実装します。
Named Slot の機能を使って簡潔に書いています。

```elixir:lib/scrollable_table_sample_web/live/components/scrollable_table.ex
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
```

使う側はこんな感じですね。

```elixir:lib/scrollable_table_sample_web/live/user_live/index.html.heex
<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>
```

以下にリポジトリも置いているので気になる方はみてみてください。

https://github.com/koyo-miyamura/scrollable_table_sample/tree/feature/named_slot_with_attributes_component

# コンポーネントを関数単位で細かく分ける方法
スロットを使う方法でもいいのですが、柔軟性に欠ける場合があります。
例えばスロットはコンポーネントの直下に書かないといけない制約があります。

例えば下記のようにはできません。

```elixir:lib/scrollable_table_sample_web/live/components/scrollable_table.ex
<ScrollableDataTable.table>
  <div>
    <:columns let={user} label="Name">
      <td><%= user.name %></td>
    </:columns>
  </div>
...
</ScrollableDataTable.table>
```

![2022-11-19_20h20_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/21d838ad-40cc-aeaa-4479-d858d2b4aa6f.png)

これだと困ることがあって、例えば DataTable コンポーネントがあって、 ScrollableDataTable はそれを利用して作りたい！みたいな場合に行き詰りますｗ

なのでテーブルの構造をスロットで表現するのではなく、コンポーネント内の関数単位で表現する手法を紹介します。
こっちの方が記述は煩雑ですが、後から柔軟に構造を組み替えたりできるので、いいかなと思います。

## 実装

まずデータテーブルを以下のように作ります。
コンポーネントの中にテーブルの構造を関数単位で表現するようにしています。

```elixir:lib/scrollable_table_sample_web/live/components/data_table.ex
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 やスタイルを定義するようにしています。

```elixir:lib/scrollable_table_sample_web/live/components/scrollable_table.ex
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
```

使う側はこんな感じです。ちょっと記述増えますが柔軟に記述できます。

```elixir:lib/scrollable_table_sample_web/live/user_live/index.html.heex
<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>
```

こちらも以下にリポジトリも置いているので気になる方はみてみてください！

https://github.com/koyo-miyamura/scrollable_table_sample/tree/feature/function_component

# まとめ
LiveView で無限スクロールデータテーブルをコンポーネント化する手法を紹介しました。
2パターン紹介しましたが筆者的には後者の方がおすすめです。
（前者でもっといいやり方あるかもしれませんがｗ）

コンポーネントに抽象化することで再利用も簡単なのでぜひやっていきましょう。

ちなみに LiveView 0.18 だとまた Phoenix.Component の文法が変わります。
具体的には Slot の機能がさらに強化されますね。
0.18 ver の実装出来たらぜひ記事にしてみてくださいb

https://hexdocs.pm/phoenix_live_view/0.18.3/Phoenix.Component.html

次回は
@GeekMasahiro さんの「Elixirらしいif(xx){str += xxx}はこれだ」
です！お楽しみに！