jQuery:追加した「ボタン」をクリックしてもイベントが発火しない

この記事は約7分で読めます。
記事内に広告が含まれています

操作画面

こんにちは、さち です。

先日、「スクフェス ツールズ」をいじっているときに
jQuery で要素(入力欄)を動的に追加したんですが
その追加した要素に設定しているイベントが発火せずハマりました。

解決方法はそんなに難しくないですが
知らないとどうしようもない方法なので記事にまとめておきます。

スポンサーリンク

問題の症状

jQuery Mobile を利用したこのようなページがあります。
操作画面

HTML ソースはこんな感じ。

<div class="wrap">
  <button class="friends">たーのしー!</button>
</div>

<button class="push">追加</button>

続いて、下記のような jQuery(JavaScript) を作りました。
.friends を持つボタンをクリックすると内容がダイアログで表示されます。
「追加」ボタンをクリックすると新しいボタン「わーい!」が追加されます。
(trigger('create') ってなーに?というヒトはこちらの記事をどうぞ)

//「.friends」のボタンをクリックしたらダイアログを表示
$('.friends').click(function() {
  var txt = $(this).text();
  alert(txt);
});

//「追加」をクリックすると「わーい!」ボタンを追加
$('.push').click(function() {
  var $btn = $('<button>').addClass('friends').text('わーい!');
  $('.wrap').append($btn).trigger('create');
});

「たーのしー!」をクリックするとこうなります。
操作画面

「追加」をクリックして、新しく「わーい!」を追加しました。
操作画面

「わーい!」をクリックします。しかし、ダイアログが出ません。
えー、.friends を持っているボタンなのになんでー?
操作画面

解決方法

ダイアログを表示するコードを
click() ではなく on() を使って次のように書き換えます。

//変更前
$('.friends').click(function() {
  var txt = $(this).text();
  alert(txt);
});

//変更後
$(document).on('click', '.friends', function() {
  var txt = $(this).text();
  alert(txt);
});

これで、追加した「わーい!」をクリックしてもダイアログが出ます。
わーい!すごーい!
操作画面

問題の原因(追記)

原因は、イベントを設置する場所です。

click() は、実は bind() を簡略化した記述方法です。

//click()を使ってこう書くのは……
$('.friends').click(function() {
  var txt = $(this).text();
  alert(txt);
});

//bind()を使ってこう書くのとまったく同じ
$('.friends').bind('click', function() {
  var txt = $(this).text();
  alert(txt);
});

bind() は、指定したセレクター(場所)に直接イベントを設置します。
また、イベントの発火場所も同じセレクター(場所)になります。
つまり、bind() はイベントの設置場所と発火場所が同じです
例で言えば、イベントの設置場所と発火場所はともに .friends です。

$('.friends').bind('click', function() {
  var txt = $(this).text();
  alert(txt);
});

//bind()の構造
$('イベントの設置場所&発火場所').bind('イベント型', function() {
  /* 実行する内容 */
});

イベントの設置はページを開いた直後に行われます。
例で言えば .friend を持つ要素は2つありますが
bind() によりイベントが設置されるのは最初からある「たーのしー!」の方だけ。
後で動的に追加する「わーい!」の方には設置されません。
イメージ

この状態で「わーい!」の方をクリック(発火)します。
バブリングという仕組みによりクリックの情報が親(上)の要素に伝わりますが
経路上にイベントがないため、イベントは発生しません(不発)。
(「たーのしー!」をクリックした場合、同じ場所にあるイベントが発生します)
イメージ

正常に動作する on() を使った記述は下記のとおりでしたよね。
on() の場合、イベントは先頭で指定したセレクター(場所)に設置されます。
ただし、イベントの発火場所は on() の第二引数で指定したセレクター(場所)です。
つまり、on() はイベントの設置場所と発火場所を別々にできます
例で言えば、イベントの設置場所は document、発火場所は .friends です。

$(document).on('click', '.friends', function() {
  var txt = $(this).text();
  alert(txt);
});

//on()の構造
$('イベントの設置場所').on('イベント型', 'イベントの発火場所', function() {
  /* 実行する内容 */
});

この場合、ページを開いた直後のイベントの設置は document に行われます。
イメージ

この状態で「わーい!」をクリック(発火)すると
バブリングにより親(上)の要素に伝わり document にたどり着き
そこにあるイベントが発生します。
「たーのしー!」の方でも同様の理由でイベントが発生します。
イメージ

div.wrapbutton.push をクリックしてもイベント発火するんじゃ?』
と思うかもしれませんが、それはありません。
on() の第二引数で指定した場所での発火にしか反応しません。
つまり、例の場合では .friends 以外をクリックしてもイベントは発火しません。

すでに気づいている人もいるかもしれませんが
実は、イベントを設置する場所は document である必要はありません。
例の場合で言えば、div.wrapbody などの親要素でも構いません。
document が最上位なので面倒なことになりにくいというだけです。
(厳密には最上位ではないけれど話がややこしくなるから省略)

ちなみに、live()delegate() でも
後から追加した要素でイベントを発火できますが
現在は共に廃止や非推奨になっているので使わない方がいいです。
特別な理由でもない限り on() を使いましょう。

//live()をon()で再現
$('.friends').live('click', function(){});
$(document).on('click', '.friends', function(){});

//delegate()をon()で再現
$(document).delegate('.friends', 'click', function(){});
$(document).on('click', '.friends', function(){});

//click(),bind()をon()で再現
$('.friends').click(function(){});
$('.friends').bind('click', function(){});
$('.friends').on('click', function(){});

仕組みが分かれば今後はきっと大丈夫。
後から動的に追加したボタン等のイベントは
click()bind() などではなく $(document).on() を使う!
しっかり覚えておきたいと思います。

 

【関連記事】

→ jQuery Mobile:追加した要素(select等)にスタイルが付かない

コメント

  1. 匿名 より:

    多分
    $('.friends').on('click', function() {
    var txt= $(this).text();
    alert(txt);
    });
    でもいけますよー!!

  2. うみの さち より:

    試してみたんですが、その記述の on() ですと
    実質 bind() と同じ動きになるのでダメみたいです。

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