私たちは、ハンドラを割り当てるだけでなく、JavaScript からイベントを生成することもできます。
カスタムイベントを使用して「グラフィックコンポーネント」を作成できます。例えば、メニューのルート要素は、メニューで起きたことを伝えるイベントをトリガすることができます: open
(メニューを開く), select
(項目が選択された) など。
click
, mousedown
などのような、組み込みのイベントを生成することもでき、テストをするときに便利です。
イベントコンストラクタ
イベントはDOM 要素クラスと同様、階層を形成します。ルートは組み込みの Event クラスです。
このようにして Event
オブジェクトを生成できます:
let event = new Event(event type[, options]);
引数:
-
event type –
"click"
や独自の"hey-ho!"
のような任意の文字列です。 -
options – 2つのオプションのプロパティを持つオブジェクトです:
bubbles: true/false
–true
の場合、イベントがバブルします。cancelable: true/false
–true
の場合、“デフォルトアクション” が防がれます。後ほど、 カスタムイベントに対して意味していることを見てきます。
デフォルトでは、両方とも 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
は true
になります。スクリプトで生成されたイベントは false
です。
バブリング例
"hello"
という名前のバブリングイベントを作成し、document
でキャッチすることができます。
必要なことは、bubbles
を true
にセットすることです。:
<h1 id="elem">Hello from the script!</h1>
<script>
// document でキャッチ...
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});
// ...elem でディスパッチ!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
</script>
補足:
- カスタムイベントに対しては、
addEventListener
を使うべきです。なぜなら、on<event>
は組み込みイベントに対してのみ存在するからです。document.onhello
は動作しません。 bubbles:true
を設定しなければなりません。さもないと、イベントはバブルしません。
バブリングの仕組みは、組み込み (click
) と カスタム (hello
) イベントで同じです。キャプチャリングとバブリングのフェーズもあります。
MouseEvent, KeyboardEvent 等
以下は UI Event specification の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, // bubbles と cancelable のみ
cancelable: true, // Event constructor で動作します
clientX: 100,
clientY: 100
});
alert(event.clientX); // undefined, 未知のプロパティは無視されます!
技術的には、生成後に直接 event.clientX=100
を割り当てることで回避できます。それは利便性の問題であり、ルールに従うためです。ブラウザで生成されたイベントは常に正しいタイプを持っています。
異なるUIイベントの、プロパティの完全なリストは仕様にあります。例えば、MouseEvent など。
カスタムイベント
"hello"
のような独自のカスタムイベントに対しては、new CustomEvent
を使うべきです。技術的には CustomEvent は1つの例を除いて Event
と同じです。
2つ目の引数(オブジェクト)では、イベントと一緒に渡したい任意の情報のために、追加のプロパティ detail
を追加することが出来ます。
例:
<h1 id="elem">Hello for John!</h1>
<script>
// イベントと一緒にハンドラに来る追加の詳細情報です
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()
もし cancelable:true
フラグが指定されている場合、スクリプトで生成されたイベントで event.preventDefault()
を呼び出すことができます。
もちろん、イベントが非標準の名前である場合、ブラウザはそれを知らないので、そのための “デフォルトブラウザアクション” はありません。
しかし、イベントを生成するコードは dispatchEvent
の後にいくつかのアクションを計画するかもしれません。
event.preventDefault()
の呼び出しは、ハンドラがそれらのアクションを実行すべきではないという信号を送信する方法です。
その場合、elem.dispatchEvent(event)
への呼び出しは false
を返します。そして、イベント生成コードは処理は継続すべきでないと知ります。
例えば、下の例では hide()
関数があります。それは要素 #rabbit
で "hide"
イベントを生成し、すべての関係者に うさぎ(rabbit)が隠れることを通知します。
rabbit.addEventListener('hide',...)
で設定されたハンドラは、それについて学び、必要な場合には event.preventDefault()
を呼び出すことでそのアクションを防ぐことができます。すると、うさぎは隠れません。:
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<script>
// hide() は2秒後に自動で呼び出されます
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // このフラグがないと preventDefault が動作しません
});
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();
}
});
// hide in 2 seconds
setTimeout(hide, 2000);
</script>
イベント中のイベントは同期的です
通常、イベントは待ち行列に入れられてから処理されます。つまり: ブラウザが onclick
を処理しており、そのプロセスの中で新しいイベントが起きた場合(例えばマウスが移動したなど)、その処理は待ち行列に入れられ、対応する mousemove
のハンドラは onclick
の処理が終了したあとに呼ばれます。
注目すべき例外は、あるイベントが別のイベントから開始された場合(例えば dispatchEvent
の使用)です。そのようなイベントは直ちに処理されます: 新しいイベントハンドラが呼ばれ、それから現在のイベント処理が再開します。
例えば、以下のコードでは、 menu-open
イベントは onclick
の途中でトリガされます。
これは、 onclick
のハンドラの終了を待たずに直ちに処理されます:
<button id="menu">Menu (click me)</button>
<script>
// 1 -> nested -> 2
menu.onclick = function() {
alert(1);
// alert("nested")
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
document.addEventListener('menu-open', () => alert('nested'))
</script>
出力の順番は次のとおりです: 1 → nested → 2
ネストされたイベント menu-open
は document
で捕捉されることに注意してください。ネストされたイベントの伝搬や処理は、外部のコード(onclick
) に戻る前に完全に終了します。
それは dispatchEvent
についてだけでなく、他のケースも同様です。もしイベントハンドラが他のイベントを引き起こすメソッドを呼び出すと – これらも同期的に、ネストされた関数の要領で処理されます。
これが気に入らないとしましょう。 menu-open
や他のネストされたイベントとは無関係に onclick
を最初に完全に処理したいとします。
その場合、 dispatchEvent
(あるいは他のイベントを引き起こす呼び出し)を onclick
の最後に置くか、もっと良い方法として、それらをゼロ遅延の setTimeout
でラップします:
<button id="menu">Menu (click me)</button>
<script>
// 1 -> 2 -> nested
menu.onclick = function() {
alert(1);
// alert(2)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})), 0);
alert(2);
};
document.addEventListener('menu-open', () => alert('nested'))
</script>
これで現在のコードの実行後に、 menu.onclick
を含め、 dispatchEvent
を非同期に実行できます。つまりイベントハンドラが完全に切り離されました。
出力の順番は次の通りとなりました: 1 → 2 → nested
サマリ
イベントを生成するためには、最初にイベントオブジェクトを作成する必要があります。
汎用的な Event(name, options)
コンストラクタは、任意のイベント名と2つのプロパティを持つ options
オブジェクトを受け取ります。:
- イベントがバブルするべきであれば、
bubbles: true
。 cancelable: true
はevent.preventDefault()
が動作します。
他の MouseEvent
, KeyboardEvent
などのようなネイティブイベントのコンストラクタはそのイベントタイプに固有のプロパティを受け入れます。例えば、マウスイベントであれば clientX
です。
カスタムイベントの場合、CustomEvent
コンストラクタを使うべきです。それは detail
と言う名の追加のオプションを持っており、そこへイベント固有のデータを割り当てるべきです。そして、以降すべてのハンドラは event.detail
でそこにアクセスすることができます。
click
や keydown
のようなブラウザイベントを生成することは技術的には可能性ですが、大きな注意を払って使うべきです。
ハンドラを実行するために、ブラウザイベントをハックな方法で生成するべきではありません。 これは、ほとんどの場合、悪いアーキテクチャです。
ネイティブイベントは次のような場合に生成される場合があります:
- サードパーティライブラリが他のインタラクションの手段を提供していない場合に、動作させるための汚いハックとして。
- 自動テストの場合で、スクリプトの中で “ボタンをクリック” し、インタフェースが正しく反応するかを見るために。
独自の名前を持つカスタムイベントは、アーキテクチャ上の目的で、メニュー、スライダ、カルーセルなどの内部で何が起こるかを伝えるために生成されることがよくあります。