2022年10月14日

カスタムイベントのディスパッチ

ハンドラーを割り当てるだけでなく、JavaScriptからイベントを生成することもできます。

カスタムイベントは、「グラフィカルコンポーネント」を作成するために使用できます。たとえば、独自のJSベースのメニューのルート要素は、メニューで何が起こるかを示すイベント(open(メニューが開く)、select(アイテムが選択される)など)をトリガーする可能性があります。別のコードはイベントをリッスンし、メニューで何が起こっているかを監視できます。

独自の目的で考案したまったく新しいイベントだけでなく、clickmousedownなど、組み込みのイベントも生成できます。これは自動テストに役立つ場合があります。

イベントコンストラクタ

組み込みのイベントクラスは、DOM要素クラスと同様に階層を形成します。ルートは組み込みのEventクラスです。

Eventオブジェクトは次のように作成できます。

let event = new Event(type[, options]);

引数

  • type – イベントタイプ。「click」のような文字列、または「my-event」のような独自の文字列。

  • options – 2つのオプションのプロパティを持つオブジェクト。

    • bubbles: true/falsetrueの場合、イベントはバブリングします。
    • cancelable: true/falsetrueの場合、「デフォルトアクション」を阻止できます。後でカスタムイベントでそれが何を意味するのか説明します。

    デフォルトではどちらもfalseです:{bubbles: false, cancelable: false}

dispatchEvent

イベントオブジェクトが作成された後、elem.dispatchEvent(event)呼び出しを使用して、要素上で「実行」する必要があります。

その後、ハンドラーはそれが通常のブラウザイベントであるかのように反応します。イベントがbubblesフラグで作成された場合、バブリングします。

次の例では、clickイベントがJavaScriptで開始されます。ハンドラーは、ボタンがクリックされた場合と同じように動作します。

<button id="elem" onclick="alert('Click!');">Autoclick</button>

<script>
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>
event.isTrusted

スクリプトで生成されたイベントと「リアル」なユーザーイベントを区別する方法があります。

プロパティevent.isTrustedは、実際のユーザー操作からのイベントではtrue、スクリプトで生成されたイベントではfalseになります。

バブリングの例

"hello"という名前のバブリングイベントを作成し、documentでキャッチできます。

必要なのは、bubblestrueに設定することだけです。

<h1 id="elem">Hello from the script!</h1>

<script>
  // catch on document...
  document.addEventListener("hello", function(event) { // (1)
    alert("Hello from " + event.target.tagName); // Hello from H1
  });

  // ...dispatch on elem!
  let event = new Event("hello", {bubbles: true}); // (2)
  elem.dispatchEvent(event);

  // the handler on document will activate and display the message.

</script>

注意事項

  1. カスタムイベントにはaddEventListenerを使用する必要があります。なぜなら、on<event>は組み込みイベントにしか存在せず、document.onhelloは機能しないためです。
  2. bubbles:trueを設定する必要があります。そうしないと、イベントはバブリングしません。

バブリングメカニズムは、組み込み(click)イベントとカスタム(hello)イベントで同じです。キャプチャとバブリングのステージもあります。

MouseEvent、KeyboardEventなど

UIイベント仕様からのUIイベントのクラスの簡単なリストを以下に示します。

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent

このようなイベントを作成したい場合は、new Eventの代わりにそれらを使用する必要があります。たとえば、new MouseEvent("click")です。

適切なコンストラクタを使用すると、そのタイプのイベントの標準プロパティを指定できます。

マウスイベントのclientX/clientYなど。

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // 100

注意:汎用Eventコンストラクタでは、それは許可されません。

試してみましょう

let event = new Event("click", {
  bubbles: true, // only bubbles and cancelable
  cancelable: true, // work in the Event constructor
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined, the unknown property is ignored!

技術的には、作成後にevent.clientX=100を直接割り当てることで回避できます。そのため、これは便宜上の問題であり、ルールに従うことです。ブラウザで生成されたイベントは常に正しいタイプです。

さまざまなUIイベントのプロパティの完全なリストは、仕様(たとえばMouseEvent)にあります。

カスタムイベント

hello」のような独自のまったく新しいイベントタイプには、new CustomEventを使用する必要があります。技術的にはCustomEventEventと同じですが、1つの例外があります。

2番目の引数(オブジェクト)で、イベントとともに渡したい任意のカスタム情報を格納する追加のプロパティdetailを追加できます。

たとえば

<h1 id="elem">Hello for John!</h1>

<script>
  // additional details come with the event to the handler
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "John" }
  }));
</script>

detailプロパティには任意のデータを含めることができます。技術的には、作成後に通常のnew Eventオブジェクトに任意のプロパティを割り当てることができるため、なくても構いません。しかし、CustomEventは、他のイベントプロパティとの競合を回避するために、特別なdetailフィールドを提供します。

さらに、イベントクラスはそれが「どのような種類のイベント」であるかを記述し、イベントがカスタムである場合、それが何であるかを明確にするためにCustomEventを使用する必要があります。

event.preventDefault()

多くのブラウザイベントには、「デフォルトアクション」(リンクへの移動、選択の開始など)があります。

新しいカスタムイベントには、間違いなくデフォルトのブラウザアクションはありませんが、そのようなイベントをディスパッチするコードは、イベントのトリガー後に実行する独自の計画を持っている可能性があります。

event.preventDefault()を呼び出すことで、イベントハンドラーはそれらのアクションをキャンセルする必要があるというシグナルを送信できます。

その場合、elem.dispatchEvent(event)への呼び出しはfalseを返します。そして、それをディスパッチしたコードは、続行するべきではないことを認識します。

実用的な例を見てみましょう。隠れるウサギ(メニューの閉じたり、何か他のものかもしれません)。

以下に、#rabbitと、関心のあるすべての当事者にウサギが隠れることを知らせるために、その上で"hide"イベントをディスパッチするhide()関数があります。

どのハンドラーでも、rabbit.addEventListener('hide',...)を使用してそのイベントをリッスンし、必要に応じてevent.preventDefault()を使用してアクションをキャンセルできます。そうすると、ウサギは消えません。

<pre id="rabbit">
  |\   /|
   \|_|/
   /. .\
  =\_Y_/=
   {>o<}
</pre>
<button onclick="hide()">Hide()</button>

<script>
  function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // without that flag preventDefault doesn't work
    });
    if (!rabbit.dispatchEvent(event)) {
      alert('The action was prevented by a handler');
    } else {
      rabbit.hidden = true;
    }
  }

  rabbit.addEventListener('hide', function(event) {
    if (confirm("Call preventDefault?")) {
      event.preventDefault();
    }
  });
</script>

注意:イベントにはcancelable: trueフラグを設定する必要があります。そうしないと、event.preventDefault()の呼び出しは無視されます。

イベント内イベントは同期しています

通常、イベントはキューで処理されます。つまり、ブラウザがonclickを処理していて、新しいイベント(マウスが移動したなど)が発生した場合、その処理はキューに入れられ、対応するmousemoveハンドラーはonclickの処理が完了した後に呼び出されます。

注目すべき例外は、別のイベント内(たとえば、dispatchEventを使用して)でイベントが開始された場合です。このようなイベントはすぐに処理されます。新しいイベントハンドラーが呼び出され、その後、現在のイベント処理が再開されます。

たとえば、以下のコードでは、onclick中にmenu-openイベントがトリガーされます。

onclickハンドラーが終了するのを待たずに、すぐに処理されます。

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // triggers between 1 and 2
  document.addEventListener('menu-open', () => alert('nested'));
</script>

出力順序は1→ネスト→2です。

ネストされたイベントmenu-opendocumentでキャッチされていることに注意してください。ネストされたイベントの伝播と処理は、処理が外部コード(onclick)に戻る前に終了します。

これはdispatchEventだけではありません。イベントハンドラーが他のイベントをトリガーするメソッドを呼び出す場合、それらもネストされた方法で同期的に処理されます。

気に入らないとしましょう。onclickを、menu-openやその他のネストされたイベントとは独立して、最初に完全に処理したいとします。

その場合、onclickの最後にdispatchEvent(または別のイベントトリガー呼び出し)を配置するか、おそらくより良い方法として、ゼロ遅延のsetTimeoutでラップします。

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('nested'));
</script>

これで、dispatchEventは現在のコードの実行が完了した後に非同期的に実行されるため、menu.onclickを含め、イベントハンドラーは完全に分離されます。

出力順序は1→2→ネストになります。

要約

コードからイベントを生成するには、最初にイベントオブジェクトを作成する必要があります。

汎用Event(name, options)コンストラクタは、任意のイベント名と、2つのプロパティを持つoptionsオブジェクトを受け入れます。

  • イベントをバブリングさせる必要がある場合、bubbles: true
  • event.preventDefault()を機能させる必要がある場合、cancelable: true

MouseEventKeyboardEventなど、ネイティブイベントの他のコンストラクタは、そのイベントタイプに固有のプロパティを受け入れます。たとえば、マウスイベントのclientX

カスタムイベントには、CustomEventコンストラクタを使用する必要があります。これは、detailという名前の追加オプションがあり、イベント固有のデータをそれに割り当てる必要があります。その後、すべてのハンドラーはそれをevent.detailとしてアクセスできます。

clickkeydownなどのブラウザイベントを生成する技術的な可能性がありますが、注意して使用する必要があります。

ハンドラーを実行するためのハッキーな方法であるため、ブラウザイベントを生成するべきではありません。ほとんどの場合、これは悪いアーキテクチャです。

ネイティブイベントが生成される可能性があります。

  • サードパーティライブラリが適切に動作するように、他の相互作用手段を提供していない場合の、いわば苦肉の策です。
  • 自動テストのために、スクリプトで「ボタンをクリック」し、インターフェースが正しく反応するかどうかを確認します。

独自の名称を持つカスタムイベントは、メニュー、スライダー、カルーセルなど内部で何が起こっているかを知らせるために、アーキテクチャ上の目的で頻繁に生成されます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関するご提案がありましたら、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 少数のコード行を挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。