皆さまこんにちは!ウチイダユウゴです。
先日、お知り合いの方と一緒に勉強したJavaScriptのclickイベントの挙動について、
復習のために内容をまとめておくことにします!
はじめに要約
- イベントを登録した要素を取得するときはthis
- 実際に画面上でクリックされた要素を取得するときはeventオブジェクトのtarget
- label要素をクリックすると、for属性に登録されたフォーム要素のclickイベントも発生する
検証用のコードを用意しました
See the Pen Test_click event bubbling by y-uchiida (@y-uchiida) on CodePen.dark
この記事では、こちらのCodePenの内容についてまとめていきます。
やりたかったこと
- ポートフォリオの一つとして、飲食店の予約システムを作成したい
- カレンダーの中から予約を取る日付を選択させ、その日に空いている座席を表示するUIを自作する
- そのため、まずはカレンダーをクリックした際にその日付を取得する
…ということを目指しました。
実装方針
カレンダーの表組みを行うtd要素にdata-dateで日付情報を持たせ、td要素のclickイベントに日付を取得するイベントハンドラを設定することにしました。
また、tdの子要素としてラジオボタン(input[type=”radio”])とlabel要素をそれぞれ配置し、予約情報をサーバにpostできるようにしています。
本当は、作りたい機能がほかにもたくさんあったんですけど、ウチイダのポートフォリオじゃないので…とりあえず関連する部分だけ切り出してます!
clickイベントを作成する
めっちゃ恥ずかしいのですが、最初、以下のコードを書いて、「tdの要素じゃなくてlabelとinputが出てくる!なんで??」と悩んでしまったんですよね。(;^ω^)
$('td.available').click(function(e){
console.log(e.target);
});
/* 例)2019年12月1日のtdをクリックした場合
* -> <label for="date_20191201">1</label>
* -> <input type="radio" name="date" id="date_20191201">
* clickイベントが2回動作している??なぜ??
*/
tdのclickイベントを設定しているのに子要素が取得されてしまうし…
しかも2回clickイベントが実行されるし…
原因は2つありました。
event.targetはイベントの発生元の要素を取得する
まずひとつめ。eventオブジェクトのtargetプロパティは、イベントが登録されている要素自身ではなく、そのイベントが発生した実際の要素を取得します。
clickイベントであれば、実際にクリックされた要素ということですね。
画面上ではtdの子要素であるlabelが表示されており、マウスがホバーするのもクリックされるのもlabel要素です。
label要素がclickされると、イベントバブリングによって親であるtd要素のclickイベントが発火します。
バブリングした先のtd要素のイベントハンドラの中でも、実際にクリックされた要素を知りたい場合に利用できるのが、eventオブジェクトのtargetプロパティというわけですね。
イベントが登録されている要素はthisで取得できる
では、イベントハンドラが登録されている要素自身(今回でいえばtd要素)を取得するには?
その場合は、thisを利用することができます。
HTMLオブジェクトのイベントハンドラは、そのオブジェクトのメソッドという扱いになります。
メソッド内でのthisは、そのメソッドの持ち主であるオブジェクトを指します。
以下のサンプルは、clickイベントが登録されている要素をthisで取得して、自信を削除していますね。
thisの挙動は、書き始めると長くなってしまうので今回は深く立ち入りません…(;^ω^)
以上をふまえて、今回の仕様に沿うならば、e.targetではなくthisを使った方がよさそうです。
clickイベントは二度発生する ― labelのクリック誘発
ただし、もう一つ問題が残ります。
コードを以下のように修正しても、clickイベントが2回発生してしまう問題は解消しません。
$('td.available').click(function(e){
console.log(this);
});
/* 例)2019年12月1日のtdをクリックした場合
* -> <td class="sun available" data-date="20191201">...(中略)</td>
* -> <td class="sun available" data-date="20191201">...(中略)</td>
* やっぱりclickイベントが2回動作する!
*/
これは、label要素の仕様が原因でした。
label要素がクリックされると、紐づけされたinput要素のclickイベントも発生したとみなされるようです。
つまり、、、
- label要素自身のクリックがバブリングして、親要素のtd要素のclickイベントが発火
- label要素をクリックしたことでinput要素もクリックしたことになり、これがバブリングして親要素のclickイベントがもう一度発火
ということが起こっていたのです。
イベントバブリングを止めるstopPropagation()
これを回避するため、ラジオボタンのclickイベントがtd要素へ伝播しないようにする必要があります。
$('td.available').click(function(e){
console.log(this);
});
$('input[name="reserve_date[]"]').click(function(e){
/* labelがクリックされると、ラジオボタン(input)のクリックイベントも呼ばれます
* td要素のclickイベントが二重発火しないように、
* ラジオボタンのクリックイベントのバブリングを停止します
*/
e.stopPropagation();
});
/* 例)2019年12月1日のtdをクリックした場合
* -> <td class="sun available" data-date="20191201">...(中略)</td>
* td要素のclickイベントが、1回だけ呼び出されました!
*/
eventオブジェクトのstopPropagation()を呼び出すと、親要素へのイベントバブリングが止まります。
よって、label要素のclickイベントのみがtd要素に伝播し、td要素のclickイベントが1回だけ発火するようになりました。
あとは、td要素からdata-dateを取り出してあげれば当初の目的は達成できそうです!長かった~…
もうちょっときれいな実装を考えてみる
この記事を書きながら、「そもそもtd要素にdata-date持つのってどうなんだろう?」という考えがわいてきました。
どこにデータを持つのかは設計思想に依るのかもしれませんが…今回はclickイベントの処理がめんどくさいことになってしまうので、あまりよいやり方ではないような気がしています。
それならば、dataをinput要素に持たせておき、input要素のclickイベントにハンドラを設定したほうがすっきりしそうかなーなんて思いました。
また、予約したい日付に空いている席があるかはajaxでデータベースから取り出すことになるはずです。
データベースの予約データの日付と、input要素のvalueに設定されている日付が同じ形式になっているなら、いっそvalueの値でajaxのリクエストを投げるのでもよいかもしれませんね。
まあ、他の処理との兼ね合いがいろいろあるのかもしれないので、断定的なことは言えませんけどね。。。
ひとのやってることに口出しばかりしていないで、自分でも何か作らないとー! ٩( ‘ω’ )و
まとめと反省
event.targetとthisの挙動を勘違いして、散々いらぬ手間を取らせてしまいました。
うろ覚えでコーディングし始めるからいけないんですよね。。。
ちゃんと覚えていられないなら、まめに調べながらコーディングするようにしないとなあと思いました。
以上、本日はここまで!
最後までお読みいただきありがとうございました(/・ω・)/
コメント