JavaScript | DOM 操作:イベント発展 – カスタムイベント

JavaScript
スポンサーリンク

カスタムイベントとは何か

カスタムイベントは、あなたが「アプリ内の合図」を自由に作って通知できる仕組みです。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)を避ければ、初心者でも拡張しやすい、壊れにくいイベント駆動の設計ができます。

タイトルとURLをコピーしました