これでできる! クロスブラウザJavaScript入門

第7回 JavaScriptとHTMLとDOMの基本#2 イベント編

この記事を読むのに必要な時間:およそ 6 分

HTMLよるフォーム操作の導入



ここでJavaScriptがよく用いられるフォーム操作の実例(今回はあくまでよくない例なのでご注意を)を少しだけ見てみましょう。まず,あえてレガシーな書き方をしてみます。

レガシーなフォーム処理#1

<html>
<body>
<form name="cartform" onsubmit="return cart_check();">
<input type="checkbox" name="check" value="A">商品A
<input type="submit" value="送信">
</form>
<script>
function cart_check(){
  var form = document.cartform;
  if(!form.check.checked){
    alert('商品がチェックされていません');
    return false;
  }
}
</script>

サンプル1

こちらは購入したい商品にチェックして送信するフォームのサンプルです。フォームを送信しようとしたとき,商品がチェックされているかどうか,チェックしています。もしチェックされていなければcart_check関数はfalseを返し,onsubmitもfalseを返すことになります。イベントとして登録された関数自体がfalseを返すとき,そのイベントをキャンセルすることになっています。従って,フォームの送信がキャンセルされることになります。

しかし,この実装には少々問題があります。form要素をdocument.cartformで,さらにそのフォーム要素からform.checkのようにしてチェックした要素にアクセスしていますが,この方法では上手くアクセスできないケースがあります。具体的にはname属性の値が同じ要素がある時に,少々厄介な挙動をします。

レガシーなフォーム処理#1の失敗ケース

<html>
<body>
<form name="cartform" onsubmit="return cart_check();">
<input type="checkbox" name="check" value="A">商品A
<input type="checkbox" name="check" value="B" >商品B
<input type="submit" value="送信">
</form>
<script>
function cart_check(){
  var form = document.cartform;
  alert(form.check);
  if(!form.check.checked){
    return false;
  }
}
</script>

サンプル1-2

form.checkは同じnameを持つ要素があると要素配列(HTMLCollection)になり,1つだけの時は要素自身になります。この動作はバグの温床となります。そのためこの方法は避けるべきですが,あえてこの方針のまま修正すると次のようになります。

レガシーなフォーム処理#1の修正

function cart_check(){
  var form = document.cartform;
  var check = false;
  if(form.check.length > 1){
    for(var i = 0; i < form.check.length;i++){
      if (form.check[i].checked){
        check = true;
        break;
      }
    }
  } else {
    check = form.check.checked;
  }
  if(!check){
    alert('商品がチェックされていません');
    return false;
  }
}

このようにname属性が重複したときの処理が煩雑になってしまいます。こういったケースではdocument.getElementsByNameを使うことで少しすっきりさせることができます。

レガシーなフォーム処理#1-3

function cart_check(form){
  var check = false;
  var checks = document.getElementsByName('check');
  for(var i = 0; i < checks.length;i++){
    var checkbox = checks[i];
    if (checkbox.form == form && checkbox.checked){
      check = true;
      break;
    }
  }
  if(!check){
    alert('商品がチェックされていません');
    return false;
  }
}

サンプル1-3

もしくは,getElementsTagNameとname属性のチェックを組み合わせる方法もあります。

レガシーなフォーム処理#1-4

function cart_check(){
  var check = false;
  var form = document.getElementsByName('cartform')[0];
  var inputs = form.getElementsByTagName('input');
  for(var i = 0; i < inputs.length;i++){
    var input = inputs[i];
    if (input.name === 'check' && input.checked){
      check = true;
      break;
    }
  }
  if(!check){
    alert('商品がチェックされていません');
    return false;
  }
}

document.getElementsByNameはform要素が複数あるときに確認したい親フォームではないフォームに含まれる要素を含んでしまう可能性があるので,form属性をチェックしています。一方,form.getElementsByTagNameでは,親フォームが複数あっても動作しますが,今度はname属性をチェックする必要が出てきてしまいます。

上記ではあえてレガシーな書き方を選んでおり,クロスブラウザで動作はしますが将来的に動くかどうか,新しいブラウザで動くかどうかは少々不安があります。これをなるべくモダンな書き方に直してみましょう。

フォーム処理#2

<!doctype html>
<html>
<body>
<form id="cart" name="cartform">
<input type="checkbox" name="check" value="A">商品A
<input type="checkbox" name="check" value="B" >商品B
<input type="submit" value="送信">
</form>
<script>
var cart = document.getElementById('cart');
addEvent(cart,'submit',function(evt){
  var checks = document.getElementsByName('check');
  for(var i = 0; i < checks.length;i++){
    var checkbox = checks[i];
    if (checkbox.form == cart && checkbox.checked){
      check = true;
      break;
    }
  }
  if(!check){
    alert('商品がチェックされていません');
    if (evt.preventDefault) {
      evt.preventDefault();
    } else {
      evt.returnValue = false;
    }
  }
});
</script>

サンプル2

addEvent関数の定義を含めると少々コードが増えています。しかも,submitを制御していることがHTML上から確認できず,どこで定義されているのかも追いにくくなってしまっている面があります。onsubmitについてはHTMLに直接記述することも検討してもよいかもしれません(HTMLに直接記述する方法自体はHTML4.01,HTML5で定義されているので仕様的にも問題ありません⁠⁠。端的に言って,フォームが1つ以上チェックされているかどうかという処理はこれ以上簡潔にするのは難しい面があります。ここでjQueryを使って実装してみます。

フォーム処理#3

jQuery(function($){
  $('[name=cartform]').submit(function(evt){
    var check = !($(this).find('input[name="check"]').filter(function(){
      return this.checked;
    }).length);
    if (check){
      alert('商品がチェックされていません');
      return false;
    }
  });
});

サンプル3

かなりシンプルになりました。jQueryではonclick方式と同様にreturn falseでデフォルトイベントをキャンセルできる点がポイントです。

もう一つ,あえて古いブラウザへの対応を考えずに,HTML5,ECMAScript 5とDOM Level 2, 3などに準拠した新しいAPIを使った記述を試してみます。

document.addEventListener('DOMContentLoaded',function(){
  var forms = document.querySelectorAll('form[name=cartform]');
  Array.prototype.forEach.call(forms, formcheck);
  function formcheck(form){
    form.addEventListener('submit',function(evt){
      var checks = form.querySelectorAll('input[name="check"]');
      if (!Array.prototype.some.call(checks,checked)){
        alert('商品がチェックされていません');
        evt.preventDefault();
      }
    },false);
  }
  function checked(checkbox){
    return checkbox.checked;
  }
},false);

まず,querySelectorAllはHTML5で定義されるCSSのセレクタと同じルールで要素を取得できる便利なメソッドです。こちらはIE 8, Firefox 3, Safari 3.1, Chrome 1, Opera 10.0などがサポートしています。Array.prototype.forEachはECMAScript 5で追加された配列を走査するメソッドで,IE以外のブラウザは既にサポートしています。Array.prototype.someも同じくECMAScript 5で追加されたメソッドで,配列を走査して,最初にtrueを返したところで走査を中断してtrueを返し,1つもtrueを返さなかった場合はfalseを返すというメソッドです。やはり,IE以外のブラウザは既にサポートしています。2010年6月現在にリリースされているInternet Explorer Platform Previewでは,DOMContentLoadedとforEach, someをサポートしていないため動きませんが,将来的にはIEでも動くようになると期待できます。

イベントとinnerHTML



onclick方式でHTMLタグにイベントを記述した場合,そのタグがinnerHTMLなどで書き換えられたとしてもイベントが残るというメリットがあります。逆に,DOM方式はHTMLを書き換えた際にイベントが消えてしまうというリスクがあります。

イベント登録とinnerHTML

<div id="Parent">
<input id="url-alert" type="button" onclick="alert(location.href);" value="URLをアラート">
</div>
<script>
var Parent = document.getElementById('Parent');
var ua = document.getElementById('url-alert');
ua.onclick = function(){
  alert(location.protocol);
};
Parent.innerHTML = Parent.innerHTML.replace('href','pathname');
var new_ua = document.getElementById('url-alert');
alert([new_ua == ua, ua, new_ua]);
// false,[object HTMLInputElement],[object HTMLInputElement]
</script>

このサンプルではボタンにonclickイベントを2つの方法で登録しています。innerHTMLがなければprotocolがアラートされますが,innerHTMLによってJavaScriptで設定したイベントは消えてしまいます。これはイベントを設定した変数uaが参照している要素と,innerHTMLで書き換えられて作られた要素が別の要素として認識されるためです。また,変数uaが参照している要素はDOMツリーから切り離されていますが,変数としては参照されているのでガベージコレクションの対象とならず,場合によってはメモリーリークを起こすこともあります。このように,innerHTMLによる書き換えは影響範囲が大きいため慎重に行う必要があります。

まとめ



今回はJavaScriptでのイベント処理を中心に解説しました。次回は引き続きイベントとDOMについて実用的なコードを解説していきたいと思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/