RailsのTurboでAjaxで要素を削除したときにCSSアニメーションさせる方法

Ajaxでデータ追加・削除するのにRailsのTurbo便利ですよね。
でも、CSSアニメーションが効かなくて困ってました。

Turbo以前

image-wrapperクラス以下に画像とxボタンを配置します。

# haml
.image-wrapper.position-relative
  = image_tag image
  = link_to user_image_path(user, image), remote: true, data: { method: :delete, confirm: "Can I delete it?" }, class: "delete-image-link position-absolute top-0 end-0" do
    %span ×

cssはfade-out用にkeyframes使ってアニメーションを定義します。

.fade-out {
  animation: fade-out .4s linear;
  opacity: 0;
}
@keyframes fade-out {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

あとはjsでfade-outクラスを追加するだけでアニメーションが可能でした。

  const deleteImageLinks = document.querySelectorAll(".delete-image-link")
  if (deleteImageLinks.length) {
    deleteImageLinks.forEach((link) => {
      link.addEventListener("click", (e) => {
        e.preventDefault
        e.target.closest(".image-wrapper").classList.add("fade-out")
      })
    })
  }

まずはTurboで実装

htmlは、turbo_frame_tag で更新部分を囲います。ついでにdata属性はturbo用に書き換えます。

# haml
= turbo_frame_tag "user_image_#{image.id}" do
  .image-wrapper.position-relative
    = image_tag image
    = link_to user_image_path(user, image), remote: true, data: { turbo_method: :delete, turbo_confirm: "Can I delete it?" }, class: "delete-image-link position-absolute top-0 end-0" do
      %span ×

controllerでは、render turbo_stream で削除します。

# controller
class Users::ImagesController < ApplicationController
  def destroy
    user = User.find(params[:user_id])
    user.images.find(params[:id]).purge

    render turbo_stream: turbo_stream.remove("user_image_#{params[:id]}")
  end
end

自動で更新はできましたが、これではcssアニメーションを追加することができません。

ではどうするか

turbo:before-stream-render という、turboの更新前のイベントを利用し、一時的に処理を止めcss animationが終わってから処理を再開することで対応します。

htmlに大きな違いはありませんが、jsからアニメーション用のクラス名を取得できるようにdata属性に leaving_class を指定しておきます。

# slim
-# ※ data属性に leaving_class を用意する
= turbo_frame_tag "user_image_#{image.id}", data: { leaving_class: "fade-out" } do
  .image-wrapper.position-relative
    = image_tag image
    = link_to user_image_path(user, image), remote: true, data: { turbo_method: :delete, turbo_confirm: "Can I delete it?" }, class: "delete-image-link position-absolute top-0 end-0" do
      %span &times;

turbo:before-stream-render イベントで、以下のようにすることで対応できました。

document.addEventListener("turbo:before-stream-render", (e) => {
  const turboStreamElm = e.target
  const { action, target } = turboStreamElm

  if (action === "remove") {
    const turboFrameElm = document.getElementById(target)
    const { leavingClass } = turboFrameElm.dataset
    if (leavingClass) {
      e.preventDefault()
      turboFrameElm.classList.add(leavingClass)
      turboFrameElm.addEventListener("animationend", () => {
        e.target.performAction()
      })
    }
  }
})

所感

正直Turbo以前の方がsimpleですよね。

参考

Tweet / Twitter
Turbo Streamsでの削除前にアニメーションを入れる

See Also

RailsのTurboでAjaxで要素を削除したときにCSSアニメーションさせる方法2(Stimlus化)RailsのHotwireでCSSアニメーションさせる方法(登録処理もTurbo/Stimlusで実装)