カスタムイベントとは何か
カスタムイベントは、あなたが「アプリ内の合図」を自由に作って通知できる仕組みです。DOM の仕組み(addEventListener / dispatchEvent)をそのまま使い、任意の名前でイベントを発火し、必要な文脈を detail に詰めて渡せます。ここが重要です:画面操作(クリック等)に依存せず、コンポーネント間の連携・状態完了の知らせ・データ到着の宣言などを“疎結合”に実現できます。
作り方と基本の使い方(CustomEvent / dispatchEvent)
最小の例(作る→聴く→発火する)
<div id="bus"></div>
<script>
const bus = document.getElementById("bus");
// 1) 聴く(リスナー登録)
bus.addEventListener("loaded", (e) => {
console.log("読み込み完了:", e.detail); // { count: 42 }
});
// 2) 作る(detail に文脈を詰める)
const ev = new CustomEvent("loaded", { detail: { count: 42 } });
// 3) 発火する(通知を送る)
bus.dispatchEvent(ev);
</script>
HTMLここが重要です:CustomEvent の detail は“自由な入れ物”。処理に必要な最小の情報を詰めて渡すと、受け手の依存が減り、保守が楽になります。
イベント名の指針
- 動詞過去形: 「loaded」「saved」「updated」など、完了の合図が読みやすい。
- 名前空間風: 「user:login」「cart:item:add」など、衝突回避と意味の整理に有効。
- 短く一貫: 名前が増えるほど覚えるのが難しい。共通語彙を早めに決める。
伝播・キャンセル・シャドウDOM(bubbles / cancelable / composed)
親へ伝える(bubbles)
<div id="outer">
<div id="inner"></div>
</div>
<script>
outer.addEventListener("notify", (e) => console.log("親で受信:", e.detail));
inner.dispatchEvent(new CustomEvent("notify", {
detail: { msg: "こんにちは" },
bubbles: true // バブルで親へ届く
}));
</script>
HTMLここが重要です:bubbles: true を付けると、親に委譲できて“受け口が一箇所”にまとまります。動的に追加される子にも強い。
キャンセル可能にする(cancelable)
<script>
const ev = new CustomEvent("confirm-delete", { cancelable: true, detail: { id: 42 } });
document.addEventListener("confirm-delete", (e) => {
if (!confirm(`本当に削除しますか? #${e.detail.id}`)) {
e.preventDefault(); // キャンセルの合図
}
});
const canceled = !document.dispatchEvent(ev);
console.log("キャンセルされた?", canceled); // preventDefault が呼ばれると true
</script>
HTMLここが重要です:ユーザー確認やバリデーションの“拒否権”をリスナー側に持たせたい時は cancelable。dispatchEvent の戻り値で中止判定ができます。
シャドウDOM越しに届ける(composed)
<script>
const ev = new CustomEvent("notify", { bubbles: true, composed: true, detail: { x: 1 } });
// シャドウDOM内から発火しても、composed: true なら外側(Light DOM)へ届く
</script>
HTMLここが重要です:Web Components を使う場合は composed: true がカギ。境界を越えて通知したいかどうかを設計で決めます。
現実的なパターン(疎結合の連携・完了合図・ストア更新)
フォーム送信完了をページに知らせる
<form id="f">
<input name="email" required>
<button>送信</button>
</form>
<script>
f.addEventListener("submit", async (e) => {
e.preventDefault();
await new Promise(r => setTimeout(r, 300)); // 送信の代用
document.dispatchEvent(new CustomEvent("user:saved", { detail: { email: f.email.value } }));
});
document.addEventListener("user:saved", (e) => {
console.log("保存完了:", e.detail.email);
// トースト表示や一覧の再取得など
});
</script>
HTMLここが重要です:“保存完了”という合図をイベントにすることで、送信側と表示側の結合を断てます。追加の受け手が増えても送信側を触らず拡張可能。
リストアイテムの操作を親で集約
<ul id="list"></ul>
<script>
list.addEventListener("item:delete", (e) => {
e.target.closest("li")?.remove();
});
function makeRow(text) {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.textContent = "削除";
btn.addEventListener("click", () => {
btn.dispatchEvent(new CustomEvent("item:delete", { bubbles: true }));
});
li.append(text, " ", btn);
return li;
}
list.appendChild(makeRow("りんご"));
</script>
HTMLここが重要です:子は“合図を出すだけ”。実処理は親に集約すると、数が増えてもコードが散らばりません。
簡易 Pub/Sub(イベントバス)
<div id="bus" hidden></div>
<script>
const bus = document.getElementById("bus");
function publish(type, detail) {
bus.dispatchEvent(new CustomEvent(type, { detail, bubbles: false }));
}
function subscribe(type, handler) {
bus.addEventListener(type, handler);
}
subscribe("theme:change", (e) => document.documentElement.dataset.theme = e.detail);
publish("theme:change", "dark");
</script>
HTMLここが重要です:DOM を“バス”として使えば、フレームワークなしでもシンプルな Pub/Sub が作れます。バスを1箇所に決めると可視性が高い。
コンポーネントでの活用(Web Components / EventTarget)
Web Components から外へ通知
<script>
class Counter extends HTMLElement {
#n = 0;
connectedCallback() {
this.innerHTML = `<button>+1</button>`;
this.querySelector("button").addEventListener("click", () => {
this.#n++;
this.dispatchEvent(new CustomEvent("counter:change", {
detail: { value: this.#n }, bubbles: true, composed: true
}));
});
}
}
customElements.define("x-counter", Counter);
document.addEventListener("counter:change", (e) => console.log("値:", e.detail.value));
</script>
<x-counter></x-counter>
HTMLここが重要です:composed: true で“外へ開く”か、内だけで完結させるかを設計で決める。外部 API としてイベント名・detail の形をドキュメント化しておくと使われやすくなります。
DOM 以外でもイベント(EventTarget を継承)
<script>
class Store extends EventTarget {
#state = { count: 0 };
increment() {
this.#state.count++;
this.dispatchEvent(new CustomEvent("change", { detail: this.#state }));
}
get state() { return this.#state; }
}
const store = new Store();
store.addEventListener("change", (e) => console.log("更新:", e.detail.count));
store.increment(); // → change が飛ぶ
</script>
HTMLここが重要です:EventTarget を継承すれば、非 DOM でも同じインターフェイスで連携できます。UI とロジックの疎結合がさらに進みます。
設計指針(名前付け・契約・拡張性)
イベントは“契約”として定義する
- 誰が発火し、何を約束するか: イベント名、detail の形、bubbles/composed の有無を明文化。
- 安定性: detail のスキーマは後方互換を意識し、追加は許して変更は避ける。
- 最小情報: 受け手が必要な最小限だけ渡す。内部実装の漏れは避ける。
UI とロジックを分ける
- UI: ボタンやフォームは“イベントを出す”だけ。
- ロジック: データ保存・ルーティング・通知表示は“イベントを聴く”側に集約。
ここが重要です:責務分離でテストが簡単になり、後から機能追加しても既存コードに手を入れずに済みます。
よくある落とし穴と回避策
bubbles を忘れて親で受けられない
- 対策: 親で受けたいなら必ず bubbles: true。範囲は currentTarget.contains(…) で安全確認。
cancelable を付けないまま“キャンセルしたい”設計
- 対策: キャンセル権が必要なら cancelable: true。dispatchEvent の戻り値で中止判定し、分岐を徹底。
イベント名の乱立・衝突
- 対策: 名前空間風(module:action)で整理。チームの語彙表を作る。
detail に巨大データを詰める
- 対策: 必要最小限だけ。大きなデータは ID を渡し、受け手が取得するほうが安全。
シャドウDOMで外に届かない
- 対策: composed: true を付ける。外へ出したくない設計なら false のまま意図的に閉じる。
実践例(確認キャンセル、通知トースト、進捗連動)
確認キャンセルを“合図”で表現
<button id="del">削除</button>
<script>
del.addEventListener("click", () => {
const ev = new CustomEvent("item:delete", { cancelable: true, detail: { id: 7 } });
const ok = document.dispatchEvent(ev); // preventDefault されていなければ true
if (ok) console.log("削除実行");
});
document.addEventListener("item:delete", (e) => {
if (!confirm(`本当に #${e.detail.id} を削除しますか?`)) e.preventDefault();
});
</script>
HTMLグローバル通知(トースト表示)
<div id="toast" style="position:fixed;right:12px;bottom:12px;background:#333;color:#fff;padding:8px;display:none"></div>
<script>
document.addEventListener("notify", (e) => {
toast.textContent = e.detail.message;
toast.style.display = "block";
setTimeout(() => toast.style.display = "none", 1200);
});
// どこからでも
document.dispatchEvent(new CustomEvent("notify", { detail: { message: "保存しました" } }));
</script>
HTML非同期進捗の連動
<progress id="bar" value="0" max="100"></progress>
<script>
document.addEventListener("task:progress", (e) => bar.value = e.detail.percent);
async function runTask() {
for (let p = 0; p <= 100; p += 10) {
await new Promise(r => setTimeout(r, 100));
document.dispatchEvent(new CustomEvent("task:progress", { detail: { percent: p } }));
}
}
runTask();
</script>
HTMLまとめ
カスタムイベントは「アプリ内の合図」を作り、疎結合に連携させる強力な基礎です。detail に文脈を載せ、bubbles で親に集約、必要なら cancelable と composed を設計に合わせて使う。イベントは“契約”として名前とペイロードを安定化し、UI は発火、ロジックは受信に分離する。落とし穴(bubbles/composed/cancelable の指定漏れ、名前の乱立、巨大 detail)を避ければ、初心者でも拡張しやすい、壊れにくいイベント駆動の設計ができます。
