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.

LiveView で無限スクロールデータテーブルを作る

Last updated at Posted at 2022-11-16

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

背景

副業で「無限スクロールデータテーブル」を実装する機会がありました。

無限スクロールデータテーブルというのはこういう感じで、テーブルをスクロールしたら次のページへのアクセスが行われるようなデータテーブルです。
(ツイッターなどのタイムラインをイメージするとわかりやすいです。)

edit_20221113214417.gif

実際に英語含めて調べてみても、あまり実装例なくて Javascript の動作を細かく追ったりして苦労したので、アルケミストの皆様のためにご紹介させていただければ!
(自分がやるときに欲しかった笑)

バージョン

Phoenix 1.6.15
LiveView 0.17.12

無限スクロールデータテーブルとは

実は「無限スクロール」の実装自体は以前 @torifukukaiou さんが過去に紹介しています。

実は LiveView 公式でも紹介されていたり。

しかしこれはスクロール領域が「画面全体」である場合の実装であり今回の要件を満たしませんでした。
スクロール領域を「データテーブル領域」にするにはここから一工夫必要となります。

実装

ということで実装してみましょう!

プロジェクト準備

以前ご紹介した方法で docker でプロジェクト準備します。
(もちろん普通にローカルで mix phx.new しても ok です!)

docker compose run --rm --no-deps web mix phx.new scrollable_table_sample

いつも通り User テーブルを作ります。

docker compose exec web mix phx.gen.live Users User users name:string
docker compose exec web mix ecto.migrate

router.ex への追加も忘れずに

router.ex
  scope "/", ScrollableTableSampleWeb do
    pipe_through :browser

    get "/", PageController, :index

    live "/users", UserLive.Index, :index
    live "/users/new", UserLive.Index, :new
    live "/users/:id/edit", UserLive.Index, :edit

    live "/users/:id", UserLive.Show, :show
    live "/users/:id/show/edit", UserLive.Show, :edit
  end

100名のユーザーを作ります。
Phoenix には seed を管理する仕組みがあるのでこれを使ってみましょう!

priv/repo/seeds.exs
for i <- 1..100, do: %{name: "name_#{i}"} |> ScrollableTableSample.Users.create_user()

これで流してみます。

docker compose exec web mix run priv/repo/seeds.exs

100名のユーザーができました!

image.png

スクローラブルにする

デフォルトだとテーブルがスクロールしないので以下のようにしてみます。

lib/scrollable_table_sample_web/live/user_live/index.html.heex
<div style="overflow-y: scroll; max-height: 70vh;">
  <table>
  ...
  </table>
</div>

スクロールバーがつきました :thumbsup:

image.png

Scrivenner を導入する

事前に Scrivenner というDBのページネーション対応をサポートするライブラリを導入します。
導入は以前紹介しているのでこちらを参考に。

無限スクロールデータテーブルにする

さてここからが本題です!
ユーザー一覧のテーブルを無限スクロール仕様にしてみましょう。

テンプレートファイルの編集

まずテンプレートファイルを編集して InfiniteScroll という名前の hooks をテーブルに組み込みます。
LiveView には hook という仕組みがあり、これを用いることで Javascript <-> LiveView 間で連携できます。今回のようにブラウザのイベントを取りたい場合に特に便利ですね)

lib/scrollable_table_sample_web/live/user_live/index.html.heex
<div style="overflow-y: scroll; max-height: 70vh;" class="infinite-scrollable-table">
  <table>
    ...
    <tbody
      id="users"
      phx-update="append"
      phx-hook="InfiniteScroll"
      data-page={@page}
      data-total_pages={@total_pages}
      data-el=".infinite-scrollable-table"
    >

hook の実装

hook は以下のように実装します。
hook は別ファイルに分けて実装すると見通しが良いですね :thumbsup:

assets/js/app.js
import { Hooks } from "./hooks"

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

hook の実装は以下のようになります。

assets/js/hooks.js
let Hooks = {}

// Returns scroll rate [%] of scrollable area for target DOM (el).
const scrollAt = (el) => {
  const scrollTop = el.scrollTop
  const scrollHeight = el.scrollHeight
  const clientHeight = el.clientHeight

  const result = scrollTop / (scrollHeight - clientHeight) * 100

  return result
}

Hooks.InfiniteScroll = {
  page() { return parseInt(this.el.dataset.page, 10) },
  total_pages() { return this.el.dataset.total_pages },
  mounted() {
    this.loading = false

    const $scrollDiv = document.querySelector(this.el.dataset.el)
    $scrollDiv.addEventListener("scroll", _e => {
      if(!this.loading && this.page() != this.total_pages() && scrollAt($scrollDiv) >= 99){
        // change "loading" into true in order to prevent multiple page loading at once
        this.loading = true
        this.pushEventTo("#" + this.el.id, "load_more", {})
      }
    })
  },
  reconnected(){ this.loading = false },
  updated(){ this.loading = false }
}

export { Hooks }

細かい説明するとそれだけで1記事になるので詳細は割愛しますが、ざっくりいうと scrollAt 関数がデータテーブルのスクロール可能領域のうち、現在どこまでスクロールしているかを百分率で計算していて、それが規定値(今回は99%)を超えたら次のページを読み込むイベントが LiveView ハンドラー側に送られるという仕組みです。

scrollAt 詳細の仕組みを知りたい方は以下の MDN を読んでみるといいと思いますb

なお、 loading フラグで制御しないと複数ページ一気に読み込まれるのでご注意を!

LiveView ハンドラの実装

後はハンドラ部分を実装すれば完成です!
先ほど入れた Scrivenner を使ってページネーションしたデータを返すようにします。
また mount 時には 1ページ目だけ返すようにします。

後は Javascript 側の hook から送られてくる load_more イベントをハンドリングして、次の1ページの User データを返せば OK です!
全体ではなく差分を返すことに注意が必要ですb

lib/scrollable_table_sample/users.ex
  def list_users(page: page, per_page: per_page) do
    from(u in User,
      order_by: [desc: u.updated_at, desc: u.id]
    )
    |> Repo.paginate(page: page, page_size: per_page)
  end
lib/scrollable_table_sample_web/live/user_live/index.ex
...

  @default_page 1
  @per_page 20

  @impl true
  def mount(_params, _session, socket) do
    socket
    |> assing_paginate_users(page: @default_page, per_page: @per_page)
    |> then(&{:ok, &1})
  end

...

  def handle_event("delete", %{"id" => id}, socket) do
    user = Users.get_user!(id)
    {:ok, _} = Users.delete_user(user)

    socket
    |> assing_paginate_users(page: @default_page, per_page: @per_page)
    |> then(&{:noreply, &1})
  end

  @impl true
  def handle_event("load_more", _, %{assigns: assigns} = socket) do
    next_page = assigns.page + 1

    {:noreply,
     socket
     |> assing_paginate_users(page: next_page, per_page: @per_page)}
  end

  defp assing_paginate_users(socket, page: page, per_page: per_page) do
    paginate_users(page: page, per_page: per_page)
    |> then(&assign(socket, &1))
  end

  defp paginate_users(page: current_page, per_page: per_page) do
    %{
      entries: entries,
      page_number: page,
      page_size: _page_size,
      total_entries: _total_entries,
      total_pages: total_pages
    } =
      Users.list_users(
        page: current_page,
        per_page: per_page
      )

    [
      users: entries,
      page: page,
      total_pages: total_pages
    ]
  end

完成!

edit_20221113214417.gif

まとめ

無限スクロールデータテーブルを LiveView で実装しました。

なお、実際に実務ではさらにこちらを LiveView コンポーネント化して再利用性を高めるということをしました。
実務ではコンポーネント化することも多いので、こちらについては Elixir アドベントカレンダーの12日目にてご紹介します!
お楽しみに!

なおリポジトリはこちらにあるので、ぜひ遊んでみてください! :pray:

11
6
0

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?