🫥
イベントをPromise化する
はじめに
アニメーションの制御でフェードイン&フェードアウトを任意の時間で実装しようとしたときにコールバック地獄になりそうだったので、それを回避したい。
コード
const onceEvent = async (element, event, options = {}) => {
const { promise, resolve, reject } = Promise.withResolvers();
element.addEventListener(event, resolve, { ...options, once: true });
return promise;
};
簡単な使用例
(async () => {
let mouseEvent = await onceEvent(document.body, 'click');
alert('1回クリックされました。');
})();
より具体的な使用例
123と書かれている要素を追加する。各要素は1, 5, 10秒後にフェードアウトする。
ここで重要な点は、インデントが浅い点とフェードアウトを開始するまでの時間を関数から指定できること。
sample.html
<body />
sample.css
.box {
opacity: 0;
width: 30px;
height: 30px;
background-color: green;
}
.fadein {
animation-name: fadeIn;
animation-fill-mode: forwards;
animation-duration: 1s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fadeout {
animation-name: fadeOut;
animation-fill-mode: forwards;
animation-duration: 1s;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
sample.js
// 〇ms停止
const wait = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onceEvent = async (element, event, options = {}) => {
const { promise, resolve, reject } = Promise.withResolvers();
element.addEventListener(event, resolve, { ...options, once: true });
return promise;
};
// elementに適応されているいずれかのアニメーションがキャンセル・終了するまで待機する
const waitAnimation = async (element) => {
const controller = new AbortController();
const events = [
onceEvent(element, "animationend", { signal: controller.signal }),
onceEvent(element, "animationcancel", { signal: controller.signal })
];
const resolvedEvent = await Promise.any(events);
controller.abort();
return resolvedEvent;
};
(() => {
const appendShrinkDiv = async (ms) => {
const div = document.createElement("div");
div.innerText = "123";
div.classList.add("box");
document.body.appendChild(div);
let event;
event = waitAnimation(div);
div.classList.add("fadein");
await event;
await wait(ms);
div.classList.remove("fadein");
event = waitAnimation(div);
div.classList.add("fadeout");
await event;
div.remove();
};
appendShrinkDiv(1000);
appendShrinkDiv(5000);
appendShrinkDiv(10000);
})();
コールバック地獄の例
(animationcancelに対応してないことなど簡略化している)
(() => {
const appendShrinkDiv = async (ms) => {
const div = document.createElement("div");
div.innerText = "123";
div.classList.add("box");
document.body.appendChild(div);
div.addEventListener("animationend", async () => {
await wait(ms);
div.classList.remove("fadein");
div.addEventListener("animationend", () => {
div.remove();
}, { once: true });
div.classList.add("fadeout");
}, { once: true });
div.classList.add("fadein");
};
appendShrinkDiv(1000);
appendShrinkDiv(5000);
appendShrinkDiv(10000);
})();
更新履歴
2025/3/4 signal, animationcancelに関して修正 + ちょっと見た目変えた
Discussion
引数の options に signal を許容して やった方がいいのでは……?みたいなところあります。
animation イベントは 最後は animationend もしくは animationcancel が動くので
参考:
※ただ、この参考ページの signal は class を除去するのに使っているので signal は入れずに自己解放しています。入れるなら多段 signal しないとならないanimationcancelイベントとsignalなんてものがあったんですね初知りです。
せっかくなんで他のオプションも許容してみました。(影響は知らないですが)
更新しました。