※スマホ対応はしてません。

タグ: Advent Calendar 2015

ツリーを歩き回るならTreeWalkerはどうかな。(DOMおれおれAdvent Calendar 2015 – 14日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 14日目分

2015-12-14

TreeWalkerというオブジェクトがあります。その名の通り、(DOMの)ツリーを歩き回るオブジェクトです。

例えばこんな構成。(インデント等によるテキストノードはないものとします。)

div#root
+ div#el-1
  + div#el-1-1
    + div#el-1-1-1

あるノード(#root)を起点に、長子の子孫を巡り(#el-1→#el-1-1→#el-1-1-1)、また戻ってくる(#el-1-1→#el-1→#root)という処理が必要だとします。

普通に関連ノードを参照して移動していくコードはこんな感じです。

var origin = document.querySelector('#root');
var current = origin;

// 子孫を巡る
while (current.firstChild) {
  current = current.firstChild;
  console.log(current.id);
}

// 祖先を巡る
while (current.parent && current !== origin) {
  current = current.parent;
  console.log(current.id);
}

何も問題ないように見えますか? 実はこれ、はい、何も問題ありません。まあ問題はないんですが、せっかくなのでこれをTreeWalkerを使っておきかえるとこんな感じです。

var origin = document.querySelector('#root');
var walker = document.createTreeWalker(origin);

// 子孫を巡る
while (walker.firstChild()) {
  console.log(walker.currentNode.id);
}

// 祖先を巡る
while (walker.parentNode()) {
  console.log(walker.currentNode.id);
}

TreeWalkerが指し示すノードは currentNode に格納されており、この内容がくるくる変わります。 parentNode() はプロパティではなくメソッドで、 walker の状態を変更する副作用を持ちます。

ちなみに戻り値に次の currentNode に相当するものを返します。対象が存在しない場合は null を返し、 currentNode の値を変更しません。

全部めぐる

せっかくなので長子だけじゃなくて全部歩き回るコードも載せておきます。

var walker = document.createTreeWalker(document.body);
walkThrough(walker);

function walkThrough(walker) {
  if (walker.firstChild()) {
    do {
      var node = walker.currentNode;
      // 要素ノードなら要素名を出力し、子の子を出力
      if (node.nodeType === node.ELEMENT_NODE) {
        console.log(node, node.tagName);
        walkThrough(walker);
      }
      // 文字列ノードなら文字列を出力
      else if (node.nodeType === node.TEXT_NODE) {
        var text = String.trim(node.nodeValue);
        if (text) {
          console.log('#', text.slice(0, 127));
        }
      }
    } while(walker.nextSibling());
    walker.parentNode();
  }
}

その他

  • document.createTreeWalker() で作ります。ルートになるノードの指定が必須です。
  • 第二引数でフィルターを指定する事ができます。
    • 例: document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT)
    • フィルターは | で区切って複数指定できます。
  • ルートとされたノードは walker.root に格納されます。
  • ルートとされたノードより上位へはたどれません。 walker.parentNode() が null を返します。

使い道

んー特に思い付きませんでした。

同じインスタンスを使い続けられるので、参照の管理が楽なのが利点?

環境

IE 9+。その他問題ないようです。

参考

NodeListとHTMLCollectionも別物なので気を付けよう。(DOMおれおれAdvent Calendar 2015 – 13日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 13日目分

2015-12-13

ノードと要素の違いに近いもので、NodeListとHTMLCollectionの違いというものもあります。

NodeList

document.querySelectorAll() の戻り値の、配列風オブジェクトです。普段使っているのがこちら。

HTMLCollection

対してこちらは何であるかと言うと、例えば document.forms がこれです。フォームを全て格納したやつですね。

HTMLCollectionもNodeListと同様、やはり配列風オブジェクトですが、加えて名前( id や name )によるアクセスも可能です。

<form id="foo"></form>
<form id="bar"></form>
console.log(document.forms[0]);      // <form id="foo"></form>
console.log(document.forms[1]);      // <form id="bar"></form>
console.log(document.forms['foo']);  // <form id="foo"></form>
console.log(document.forms['bar']);  // <form id="bar"></form>

forms については過去記事をどうぞ。

HTMLCollectionの出番

以下の場合がHTMLCollectionになります。

  • document.anchors
  • document.forms
  • document.images
  • document.links
  • document.getElementsByClassName()
  • document.getElementsByTagName()
  • node.children

他にもあるかも。(※NSはあまり使わない気がするのであえて外しました。)

ちなみに node.childNodes は自動的に更新されるNodeListです。ややこし。

children vs childNodes はこちら。

違い

NodeListは静的でありHTMLCollectionは動的である、という説明が一般的かと思います。

querySelectorAll() であるクラスを持つ全ての要素を全て引っ張ってきた後、JSからの操作により文書内の該当要素が増減等しても、先程取得しておいた結果は変わりません。

// あらかじめ <div class="foo"></div> が三つある状態

var fooList = document.querySelectorAll('.foo');
console.log(fooList.length);  // => 3

var elNewFoo = document.createElement('div');
elNewFoo.classList.add('foo');
document.body.appendChild(elNewFoo);

console.log(fooList.length);  // => 3 (変化なし)

一方で getElementsByClassName() で得た結果は更新されるので、常に最新の状態になります。

// あらかじめ <div class="foo"></div> が三つある状態

var fooCollection = document.getElementsByClassName('foo');
console.log(fooCollection.length);  // => 3

var elNewFoo = document.createElement('div');
elNewFoo.classList.add('foo');
document.body.appendChild(elNewFoo);

console.log(fooCollection.length);  // => 4 (変化あり!)

参考

ノードと要素は別物なので気を付けよう。(DOMおれおれAdvent Calendar 2015 – 12日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 12日目分

2015-12-12

ノードの子ノードのうち先頭のものを手早く取得するのには firstChild と firstElementChild の二種類があります。

<html><head></head><body></body></html>
var elHtml = document.documentEement;  // <html>
console.log(elHtml.firstChild);  // => <head>
console.log(elHtml.firstElementChild);  // => <head>

何が違うかと言うと、まあ読んで字の如くなんですが、後者は要素ノードに絞った中から先頭のものを返します。

例えばこんなHTML。

<ul>
  <li>This is the 1st item.</li>
  <li>This is the 2nd item.</li>
  <li>This is the 3rd item.</li>
</ul>

ここで el に上記 <ul> が格納されているものとすると、 el.firstElementChild は最初の <li> になります。想定通りと思います。一方 el.firstChild はというと、 <ul> と最初の <li> の間の「改行とインデント」から成る文字列ノードが格納されています。そう、文字列もノードです。

他にもこんな感じで「ノード全種から取得」するものと「要素ノードの中から取得」するものとがあります。

  • parentNode vs parentElementNode
  • children vs childNodes
  • firstChild vs firstElementChild
  • lastChild vs lastElementChild ?
  • previousSibling vs previousElementSibling ?
  • nextSibling vs nextElementSibling ?

まあ適宜使い分けましょう。

参考

全部挙げたら長くなった(笑)。

細かい文字列ノードが邪魔なら正規化 (normalize) してみては。(DOMおれおれAdvent Calendar 2015 – 11日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 11日目

2015-12-11

画面上では連続したひとかたまりの文字列でも、実は複数の文字列ノードから成っている場合もあります。普通にHTML書いてもそうはならんのだけど、JavaScriptからあれこれ挿入したりするとそうなります。

var el = document.createElement('span');

for (var i=0, l=10; i<l; i++) {
  var t = document.createTextNode(i + ' ');
  el.appendChild(t);
}

console.log(el.childNodes.length);  // => 10
// el = <span>0 1 2 3 4 5 6 7 8 9 </span>

そういうのをぱぱーっとまとめてくれるのが、 normalize() です。

var el = document.createElement('span');

for (var i=0, l=10; i<l; i++) {
  var t = document.createTextNode(i + ' ');
  el.appendChild(t);
}
el.normalize();

console.log(el.childNodes.length);  // => 1
// el = <span>0 1 2 3 4 5 6 7 8 9 </span>

ちなみに空の文字列ノードも削除してくれるみたいです。空白文字で構成される文字列ノードは残ります。

参考

各種ノードの作り方と表示のさせ方をまとめておきました。(DOMおれおれAdvent Calendar 2015 – 10日目)

カテゴリー: JavaScript

DOMおれおれAdvent Calendar 2015 – 10日目

2015-12-10

作り方

各種と言いながら三種類だけだけど。

実例は挿入の方に。

要素ノード

createElement(tagName) で作ります。

子として作るなら innerHTML も使えます。

文字列ノード

createTextNode(text) で作ります。文字列ノードの文字列の内容は後から nodeValue で取得、設定できます。

子として作るなら textContent も使えます。

文書断片 (document fragment)

createDocumentFragment() で作ります。

使い方は後で。

複製して作成

cloneNode() を使うと、既存のノードを複製して新しいノードを作成する事ができます。

// elLabel = <label class="super-label"><input type="checkbox" checked>同意する</label>

var elNewLabel = elLabel.cloneNode();
// elNewLabel = <label class="super-label"></label>

あ、ノードを複製するだけなので、子ノードは空です。子孫もまとめて複製する場合は引数でフラグを与えてやります。

// elLabel = <label class="super-label"><input type="checkbox" checked>同意する</label>

var elNewLabel = elLabel.cloneNode(deep);
// elNewLabel = <label class="super-label"><input type="checkbox" checked>同意する</label>

たくさんのノードを作るとき何度もDOM操作を繰り返すより、一度作って複製からの微調整、の方が高速かもね。まあそういうのはテンプレートエンジンに任せちゃった方が良いけれど。

あとは破壊的変更をしたいけど途中で(エラーとかで)中止するときとか。複製してからそっちを操作して、最後に差し替え。

挿入

末子として挿入

appendChild(el) で、自分の子要素の最後に挿入します。

// 親の要素ノードを作る
var elLabel = document.createElement('label');

// 要素ノードを作る
var elCheckbox = document.createElement('input');
elCheckbox.type = 'checkbox';
elCheckbox.checked = true;

// 文字列ノードを作る
var tLabel = document.createTextNode('同意する');

// 末子に挿入
elLabel.appendChild(elCheckbox);
elLabel.appendChild(tLabel);
// elLabel = <label><input type="checkbox" checked>同意する</label>

HTMLをまとめて挿入

innerHTML を使って、文字列から作成します。上↑と同じ結果になります。

// 親の要素ノードを作る
var elLabel = document.createElement('label');

// HTMLをまとめて挿入
elLabel.innerHTML = '<input type="checkbox" checked>同意する';
// elLabel = <label><input type="checkbox" checked>同意する</label>

(本当は checked のところがちょっと違うけども。)

要素を指定の位置に挿入

appendChild() だと常に最後に挿入されますが、最後じゃなくて途中に挿入する場合は insertBefore() を使います。

// 親の要素ノードを作る
var elLabel = document.createElement('label');

// 要素ノードを作る
var elCheckbox = document.createElement('input');
elCheckbox.type = 'checkbox';
elCheckbox.checked = true;

// 文字列ノードを作る
var tLabel = document.createTextNode('同意する');

// 先に末子にしたいノードを挿入してみる
elLabel.appendChild(tLabel);
// elLabel = <label>同意する</label>

// さっき挿入した要素の直前に挿入
elLabel.insertBefore(elCheckbox, tLabel);
// elLabel = <label><input type="checkbox" checked>同意する</label>

ちなみに insertAfter() はないです。参照ノードの次のノードが nextSibling で得られるので、これを使いましょう。

elLabel.insertBefore(elCheckbox, tLabel.nextSibling);
// elLabel = <label>同意する<input type="checkbox" checked></label>

参照ノードの次がないとき nextSibling は null になりますが、その場合 insertBefore() は末子に挿入します。 appendChild() と同じになるという事ですね。やったね。

要素をまとめて挿入

文書断片 (document fragment) を使うとまとめて挿入できます。

// elList =
//   <ul>
//     <li>1</li>
//     <li>2</li>
//     <li id="reference">5</li>
//   </ul>

// 挿入する要素を用意
var elItem3 = document.createElement('li');
elItem3.textContent = '3 (New!)';
var elItem4 = document.createElement('li');
elItem4.textContent = '4 (New!)';

// 文書断片を用意
var fragment = document.createDocumentFragment();
fragment.appendChild(elItem3);
fragment.appendChild(elItem4);

// まとめて挿入
var elReference = elList.querySelector('#reference');
elList.insertBefore(fragment, elReference);
// elList =
//   <ul>
//     <li>1</li>
//     <li>2</li>
//     <li>3 (New!)</li>
//     <li>4 (New!)</li>
//     <li id="reference">5</li>
//   </ul>

基本的に挿入系APIは要素や文字列のノードに限らず、文書断片ノードでも使えます。文書断片の場合はそれ自体ではなく、それに格納されているノードが挿入されます。

差し替えて挿入

replaceChild() で既存の要素を任意の要素に差し替える事ができます。

// el = <div><img src="img1.png" /></div>

var elNewImg = document.createElement('img');
img.setAttribute('src', 'img2.png');

el.replaceChild(elNewImg, el.firstChild);
// el = <div><img src="img2.png" /></div>

参考