筆者について
JavaScriptを勉強し始めて、1-2日目。(Java-Gold取得済み)
記事の内容
「[改訂新版]JavaScript 本格入門」 Chapter6 「HTMLやXMLの文書を操作する - DOM ( Document Object Model ) -」(山田祥寛, 出版 : 技術評論社) 以降を読みながら、メモをしたものなので、内容の誤記、誤字等に関しては、ご容赦いただいたうえで、ご指摘いただけると幸いである。またやんわりとHTMLなどに関する知識がないと難解な内容に感じるかもしれない。ご容赦。
これのつづき。
フォーム要素へのアクセス
フォームはエンドユーザーから入力を受けとる代表的な手段。入力ボックスや、チェックボックス、ラジオボタンなどいろいろある。
入力ボックス・選択ボックスの値を取得
<form>
<label> for="name">名前:</label>
<input id="name" name="name" type="text" size="30" />
<input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
document.addEventListener('DOMContentLoaded', function(){
document.getElementById('btn').addEventListener('click', function(){
var name = document.getElementById('name');
console.log(name.value);
}, false;
}, false);
ボタンをクリックすると、フォーム値をログに列挙する。
チェックボックスの値の取得・設定
入力ボックスの値の取得より複雑で、id属性が異なるが、
name属性が共通のチェックボックスの値の取得には、
getElementsByNameメソッドを使うのが良い。
<form>
<div>
好きな食べ物は?
<label>
<input type="checkbox" name="food" value="ピーマン" />
ピーマン
</label>
<label>
<input type="checkbox" name="food" value="きゅうり" />
きゅうり
</label>
<label>
<input type="checkbox" name="food" value="サンマ" />
サンマ
</label>
<label>
<input type="checkbox" name="food" value="寒天" />
寒天
</label>
<input id="btn" type="button" value="送信" />
</div>
</form>
document.addEventListener('DOMContentLoaded', function () {
document.getElementsById('btn').addEventListener('click', function () {
var result = [];
var foods = document.getElementsByName('food');
for (var i = 0, len = foods.length; i < len, i++) {
var food = foods.item(i);
if (food.checked) {
result.push(food.value);
}
}
window.alert(result.toString());
}, false);
}, false);
valueプロパティは選択の有無にかかわらず、value属性で指定された値を返すため、選択状況の確認には使えない。
ラジオボタンに関しても同様の処理で値を取得することができる。
一方でラジオボタンは選択肢の一つしかチェックをいれることができないため、選択済み要素が見つかり次第、for文から抜ける処理を書くのがベターだろう。
ラジオボタン/チェックボックスにチェックを入れるには、同じような繰り返し処理で、指定したvalue値と等しい要素のcheckedプロパティをtrueにすれば良い。
これらの処理方法と、それぞれのフォームのプロパティ値を合わせて、応用することができる。
//複数選択できるリストボックスの選択肢を返す
elem.options;
//チェックボックス・ラジオボタンの選択状況
elem.items(i).checked //Bool
//ファイル
file.name //ファイル名
file.type //コンテンツタイプ
file.size //サイズ(byte単位)
file.lastModifiedDate //最終更新日
画像ファイル、テキストファイル、バイナリファイルなどに加え、データをデータベースに保存することもできる。
ノードを追加、置換、削除
DOMでは既存ノードの参照のみならず、新規ノードの作成などもできる。
先にinnerHTMLについて挙げたが、これは
コンテンツが複雑になったときに、可読性が下がる可能性がある。
ユーザーからの入力によりコンテンツを作成する場合に、脆弱性となる恐れがある。
シンプルなコンテンツの編集でない限りは避けたほうが良いかもしれない。
新規ノードの作成
<form>
<div>
<label for="name">サイト名</label><br />
<input id="name" name="name" type="text" size="30"/>
</div>
<div>
<label for="url">URL : </label><br />
<input id="url" name="url" type="url" size="50" />
</div>
<div>
<input id="btn" type="button" value="追加" />
</div>
</form>
<div id="list"></div>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('btn').addEventListener('click', function () {
var name = document.getElementById('name');
var url = document.getElementById('url');
// <a>要素の生成
var anchor = document.createElement('a');
// <a>要素のhref属性の設定
anchor.href = url.value;
// テキストノードを生成し、<a>直下に追加
var text = document.createTextNode(name, value);
anchor.appendChild(text);
// <br>要素の生成
var br = document.createElement('br');
// <div id="list">を取得
var list = document.getElementById('list');
// <div>要素直下に<a>、<br>要素を順番で追加
list.appendChild(anchor);
list.appendChild(br);
}, false);
}, false);
フォームに入力した値に従って、ページ下部にリンクを追加するコード。
要素などの生成
createXxxxxメソッドは以下のようなものがあり、指定したノードを返す。
createElement(elem); //要素ノード
createAttribute(attr); // 属性ノード
createTextNode(text); // テキストノード
createCDATASection(text); // CDATAノード
createComment(text); // コメントノード
createEntityReference(enti); // 実体参照ノード
createProcessingInstruction(target, data); // 処理命令ノード
createDocumentFragment(); // ドキュメントの断片
※テキストノードに<>/などのような文字を入れると、自動的にエスケープされる。
ノードを組み立てる
elem.appendChild(node)
elem.insertBefore(node1, node2)
先に上げたコードでは、生成した、< a > 要素の属性を設定してから、指定した要素の下に子ノードとして追加している。
insertBeforeメソッドは、第一引数に指定したノードを第二引数で指定したノードの直前に挿入する。第二引数をnullにした場合、ノードを最後尾に挿入する。
コンテンツを追加する際には、再描画がを少なくするように処理をさせる。
再描画はオーバーヘッドが高い処理のため、発生させる回数は少なくする。
そのために
・DocumentFragmentオブジェクト上でコンテンツを組み立て
・まとめて文書ツリーに追加
という手順を踏む。
<ul id="list"></ul>
document.addEventListener('DOMContentLoaded', function () {
var books = [
{ title: 'Sample', price: 1980 },
{ title: 'Beispiel', price: 2300 },
{ title: 'Rei', price: 6890 }
];
var frag = document.createDocumentFragment();
//fragmentDocument上で組み立てて
for (var i = 0, len = books.length; i < len; i++) {
var b = books[i];
var li = document.createElement('li');
var text = document.createTextNode(b.title + ':' + b.price + '円');
li.appendChild(text);
frag.appendChild(li);
}
//まとめて文書ツリーに追加
document.getElementById('list').appendChild(frag);
}, false);
####既存のノードを置換・削除
//置換
elem.replaceChild(after, before)
elem:要素オブジェクト after:置き換え後のノード before:置き換え対象のノード
//削除
elem.removeChild(node)
node:削除対象ノード
//属性の削除
elem.removeAttribute(attr);
attr:属性名
繰り返し処理をする際の注意
HTMLCollection、NodeListは現在の状態を常に参照するため、
for(var i = 0; i < li.length; i++){
//<li>要素を追加する処理
...
}
のような処理があった場合、i < li.length
がfalseになることがなくなり、無限ループになる。
len = li.length ; i < len
のように初期化式で、現在の値を格納した変数を用意することが良い。
また基本的に、lengthプロパティの参照はオーバーヘッドが大きいため、常に先に上げたような、条件式が望ましいだろう。
スタイルシートの操作
DOMからスタイルシートにアクセスするのは、
・インラインスタイルへのアクセス
・外部のスタイルシートを適用する
の二つがある。
styleプロパティ
個々に適用されたインラインスタイルにアクセスする。
elem.style.prop[ = value]
elem:要素オブジェクト prop:スタイルプロパティ value:設定値
<div id="elem">マウスポインターを乗せると色が変わる</div>
document.addEventListener('DOMContentLoeded', funtion(){
var elem = document.getElementById('elem');
//ポインタが載ったときに背景色を変更
elem.addEventListener('mouseover', function() {
this.style.backgroundColor = 'YELLOW';
}, false);
//ポインタが離れたら背景色を戻す
elem.addEventListener('mouseout', function() {
this.style.backgroundColor = '';
}, false);
}, false);
JavaScript内でスタイルプロパティ名を指定するときは、html内で「-」を含んでいたものは、それを取り除き、2単語目以降の頭文字を大文字にして書き換える必要がある。
classNameプロパティ
styleプロパティは簡単に記述できる一方で、スタイルの定義とスクリプトが混在しているので、
可読性が低く、保守しづらい。変更が出たときに、変更すべき箇所がわかりにくい。
スクリプト側では、スタイルの関連付けを切り替える程度でとどめることが望ましい。
外部スタイルシートで定義されたスタイルにアクセス
elem.className [= clazz]
elem:要素オブジェクト clazz:スタイルクラス
<link rel="stylesheet" href="css/style.css" />
...
<div id="elem">マウスを乗せると色が変化</div>
.highlight {
background-color: YELLOW;
}
document.addEventListener('DOMContentLoaded', function () {
var elem = document.getElementById('elem');
elem.addEventListener('mouseover', function () {
this.className = 'highlight';
})
elem.addEventListener('mouseout', function () {
this.className = '';
}, false);
}, false);
以上のようにスクリプトでは、class属性の変更のみを行っている。また複数のクラスの関連付けの場合は
this.className = 'clazz1 clazz2';
のように、半角スペースで区切って列挙することで可能となる。
条件演算子「?」を使ってスタイルクラスの着脱もできる。
this.className = (this.className === 'highlight' ? '' : 'highlight');
classListプロパティ
classListプロパティを参照することで、class属性の値をDOMTokenListオブジェクトとして取得できる。DOMTokenListのメンバは以下のものがある。
length リストの長さ
item(idex) インテックスの番目のクラス取得
contains(clazz) 指定したクラスが含まれているか
add(clazz) リストにクラスを追加
remove(clazz) リストからクラスを削除
toggle(clazz) クラスのオン/オフの切り替え
<link rel="stylesheet" href="css/style.css" />
...
<div id="elem" class="line">クリックすると色が変化</div>
document.addEventListener('DOMContentLoaded', function () {
var elem = document.getElementById('elem');
elem.addEventListener('click', function () {
this.classList.toggle('highlight');
}, false);
}, false);
このように簡潔にわかりやすく記述することができた。
高度なイベント処理
イベントリスナ・イベントハンドラの削除
イベントハンドラの削除は
onxxxx = null
のように、イベントハンドラにnull値を設定することで削除できる。
イベントリスナの削除
elem.removeEventListener(type, listener, capture)
elem:要素オブジェクト type:イベントの種類 listener:削除するイベントリスナ capture:イベント伝播方向
削除するには、イベントリスナを指定しなければならないため、イベントリスナ作成時に匿名クラスで作成すると、指定することができず、削除することができない。そのため、あとから参照できるように関数オブジェクトとして格納しておく必要がある。
イベントオブジェクト
イベントハンドラ、イベントリスナは引数としてイベントオブジェクトを受けとることができる。
このイベントオブジェクトがもつプロパティにアクセスすることで、様々な情報にアクセスすることができる。
イベントオブジェクトにはイベント発生時の情報がまとめて格納される。
(ポインタ座標、イベント種類、押されたマウスボタン、キーボードの入力情報など)
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('btn').addEventListener('click', function (e) {
//target イベント発生元の要素
var target = e.target;
console.log(target.nodeName + target.id);
//type イベントの種類
console.log(e.type);
}, false)
}, false)
イベントオブジェクトを表す引数は一般的に「e」「ev」。
利用しない場合は、省略することもできる。
マウスポインタの位置を取得する場合に、
スクリーン上、ページ上、ブラウザの表示領域、要素内
の各それぞれの左端を原点に右、下の方向に向かっての座標を取得する。
screenX/screenY スクリーン上
pageX/pageY ページ上
clientX/clientY 表示領域上
offsetX/offsetY 要素領域上
イベント処理のキャンセル
イベントの伝播について
イベントが特定の要素に達するまでには
・キャプチャフェーズ
・ターゲットフェーズ
・バブリングフェーズ
のような手順を踏んでいる。
キャプチャフェーズでは最上位のwindowオブジェクトから文書ツリーをたどって、下位要素にイベントが伝播していく。
ターゲットフェイズでイベントとの発生元の要素を特定する。
バブリングフェーズでは、イベントの発生元から、ルート要素をたどって、イベントが伝播していく。
最終的に最上位のwindowオブジェクトに伝播していき、終了する。
イベントリスナ生成時の第3引数を
false
にすると、バブリングフェーズ時に、イベントが処理される。
→下位要素からイベントが実行。
true
にすると、キャプチャフェーズ時に、イベントが処理される。
→上位要素からイベントが実行。
イベントの伝播をキャンセル
e.stopPropagation()
メソッドによって、下位、上位要素へのイベントの伝播を中止する。
同じ階層のオブジェクトのイベントは実行される。バブリング、キャプチャが中止される。
e.stopImmediatePropagation()
メソッドは、その場で直ちに電波を中止する。
同じ要素に登録されたリスナがあっても、それも実行されない。
イベント本来の挙動をキャンセル
e.preventDefault()
メソッドを行うと、本来挙動(リンク先への移動など)が実行されなくなる。
これは、イベントオブジェクトのcancelable
プロパティがtrueの場合に可能であり、そうでない場合は、キャンセルすることができない。
イベントハンドラ、イベントリスナ下のthisキーワード
それぞれの下にある、thisキーワードは常に、
イベントの発生元を表す。
→メソッドの呼び出し先にthisキーワードが使用されていた場合、想定していた挙動とは違う挙動をする恐れがある。
そこで、bind
オブジェクトを利用する
func.bind(that [,arg1 [, arg2[, ...]]])
func:関数オブジェクト that:関数の中でthisキーワードが表すもの
arg1, ... :関数に渡す引数
document.addEventLitener('DOMContentListener', data.show, false);
というところを
document.addEventLitener('DOMContentListener', data.show.bind(data), false);
とすることで、showメソッドにあるであろう、thisキーワードが表す変数を、dataに指定することができた。
イベントリスナにEventListenerオブジェクトを指定
addEventListener
メソッドの第二引数には、function
オブジェクトを指定してきたが、
handleEvenrメソッドをもったオブジェクトを指定することもできる。
オブジェクトを指定することで、thisキーワードを持ったメソッドに対して、bindメソッドを用いることなく、想定した挙動を導くことができる。
document.addEventListener('DOMContentLoaded', function () {
var data = {
title: 'sample',
price: 1980,
handleEvent: function () {
console.log(this.title + this.price);
}
};
document.getElementById('btn').addEventListener(
'click', data, false);
}, false);