👻
Turbo Streamsでの削除前にアニメーションを入れる
Turbo Sreamsでは action="remove"
で要素を削除することができます。
これは例えば DELETE /resources/:id
へのHTTPリクエストのレスポンスで使うと便利です。
この記事ではStimulusを利用して、Turbo Streamsでの要素削除時にアニメーションを入れる方法について書きたいと思います。
以下は
-
@hotwired/turbo
:7.0.0-beta.8
-
stimulus
:2.0.0
が前提です。
どうやるか
Stimulusで、以下のように使える leave_animation_controller.ts
を書きます。
<li
id="resource-1"
data-controller="leave-animation"
data-leave-animation-base-class="fade-out"
>
<!-- ... -->
</li>
.fade-out {
transition: opacity 200ms;
opacity: 1;
}
.fade-out-active {
opacity: 0;
}
leave_animation_controller.ts
は以下のようにしてみました。
leave_animation_controller.ts
import { Controller } from "stimulus";
import { StreamElement } from "@hotwired/turbo/dist/types/elements";
function nextAnimationFrame() {
return new Promise<void>(resolve => requestAnimationFrame(() => resolve()))
}
const DEFAULT_TRANSITION = "all 0s ease 0s"
const DEFAULT_ANIMATION = "none 0s ease 0s 1 normal none running"
export default class extends Controller {
static classes = ["base", "active"]
readonly baseClass!: string
readonly activeClass!: string
readonly hasActiveClass!: boolean
connect() {
document.addEventListener("turbo:before-stream-render", this.hook)
}
disconnect() {
document.removeEventListener("turbo:before-stream-render", this.hook)
}
hook = async (e: CustomEvent) => {
const stream = e.target as StreamElement;
if (stream.action === "remove" && stream.targetElements.includes(this.element)) {
e.preventDefault();
await this.leaveAnimation();
stream.performAction()
}
}
leaveAnimation = () => new Promise<void>(resolve => {
const activeClass = this.hasActiveClass ? this.activeClass : `${this.baseClass}-active`
this.element.addEventListener('transitionend', (event) => {
if (event.target === this.element) resolve()
})
this.element.addEventListener('animationend', (event) => {
if (event.target === this.element) resolve()
})
this.element.classList.add(this.baseClass)
const style = window.getComputedStyle(this.element)
if (style.getPropertyValue("transition") === DEFAULT_TRANSITION && style.getPropertyValue("animation") === DEFAULT_ANIMATION) {
console.warn("No leave animation style found", this.element)
resolve()
} else {
nextAnimationFrame()
.then(() => this.element.classList.add(activeClass))
}
})
}
解説
Turboには様々なイベントがありますが、このうち turbo:before-stream-render
を利用します。
このイベントは、Turbo Streamでの更新が起きる前に document
に対してdispatchされて、
-
event.preventDefault()
によって更新を止めることができる -
event.target
にはStreamElement
が入っており、StreamElement#performAction
によって更新を実行することができる
となっています。
なので、このイベントを利用して transitionend
か animationend
を待ってから削除するようにしています。
Controller#connect
内でイベントリスナーを追加していますが、これは
data-action="turbo:before-stream-render@document->leave-animation#hook"
とさせることもできました。ただ、必ず毎回指定するものですし、毎回明示的に書くメリットがないように感じたので Controller#connect
で登録する形にしました。
実際動かせるものを置いておきます。
Discussion