エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Phoenix LiveView で temporary_assigns を設定したときにどのように DOM を削除するか

動作確認に利用したコードは GitHub に push しました。 この記事で解説しているコードは このコミット にまとまっています。

temporary_assigns を設定したときの削除問題

LiveView では、一部が更新されたときに全体を再描画するのではなく、更新された部分だけを再描画するように設定することができます。

hexdocs.pm

ただし可能なのは、新規の DOM の追加と既存の DOM の更新だけで、削除はできません。 削除するには、該当する DOM を削除した全体をサーバからクライアントに送り、再描画する必要があります。 要素の数が少なければそのような方法も使えますが、数が多くなると送信のコストも再描画のコストも無視できなくなります。

これは Elixir Forum でも質問にあがっていた内容です。

elixirforum.com

解決策としては、Forum のこの質問にも回答されているように、JavaScript のフックを使って削除されたことの情報を持っている DOM を削除するというのが現実的なようです。

hexdocs.pm

と、いうわけで。 具体的に実装してみました。

実装のアイディア

アイディアはいたって単純です。

削除したい要素の DOM を更新して、削除の情報を設定します。 DOM の更新後に呼び出されるフック関数で、削除の情報が設定された DOM を削除します。

前提として。

ここでは Ecto ででデータベースを利用していることを想定しています。 ですが、対応するデータが削除されたことが DOM に反映されるようになっていれば、Ecto でなくてもデータベースのデータでなくても、同じように処理できます。

Ecto.Schema の状態を調べる

Ecto.Schema には :__meta__ という名前でメタデータが格納され、そこには :state という名前で状態が格納されています。

hexdocs.pm hexdocs.pm

メタデータの値を取得する Ecto.get_meta/2 が用意されているので、これで状態を確認できます。

Hoge というプロジェクトの Foos というコンテクストに Bar というスキーマが設定され bars というテーブルがあるような状況で、状態を確認してみます。

iex(1)> {:ok, bar} = Hoge.Foos.create_bar(%{name: "hoge"})
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:loaded, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(2)> Ecto.get_meta(bar, :state)
:loaded
iex(3)> {:ok, bar} = Hoge.Foos.get_bar!(1) |> Hoge.Foos.delete_bar()
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:deleted, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(4)> Ecto.get_meta(bar, :state)                                  
:deleted

state の値は次の 3 種類です。

状態
:built データはメモリ上に構築されているがデータベースに保存されていない状態
:loaded データはデータベースに保存されている状態
:deleted データはデータベースから削除されている状態

DOM にスキーマの状態を持たせる

これを利用して。 例えば data-state という属性を追加して、DOM にスキーマの状態を追加します。 また phx-hook でフックを設定します。

<div id="bars" phx-update="append">
  <%= for bar <- @bars do %>
    <div id="bar-<%= bar.id %>" data-state="<%= Ecto.get_meta(bar, :state) %>" phx-hook="Bar">
      <span><%= bar.id %></span>: <span><%= bar.name %></span>
    </div>
  <% end %>
</div>

JavaScript のフックで削除された状態の DOM を削除する

フックでは、DOM が更新された後に呼び出される関数 updated の中で data-state の内容を調べて、"deleted" になっていれば、状態が更新された要素を削除します。

let Hooks = {}

Hooks.Bar = {
  updated() {
    if (this.el.dataset.state == "deleted") {
      this.el.remove()
    }
  }
}

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

Hotwire のことを少し

先日 Hotwire が公表されました。 サーバで DOM を構築して送り出すことでページを更新するという、LiveView とも似ている部分があります。

さっそくさわってみました。

気がついたことはいくつかあるのですが、最初に目がいったのは部分的な DOM の削除の箇所。 予想どおりですが、削除のアクションが用意されていて、メソッド呼び出しひとつで指定した DOM を削除できるようになっていました。

www.rubydoc.info

実は LiveView での部分的な DOM の削除は、以前から気になっていた箇所です。 気になりつつも先送りしていたのですが、今回 Hotwire でそれができると知り、がぜん解決したくなったというのが今回の記事を書く動機になりました。

仕事では Ruby on Rails を主戦場にしていることもあり、ゆくゆくは Hotwire を使う機会があるかもしれません。 JavaScript 嫌いマン、JavaScript 書きたくないマン であるわたしにとっては LiveView や Hotwire のようなフレームワークが普及することを願って止みません。

Hotwire については。もう少し使ってみてから記事にしようと思います。

いつか読むはずっと読まない:YOUはSHOCK

日々仕事でデータベースを扱っていて。 SQL以外にもデータをうまくあつかえる手段を手に入れたい、という思いが高まり。 一度は R に手を出したものの挫折。

構文が肌に合わなかったのかもしれないと、Julia に手を出し。 現在挫折中。

構文云々ではなく、まずなにより「データをあつかう」という領域のメンタルモデルが必要なのかもしれない、と感じる今日このごろ。

1から始める Juliaプログラミング

1から始める Juliaプログラミング

そんなわけで。 みなさん、よいお年を。