【STEP.32】
DOMをマスターしよう!(Part.03)
~addEventListenerメソッド編~

DOM(Document Object Model)とは

クライアントサイドJavaScriptプログラミングでは、「エンドユーザーや外部サービスからなんらかの入力を受け取って、これを処理した結果をページに反映させる」という流れが一般的です。

「ページに反映させる」とは、つまり、HTMLをJavaScriptで編集するということです。

この際、もちろん文字列として編集することも可能です。

しかし、一般的に、複雑な文字列の編集はコードも読みにくくなり、バグのもとともなります。

<div>要素やアンカータグといったかたまりで、オブジェクトとして操作できた方が便利です。

そこで登場するのがDOM(Document Object Model)です。

DOMは、マークアップ言語(HTML、XMLなど)で書かれたドキュメントにアクセスするための標準的な仕組みで、JavaScriptに限らず、現在よく利用されている言語のほとんどがサポートしています。

DOMは、Web技術の標準化団体であるW3C(World Wide Web Consortium)で標準化が進められ、現在は4つのレベル(Level1~4)があります。

また、時として「DOM Level 0」「DOM 0」という言葉が登場することもありますが、これはDOM Level 1が策定される前の(標準化されていない)ブラウザーオブジェクトのことです。

標準仕様として「DOM Level 0」が存在するわけではありません。

文書ツリーとノード

DOMは、ドキュメントを文書ツリー(ドキュメントツリー)として扱います。

たとえば、次のようなコードであれば、DOMの世界では図のようなツリー構造と解釈されることになります。

<!DOCTYPE html>  
<html>
<head>
  <meta charset="UTF-8">
  <title>ユタスク【Utask】</title>
</head>
<body>
  <p id="greet">これが<strong>文書ツリー</strong>です。</p>
</body>
</html>

dom.htmlのツリー構造

DOMでは、Document Object Modelという名前の通り、文書に含まれる要素や属性、テキストをそれぞれオブジェクトと見なし、「オブジェクトの集合(階層関係)」と考えるわけです。

ちなみに、文書を構成する要素や属性、テキストといったオブジェクトのことをノードと呼び、オブジェクトの種類に応じて要素ノード属性ノードテキストノードなどと呼びます。

DOMは、これらノードを抽出/追加/置換/削除するための汎用的な手段を提供するAPI(Application Programming Interface)といえます。

それぞれのノードは、ツリー上での上下関係によって、以下のように呼ばれることもあります。

これらの呼称は、ルートノードを除いて相対的なものです。

あるノードに対して子ノードだったノードが、別のノードに対して親ノードになることもあります。

【ノードの種類】
ノード概要
ルートノードツリーの最上位に位置するノード。最上位ノードと呼ぶ場合もあります。
親ノード/子ノード上下関係にあるノード。直接つながっているノードで、ルートノードに近いノードを親ノード、遠いノードを子ノードと呼びます(上下関係にあるが、直接の親子でないものを先祖ノードと子孫ノードと呼ぶ場合もあります)。
兄弟ノード同じ親ノードを持つノード同士。先に書かれているものを兄ノード、後に書かれているものを弟ノードとして区別する場合もあります。

クライアントサイドJavaScript(DOM)で開発を進めていくうえで、最初に押さえておきたい事項が以下の3つです。

  1. 要素/属性/プロパティの取得
  2. 文書ツリー間の行き来(ノードウォーキング)
  3. イベントドリブンモデル(addEventListenerメソッド)

このページでは、(3)のイベントドリブンモデル(addEventListenerメソッド)について詳しく解説しています。

(1)の要素/属性/プロパティの操作については参考ページをご覧ください。
(2)の文書ツリー間の行き来(ノードウォーキング)については参考ページをご覧ください。

addEventListenerメソッドとは

クライアントJavaScriptでは、アプリの中で発生した出来事(イベント)に応じて、なんらかの処理を実行するのが基本です。

このようなイベント処理のことをイベントリスナーといいます。

JavaScriptでイベントリスナーを設定するのは、addEventListenerメソッドの役割です。

addEventListenerメソッド

elem.addEventListener(type, listener, capture)
type      イベントの種類
listener  イベントに応じて実行する処理
capture   イベントの方向

たとえばマウスポインターが重なったときに、画像(id="pic1")を切り替え、外れたら元に戻すならば、次のようなコードを書きます。

<img id="pic1" src="https://bit.ly/2WVAvDx" />
let pic = document.getElementById('pic1');
	
// マウスポインターが画像に乗ったとき
pic.addEventListener('mouseenter', function (e) {
  this.src = 'https://bit.ly/2WUxDXF';
}, false);
	
// マウスポインターが画像から外れたとき
pic.addEventListener('mouseleave', function (e) {
  this.src = 'https://bit.ly/2WVAvDx';
}, false);

マウスポインターの出入りによって画像を切り替え

イベントリスナーの配下では、thisキーワードでイベントの発生元にアクセスできます。

この例であれば、thisと変数picとは同じ意味です。

アロー関数で記述した場合

ES2015以降の環境であれば、アロー関数を利用することもできます。

ただし、アロー関数を利用した場合、thisの示す先は、それを含む関数のthisとなります(この例であれば、documentオブジェクトです)。

thisが定義方法によって変換する点に注意してください。

pic.addEventListener('mouseleave' , (e) => {
  pic.src = 'https://bit.ly/2WVAvDx';
}, false);

onxxxxxプロパティにより設定した場合

イベントに対応する処理は、on xxxxxプロパティ(xxxxxはイベントの名前)でも割り当てられます。

たとえば、上のJavaScriptのソースコード4~6行目は、以下のように書いても、ほぼ同じ意味です。

pic.onmouseenter = function (e) {
  this.src = 'https://bit.ly/2WUxDXF';
};

ただし、on xxxxxプロパティでは

同一の要素/同一のイベントに対して、複数の処理をひも付けられない

という制約があります。

addEventListenerが導入される前の古い記法でもあるので、特別な理由がないならば、addEventListenerメソッドを利用すべきです。

ブラウザー上で利用できるイベントについて理解しよう

ブラウザー上で利用できるイベント一覧

ブラウザー上で利用できるイベントには、次に掲げる表のようなものがあります。

【ブラウザー上で利用できる主なイベント】
分類イベント発生タイミング
マウスclick要素をクリックした
dblclick要素をダブルクリックした
mousedownマウスのボタンを押した
mouseenter要素にマウスポインタ―が乗った
mouseleave要素からマウスポインタ―が外れた
mousemove要素の中をマウスポインタ―が移動した
mouseout要素からマウスポインタ―が外れた
mouseover要素にマウスポインタ―が乗った
mouseupマウスのボタンを放した
keydownキーを押した
keypressキーを押し続けている
keyupキーを離した
フォblur要素がフォーカスから外れた
focus要素にフォーカスが入った
focusin要素にフォーカスが入った(イベントバブリングあり)
focusout要素からフォーカスが外れた(イベントバブリングあり)
input要素の値を変更した(入力都度)
change要素の値を変更した(変更後、フォーカスを外したとき)
selectテキストボックス/テキストエリアのテキストを選択した
submitフォームから送信した
その他resizeウィンドウのサイズを変更した
scrollページや要素をスクロールした
contextmenuコンテキストメニューを表示する前

ほとんどが直感的に理解できるイベントばかりなので、以降では特筆すべきイベントについてのみ捕捉します。

mouseenter/mouseleaveとmouseover/mouseoutの挙動の違い

mouseentermouseleavemouseovermouseoutは、いずれも要素に対してマウスポインタ―が出入りしたタイミングで発生するイベントですが、微妙に挙動が異なります。

具体的には、次のような要素が入れ子になっている状況です。

イベントリスナーは、外側の要素(id="outer")に対して設定されているものとします。

この場合、mouseentermouseleaveイベントは対象要素の出入りに際してのみイベントが発生しますが、mouseovermouseoutイベントは内側の要素へ出入りしたときにも発生します。

思わぬ挙動に悩まないためにも、両者の違いを理解してください。

<div id="outer">
  外側(outer)
  <p id="inner">
    内側(inner)
  </p>		
</div>
<div id="result"></div>
let outer = document.getElementById('outer');
let result = document.getElementById('result');

// マウスポインタ―が領域に入ったとき
outer.addEventListener('mouseenter', function (e) {
  result.innerHTML = result.innerHTML + 'mouseenter:' + e.target.id + '<br />';
}, false);

// マウスポインタ―が領域から外れたとき
outer.addEventListener('mouseleave', function (e) {
  result.innerHTML = result.innerHTML + 'mouseleave:' + e.target.id + '<br />';
}, false);

/*// mouseover/mouseoutイベントの場合
// マウスポインタ―が領域に入ったとき
outer.addEventListener('mouseover', function (e) {
  result.innerHTML = result.innerHTML + 'mouseover:' + e.target.id + '<br />';
}, false);

// マウスポインタ―が領域から外れたとき
outer.addEventListener('mouseout', function (e) {
  result.innerHTML = result.innerHTML + 'mouseout:' + e.target.id + '<br />';
}, false);
*/

マウスを要素の左から右に横切るように動かした結果

focus/blurとfocusin/focusoutの挙動の違い

focusblurfocusinfocusoutの関係も、mouseentermouseleavemouseovermouseoutのそれによく似ています。

入れ子になった要素で、前者が内側の要素でフォーカスイン/アウトを認識できないのに対して、後者では内外の要素いずれへのフォーカスイン/アウトも認識できます。

<form id="fm">
  名前:<br />
  <input type="text" name="name" />
</form>
<hr />
<ul>
  <li>focus : <span id="focus">-</span>
  <li>blur : <span id="blur">-</span>
  <li>focusin : <span id="focusin">-</span>
  <li>focusout : <span id="focusout">-</span>
</ul>
let fm = document.getElementById('fm');
let focusin = document.getElementById('focusin');
let focusout = document.getElementById('focusout');
let focus = document.getElementById('focus');
let blur = document.getElementById('blur');
	
// フォーカスしたとき
fm.addEventListener('focusin', function (e) {
  focusin.textContent = '実行しました';
}, false);
	
// フォーカスを外したとき
fm.addEventListener('focusout', function (e) {
  focusout.textContent = '実行しました';
}, false);
	
/*//focus/blurの場合
// フォーカスしたとき
fm.addEventListener('focus', function (e) {
  focus.textContent = '実行しました';
}, false);

// フォーカスを外したとき
fm.addEventListener('blur', function (e) {
  blur.textContent = '実行しました';
}, false);
*/

テキストボックスにフォーカスを当てて、外した結果

focucblurイベントでは、テキストボックス(内側の要素)で発生したイベントは、外側の要素(フォーム)に伝播しません。

結果、テキストにも反映されません。

文書のロードが完了してからコードを実行したい【DOMContentLoadedイベントリスナー】

JavaScriptのコードは上から順に読み込まれ、実行されるのが基本です。

ということは、たとえば次のようなコードは正しく動作しないということです。

<script src="./scripts/load_bad.js"></script>
<p><a id="site" href="https://yutamanshop.com/">ユタスク</a>をよろしく。</p>
let link = document.getElementById('site');
link.style.backgroundColor = 'Yellow';

<script>要素が実行されるタイミングでは、<p>要素が読み込まれていないので、getElementByIdメソッドが目的の要素を取り出せないのです。

<script>要素をページ末尾に持ってきたり、defer属性を利用したりすれば問題は解決しますが、HTML側の記述によって動作が変化してしまうのはあまり望ましい状態ではありません。

そこで、JavaScriptで書かれたコード全体をDOMContentLoadedイベントリスナーでくくることをおすすめします。

document.addEventListener('DOMContentLoaded', function (e) {
  let link = document.getElementById('site');
  link.style.backgroundColor = 'Yellow';
}, false);

DOMContentLoadedイベントリスナーは、「ページがロードされたタイミングで処理を実行しなさい」という意味です。

これで、リスナー実行時に目的の要素が存在することを保証できるわけです。

ちなみに、よく似たイベントにloadがあります。

ただし、こちらはページそのものだけでなく、参照しているすべての画像がロードされたところで発生します。

一般的には、画像のロードを待つ必要はないはずなので、DOMContentLoadedを利用することで、スクリプトの開始タイミングを早められます。

既存のイベントリスナーを削除したい【removeEventListenerメソッド】

removeEventListenerメソッドを利用します。

removeEventListenerメソッド

target.removeEventListener(type, listener [,capture])
type      イベントの種類
listener  削除するイベント
capture   イベントの伝播方向

たとえば以下は、クリックイベントを登録した直後に、削除する例です。

<form>
  <input id="btn" type="button" value="ログを表示" />
</form>
let btn = document.getElementById('btn');
let onclick = function () {
  console.log('こんにちは、JavaScript!');
};
btn.addEventListener('click', onclick, false);
btn.removeEventListener('click', onclick, false);

removeEventListenerメソッドを利用する場合には、引数listenerで削除すべきリスナーを指定しなければなりません。

そのため、リスナー関数は(匿名関数ではなく)明示的に命名しておくようにしてください。

サンプルを実行し、ボタンをクリックしてもログが出力されないこと、続いて、「btn.removeEventListener('click', onclick, false);」をコメントアウトすることで出力されることを確認してください。

MEMO
引数captureまで一致していること
removeEventListenerメソッドでは、引数type/listenerだけでなく、引数captureもaddEventListenerメソッドでの宣言時と一致していなければなりません。

さもないと、異なるリスナー宣言と見なされて、削除は失敗します。

イベントに関わる情報を取得したい

すべてのイベントリスナーは、引数としてイベントオブジェクトと呼ばれるオブジェクトを受け取ります。

リスナーの配下では、イベントオブジェクトのプロパティにアクセスすることで、イベント発生時のさまざまな情報にアクセスできます。

たとえば以下は、ボタンをクリックしたときに、イベント発生元のタグ名、id値、イベントの種類、発生時刻などをログ出力する例です。

<div id="box">ここをクリックしてください。</div>
document.getElementById('box').addEventListener('click', function (e) {
  let area = e.target;
  console.log('発生元:' + area.nodeName + '/' + area.id);
  console.log('種類:' + e.type);
  console.log('タイムスタンプ:' + e.timeStamp);
}, false);
発生元:DIV/box
種類:click
タイムスタンプ:2535

イベントオブジェクトを参照するには、リスナーに引数を指定するだけです、

引数の名前は任意に決められますが、「e」「ev」「event」などとするのが一般的です。

イベントオブジェクトを利用しない場合には、引数は省略してもかまいません。

この例であれば、イベントオブジェクトを介して、target(イベント発生元)、type(イベントの種類)、timeStamp(イベント発生からの経過時間。ミリ秒)などの情報を取得しています。

イベント発生時のマウス情報を取得したい

イベントオブジェクトから、次に掲げる表のようなプロパティにアクセスします。

【イベントオブジェクトのマウス関連プロパティ】
プロパティ概要
buttonボタンの種類(0:左、1:中央、2:右)
screenXスクリーン上のX座標
screenYスクリーン上のY座標
pageXページ上のX座標
pageYページ上のY座標
clientX表示領域上のX座標
clientY表示領域上のY座標
offsetX要素領域上のX座標
offsetY要素領域上のY座標

それぞれの座標は、どこを基点とするかが異なります。

マウス関連プロパティ

それぞれのプロパティで得られる値を、具体的な例でも確認してみましょう。

<div id="main" style="position:absolute; margin:50px; top:30px; left:30px; width:300px; height:300px; border:solid 1px #000;"></div>
let main = document.getElementById('main');
main.addEventListener('mousemove', function (e) {
  main.innerHTML = 
    'screen :' + e.screenX + '/' + e.screenY + '<br />' +
    'page :' + e.pageX + '/' + e.pageY + '<br />' +
    'client :' + e.clientX + '/' + e.clientY + '<br />' +
    'offset :' + e.offsetX + '/' + e.offsetY + '<br />';
}, false);

「id=

イベント発生時のキー情報を取得したい

イベント発生時のキー情報を取得するには、イベントオブジェクトの次に掲げる表のようなプロパティにアクセスします。

【イベントオブジェクトのキ-関連プロパティ】
プロパティ概要
altkeyAltキーを押したか
ctrlKeyCtrlキーを押したか
shiftKeyShiftキーを押したか
metaKeyメタキーを押したか(Windowsではキー)
key押されたキーの種類

以下は、押下されたキーの種類を取得します。

押されたキーが制御/特殊文字の場合、あらかじめ決められたキー値を返します。

具体的な値は、Key Values(https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/key/Key_Values)のページも参照してください。

<form>
  キーボード入力:
  <input type="text" id="key" size="10" />
</form>
<p>入力したキーコード:<span id="code">-</span></p>
let key = document.getElementById('key');
let code = document.getElementById('code');
// キー押下時に、キーの種類を表示
key.addEventListener('keydown', function (e) {
  code.textContent = e.key;
}, false);

入力されたキーを表示

独自データ属性でイベントリスナーに値を渡したい

独自データ属性とは、タグに対して任意で付与できる属性のことです。

data-xxxxxの形式で開発者が自由に値を設定できます。

xxxxxの部分も、小文字のアルファベット+ハイフン+アンダースコアの組み合わせで自由に命名できます。

たとえば以下は、ボタンをクリックしたタイミングで、ログを出力する例です。

イベントリスナーは共通とし、押されたボタンによって表示テキストを変更している点がポイントです。

<input type="button" value="朝の挨拶" data-text="おはよう" />
<input type="button" value="昼の挨拶" data-text="こんにちは" />
<input type="button" value="夜の挨拶" data-text="こんばんは" />
let data = document.querySelectorAll('input[data-text]');
for (let i = 0; i < data.length; i++) {
  data[i].addEventListener('click', function (e) {
    console.log(this.getAttribute('data-text'));
  }, false);
}

ボタンに応じて異なるメッセージを表示(左から順にクリックした場合)

ここでは、上のHTMLのソースコード1~3行目のdata-textが独自データ属性です。

ログに表示すべきテキストをそれぞれ宣言しておきます。

リスナーに渡すべき情報の準備ができたら、あとは、上のJavaScriptのコードのように属性の有無で目的の要素を絞り込み、そのイベントリスナーを設定するわけです。

ここでは、data-text属性を持つ<input>要素についてclickイベントリスナーを設定し、ログに表示しています。

data-xxxxx属性の値は、getAttributeメソッド(console.log(this.getAttribute('data-text'));)で取得する他、datasetプロパティでアクセスすることもできます。

datasetプロパティ

elem.dataset.name
name    data-xxxxx属性の名前

nameには、data-xxxxxのxxxxxをcamelCase形式で指定します。(たとえばdata-valid-textであれば、validTextです。)

そのため、「console.log(this.getAttribute('data-text'));」のコードは以下のように表しても同じ意味です。

console.log(this.dataset.text);

イベントリスナーにパラメーターを渡したい

addEventListenerメソッドの第2引数に、(リスナー関数の代わりに)EventListenerオブジェクトを渡します。

EventListenerオブジェクトのルールは、

リスナー関数に相当するhandleEventメソッドを持つこと

だけです。

その他は、任意のプロパティを持てるので、これをパラメーターとして利用できるわけです。

たとえば以下は、あらかじめ用意されたオブジェクトmemberをリスナーとして渡す例です。

<input id="btn" type="button" value="クリック" />
// EventListenerオブジェクトを準備
let member = {
  mid: 'y001',
  name: '横浜流星',
  age: '23',
  handleEvent: function () {
    console.log(this.mid + ':' + this.name + '(' + this.age + '歳)'); 
  }
};
document.getElementById('btn').addEventListener('click', member, false);

ボタンクリック時にログを表示

thisの変化に注意

document.getElementById('btn').addEventListener('click', member, false);」を、以下のように書いてはいけません。

document.getElementById('btn').addEventListener(
  'click', member.handleEvent, false);

この場合、出力は「undefined:(undefined歳)」のようになります。

これは、メソッド(関数)として渡した場合、handleEvent配下のthisはイベントの発生元を表すためです。

イベントの発生元(ここではボタン)はmidageなどのプロパティを持たないので、undefinedを返すわけです。

EventListenerオブジェクトとして渡した場合には、handleEventメソッド配下のthisEventListenerオブジェクト自身で固定されるので、こうした問題は発生しません。

上記の問題は、bindメソッドを利用することでも回避できます。(もちろん、この例であればEventListenerオブジェクトを利用すれば良いので、あくまで説明のためのコードとして見てください。)

document.getElementById('btn').addEventListener(
  'click', member.handleEvent.bind(member), false);

bindメソッドの構文は、以下です。

bindメソッド

func.bind(that [,arg1 [,arg2 [,...]]])
that       関数の中でthisキーワードが指すもの
arg1,arg2  関数に渡す引数

bindメソッドを利用することで、関数func配下のthisを強制的に引数thatにひも付けできます。

この例であれば、thisがオブジェクトmemberを指すようになるので、今度はthis.nameageなどが、意図した値を返すようになります。

イベントの伝播について理解したい

イベントは、内部的には次のようなプロセスを経て、特定の要素に到達しています。

イベントの伝播

まずは、キャプチャフェーズで、最上位のwindowオブジェクトから文書ツリーをたどって、下位の要素にイベントが伝播します。(➀)

そして、ターゲットフェーズでイベントの発生元(ターゲット)を特定します。(➁)

バブリングフェーズは、ターゲットから再びルート要素に向かって、イベントが伝播するフェーズです。(➂)

イベントは、最終的に、最上位のwindowオブジェクトまで到達したところで伝播を終えます。

イベントリスナーを利用する際には、イベント発生元の要素でだけ実行されるわけでなく、

キャプチャ/バブリングの過程で、対応するイベントリスナーが存在する場合は、それらも順に実行される

という点を押さえておきましょう。

具体的な例を見ておきます。

<div id="outer">
  <p>外側(outer)</p>
  <a id="inner" href="https://amazon.com">内側(inner)</a>
</div>
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー1が発生');
}, false);
	
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー2が発生');
}, false);
	
document.getElementById('outer').addEventListener('click', function (e) {
  window.alert('#outerリスナーが発生');
}, false);

入れ子の関係にある<div><a>要素に対して、それぞれclickイベントリスナーを設定しています。

この状態でリンクをクリックすると、以下のような結果を得られます。

  • ダイアログ表示(#innerリスナー1が発生)
  • ダイアログ表示(#innerリスナー2が発生)
  • ダイアログ表示(#outerリスナーが発生)
  • リンクによるページ移動

ターゲットを基点として、文章ツリーの上位方向に向けて、順にリスナーが実行されています。

バブリングフェーズでリスナーが実行されている、とも言えます。

同じ要素に対してリスナーがひも付いている場合には、定義順に実行されます。

この挙動は、addEventListenerメソッドのの第3引数で変更できます。

上のJavaScriptのコード3行目の太字(false)をtrueに変更してみましょう。

以下のような結果が得られるはずです。

  • ダイアログ表示(#outerリスナーが発生)
  • ダイアログ表示(#innerリスナー1が発生)
  • ダイアログ表示(#innerリスナー2が発生)
  • リンクによるページ移動

今度は、上位要素からターゲットに向かって、順にリスナーが実行されています。

キャプチャフェーズでイベントが処理されているわけです。

ちなみに、addEventListenerメソッドの第3引数には、truefalse値の他、オブジェクトで動作オプションを渡すこともできます。

たとえば、上のJavaScriptのコード1~3行目は、以下のように書き換えても同じ意味です。

document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー1が発生');
}, { capture:false });

イベントの伝播をキャンセルしたい【stopPropagationメソッド】

イベントオブジェクトのstopPropagationメソッドを利用します

具体的な例を見てみましょう。

<div id="outer">
  <p>外側(outer)</p>
  <a id="inner" href="https://amazon.com">内側(inner)</a>
</div>
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー1が発生');
  e.stopPropagation();
}, false);
	
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー2が発生');
}, false);
	
document.getElementById('outer').addEventListener('click', function (e) {
  window.alert('#outerリスナーが発生');
}, false);

この状態でリンクをクリックすると、以下のような結果を得られます。

  1. ダイアログを表示(#innerリスナー1が発生)
  2. ダイアログを表示(#innerリスナー2が発生)
  3. リンクによるページ移動

stopPropagationメソッドによって、イベントのバブリングが中断されていることが確認できます。

もちろん、リスナーがキャプチャフェーズで動作している場合には、下位要素への伝播をキャンセルできます。

イベントの伝播を「直ちに」キャンセルしたい【stopImmediatePropagationメソッド】

上の例では、上位/下位要素への伝播を中止しました。

これを、その場で伝播を中止する(=同じ要素に登録されたリスナーも実行しない)には、stopImmediatePropagationメソッドを利用してください。

同じく、上のJavaScriptのコード3行目を、以下のように書き換えてみます。

e.stopImmediatePropagation();

実行結果は、以下のように変化します。

  1. ダイアログ表示(#innerリスナー1が発生)
  2. リンクによるページ移動

確かに、アンカータグに対して設定した2個目のリスナーも呼び出されなくなっていることが確認できます。

イベント本来の挙動をキャンセルしたい【preventDefaultメソッド】

イベント本来の挙動とは、たとえば「アンカータグのクリックによってページを移動する」「テキストボックスへの入力で文字を反映する」「サブミットボタンのクリックでフォームを送信する」など、ブラウザー標準で決められた動作のことです。

これらの動作を制御するのが、preventDefaultメソッドの役割です。

たとえばアンカータグを処理のトリガーとして利用したい(でも、ページを移動してほしくない)という場合には、リスナーの末尾でpreventDefaultメソッドを呼び出します。

これによって、リンクをボタンのように利用できるわけです。

実際の挙動も確認してみましょう。

<div id="outer">
  <p>外側(outer)</p>
  <a id="inner" href="https://amazon.com">内側(inner)</a>
</div>
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー1が発生');
  e.preventDefault();
}, false);
	
document.getElementById('inner').addEventListener('click', function (e) {
  window.alert('#innerリスナー2が発生');
}, false);
	
document.getElementById('outer').addEventListener('click', function (e) {
  window.alert('#outerリスナーが発生');
}, false);

実行結果は、以下のように変化します。

  • ダイアログ表示(#innerリスナー1が発生)
  • ダイアログ表示(#innerリスナー2が発生)
  • ダイアログ表示(#outerリスナーが発生)

すべてのリスナーが実行された後も、ページが移動しない(=リンクそのものの挙動が無効化される)ことが確認できます。

キャンセルの可否を確認したい【cancelableプロパティ】

ただし、イベントによってはpreventDefaultによるキャンセルを許可していないものもあります。

たとえばfocusinfocusoutなどです。

キャンセル可能なイベントであるかどうかは、イベントオブジェクトのcancelableプロパティで確認できます。

cancelableは、キャンセル可能な場合にはtrueを返します。

まだない要素にイベントリスナーを登録したい

addEventListenerメソッドを工夫することで、あとからページに動的に追加される(であろう)要素に対して、あらかじめイベントリスナーを登録して置くこともできます。

たとえば以下は、クリックするたびに増えていくボタンの例です。

元からあるボタンだけでなく、あとから追加されたボタンをクリックしてもボタンが増えます。

<form id="container">
  <input type="button" class="add-btn" value="増やす" />
</form>
let cont = document.getElementById('container');

// 監視したい要素をくくった領域(要素)に対してリスナーを登録
cont.addEventListener('click', function (e) {
  // イベント本来の発生元を確認
  if (e.target.classList.contains('add-btn')) {
    let btn = document.createElement('input');
    btn.type = 'button';
    btn.className = 'add-btn';
    btn.value = '増やす';
    cont.append(btn);
  }
}, false);

クリックの都度、ボタンが増殖

上のJavaScriptのコード4~13行目では、ボタンそのものではなく、その上位要素(ここでは<form id="container">)にイベントリスナーを設定しています。

この場合も下位要素で設定されたイベントは、バブリングのルールによって上位要素に伝播されます。

あとはtargetプロパティでイベントの発生元を確認し、(この例であれば)class属性にadd-btnが含まれていれば、本来のイベント処理を実施します。(上のJavaScriptのコード6~12行目)

このように、イベントリスナーの管理を上位要素に委ねることで、あとから追加された子要素にもイベント処理を適用できます。

イベントリスナーの登録

ここでは、<form id="container">要素の配下をイベント監視の対象としていますが、もしもページ全体を対象にしたい場合には、documentオブジェクトに対してリスナーを登録してください。

document.addEventListener('click', function (e) { ... }, false);

また、class属性ではなく、id属性で対象を特定したいならば、上のJavaScriptのコード6行目は、以下のように書き換えられます。(あまりないかもしれませんが、タグ名や属性値で対象を特定したい場合にも、同じ要領で条件式を書き換えます。)

if (e.target.id === 'btn') { ... }

パフォーマンス改善にも役立つ

この構文のメリットは、動的に追加された要素を認識できるというだけではありません。

対象となる要素が大量になった場合にも、イベントリスナーを効率的に登録できるという、パフォーマンス上のメリットもあります。

先ほどのを見ても分かるように、通常は、対象となる要素1つ1つに対してイベントリスナーを登録します。

しかし、本ページの書き方では、親要素に対して1つだけイベントリスナーを登録します。

その性質上、たとえば何十行、何百行に及ぶテーブルの個々のセルに対して、イベントリスナーを登録するようなケースでは、本ページの方法を利用することで、イベントリスナー登録のオーバーヘッドを軽減できます。

// △個々のセルにリスナーを登録
let tds = document.querySelectorAll('#tdl td');
tds.forEach(function (td) {
  td.addEventListener('click', function (e) {...});
});
// ○親テーブルに単一のリスナーを登録 let tdl = document.getElementById('tdl'); tdl.addEventListener('click', function (e) { if (e.target.nodeName === 'TD') {...} });

初回のクリック時にだけ処理を実行したい

addEventListenerメソッドのonceオプションを有効にします。

これによって、指定されたイベントを初回だけ検出し、イベントリスナーを実行できます。

2回目以降のイベント発生は無視されます。

<h2>今日の運勢</h2>
<input type="button" id="btn" value="占う" />
<p>今日の総合点は<span id="result">-</span>点です。</p>
let btn = document.getElementById('btn');
let result = document.getElementById('result');
btn.addEventListener('click', function (e) {
  // 0~100の値を反映
  result.textContent = Math.round(Math.random() * 100);
}, { once: true });

今日の運勢を0~100の値で表示(初回のみ、2回目以降は無視)

addEventListenerメソッドの動作オプション

addEventListenerメソッドの第3引数には、onceオプションの他にも、次に掲げる表のようなオプションを指定できます。

【addEventListenerメソッドの第3引数】
オプション概要
captureイベントをキャプチャフェーズで実行するか
passivepassiveモードを有効化

passiveオプションは、リスナーがpreventDefaultメソッドを呼び出さないことを宣言します。

scrollイベントでpassiveオプションを有効にすることで、ブラウザー(特にモバイル環境)ではイベントハンドラーの完了を待たずにスクロールを開始できるので、パフォーマンスを改善できます。

その性質上、passiveオプションをtrueに設定した状態で、リスナーからpreventDefaultメソッドを呼び出すと、preventDefaultは無視され、ブラウザーからも警告されます。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です