【STEP.31】
DOMをマスターしよう!(Part.02)
~ノードウォーキング編~

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メソッド)

このページでは、(2)の文書ツリー間の行き来(ノードウォーキング)について詳しく解説しています。

(1)の要素/属性/プロパティの操作については参考ページをご覧ください。
(3)のイベントドリブンモデル(addEventLintenerメソッド)については参考ページをご覧ください。

親子/兄弟要素の間を行き来する

現在の要素を基点として、親子/兄弟要素を取得するには、次のようなメソッドを利用します。

ツリー図は、現在の要素を基点として、それぞれのメソッドがどのような範囲の要素にアクセスできるかを表します。

要素の関係図

たとえば以下は、「id=”my”である要素」を基点として、親子/兄弟要素に対して、スタイルを設定する例です。

<div id="trav">
  波平
  <div>マスオ</div>
  <div>サザエ</div>
  <div id="my">カツオ
    <div>メダカ</div>
    <div>シラス</div>
  </div>
  <div>ワカメ</div>
</div>
let my = document.getElementById('my');
// 親要素
my.parentElement.style.border = 'groove 5px';
// 子要素群
for (let i = 0; i < my.children.length; i++) {
  my.children[i].style.fontSize = 'small';
}
// 最初の子要素
my.firstElementChild.style.color = 'Red';
// 最後の子要素
my.lastElementChild.style.color = 'Blue';
// 1つ前の要素
my.previousElementSibling.style.backgroundColor = 'Yellow';
// 1つ後の要素
my.nextElementSibling.style.backgroundColor = 'pink';

親子/兄弟要素に対してスタイルを適用

親子/兄弟ノードの間を行き来する

parentElementchildrenなどは、ノードの中でも要素だけを取得するためのプロパティでしたが、すべてのノードを対象にすることもできます。

具体的には、次に掲げる表のようなプロパティを利用します。

【要素以外のノードも取得するプロパティ】
プロパティ概要
parentNode親ノードを取得
childNodes子ノード群を取得
firstChild最初の子ノードを取得
lastChild最後の子ノードを取得
previousSibling直前のノードを取得
nextSibling直後のノードを取得

たとえば「id="my"である要素」の子ノード群を取得するのが、以下のコードです。

<div id="trav">
  波平
  <div>マスオ</div>
  <div>サザエ</div>
  <div id="my">カツオ
    <div>メダカ</div>
    <div>シラス</div>
  </div>
  <div>ワカメ</div>
</div>
let my = document.getElementById('my');
for (let i = 0; i < my.childNodes.length; i++) {
  console.log(my.childNodes[i]);
}

結果

結果に「カツオ」「#text」のような結果が追加されていることを確認してください。

これは子要素(<div>)の間にあるテキスト/改行/タブがテキストノードと見なされるためです。

childNodesプロパティでは、要素だけでなく、テキストも拾っているわけです。

ノードの種類を判定する【nodeTypeプロパティ】

childNodesプロパティで取得したノードから、要素だけを絞り込みたい場合には、以下のようなコードを書きます(最初から要素だけを取り出したいならば、childrenプロパティを利用すれば良いので、あくまでもサンプルのためのコードです)。

let my = document.getElementById('my');
for (let i = 0; i < my.childNodes.length; i++) {
  if (my.childNodes[i].nodeType === 1) {
    console.log(my.childNodes[i]);
  }
}

nodeTypeプロパティは、ノードの種類を次に掲げる表のような値で返します。

【nodeTypeプロパティの戻り値】
戻り値概要
1要素ノード
2属性ノード
3テキストノード
4CDATAセクション(<![CDATA[~]]>)
5実体参照ノード
6実体宣言ノード
7処理命令ノード
8コメントノード
9文書ノード
10文書型宣言ノード
11文書の断片(フラグメント)
12記法宣言ノード

この例では、nodeTypeプロパティが1(要素)である場合にだけ、その値を出力しています。

サンプルを実行すると、確かに、先ほどあった「カツオ」「#text」などの出力が消えていることが確認できるはずです。

新規に要素を作成する【createElement】

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

createElementメソッド

document.createElement(name)
name    要素名

たとえば以下は、<ul><li>要素に対して、新規の<li>要素を追加する例です。

<ul id="coordinate">
  <li>シャツ
</ul>
let ul = document.getElementById('coordinate');
let li = document.createElement('li');
li.textContent = 'ズボン';
ul.appendChild(li);

リストの末尾要素として追加

作成した要素(JavaScriptのソースコード2~3行目)は、その時点だとまだ、文書のどこにも関連付けていない、パズルのピースのようなものです。

これを文書にひも付けるのがappendChildメソッドの役割です(ul.appendChild(li);)。

appendChildメソッドは、指定された要素を現在の要素の最後の要素として追加します。

テキストをノードとして追加する【createTextNode】

上の例では、テキストをtextContentプロパティ経由で設定しましたが、createTextNodeメソッドでテキストノードを作成してから、これを<li>要素の子ノードとして追加することもできます。

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

let text = document.createTextNode('ズボン');	
li.appendChild(text);
ul.appendChild(li);

その他のcreateXxxxxメソッド

createXxxxxメソッドには、その他にも、生成するノードに応じて、次に掲げる表のようなメソッドが用意されています。

【主なcreateXxxxxメソッド】
メソッド概要
createAttribute(name)属性ノード
createCDATASection(data)CDATAセクション
createComment(data)コメントノード
createDocumentFragment()ドキュメントの断片
createElement(tag)要素ノード
createProcessingInstruction(target,data)処理命令ノード
createTextNode(data)テキストノード

新規の要素を任意の箇所に挿入する【insertBefore】

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

insertBeforeメソッド

node.insertBefore(inserted, ref)
inserted  挿入する要素
ref       挿入される個所

これで「node配下のノードrefの直前に、新規のノードinsertedを挿入する」という意味になります。

appendChildメソッドにも似ていますが、引数refの指定によって挿入位置を自在に指定できる点が異なります。

<ul id="coordinate">
  <li>シャツ
</ul>
let ul = document.getElementById('coordinate');
let li1 = document.createElement('li');
li1.textContent = 'ジャケット';
let li2 = document.createElement('li');
li2.textContent = 'ズボン';
		
let p1 = document.createElement('p');
p1.textContent = 'ぼうし';
		
let p2 = document.createElement('p');
p2.textContent = 'くつ';

// 要素の直前(親要素から見て、子要素のulの前)
ul.parentNode.insertBefore(p1, ul);	
// 最初の子要素
ul.insertBefore(li1, ul.firstElementChild);
// 最後の子要素
ul.insertBefore(li2, null);
// 要素の直後(親要素から見て、子要素のulの次の要素の前)
ul.parentNode.insertBefore(p2, ul.nextSibling);

既存のリストに対して要素の挿入

引数refnullを指定した場合には、「後ろに何もない(=最後の子要素)」として追加されます。

そのため、「ul.insertBefore(li2, null);」は

ul.appendChild(li2);

としても同じ意味です(そして、そのほうがシンプルです)。

既存の要素を移動させる【appendChild/insertBefore】

appendChildinserBeforeメソッドは既存の要素を移動するときにも利用できます。

上の「新規の要素を任意の箇所に挿入する【insertBefore】」の例では、appendChildinserBeforeに新規の要素(=文書ツリーにひも付いていない要素)を渡していましたが、要素を移動する際にはすでにある要素を渡すだけです。

これで、既存の要素を●●に挿入しなさい(=移動しなさい)という意味になります。

たとえば以下は、「id="shoes"」である要素を移動する例です。

<p id="hat">ぼうし</p>
<ul id="coordinate">
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
<p id="shoes">くつ</p>
let shoes = document.getElementById('shoes');
let coordinate = document.getElementById('coordinate');
coordinate.parentNode.insertBefore(shoes, coordinate);

「id=

複雑なコンテンツを動的に組み立てる【DocumentFragmentオブジェクト】

たとえば以下は、オブジェクト配列articlesをもとにリストを生成する例です。

<ul id="list">
</ul>
let articles = [
  {
    title: 'お絵描きチュートリアル',
    author: 'パク・リノ',
    url: 'https://www.amazon.co.jp/',
  },
  {
    title: '鬼滅の刃',
    author: '吾峠 呼世晴',
    url: 'https://www.amazon.co.jp/',
  },
  {
    title: '世界標準の経営理論',
    author: '世界標準の経営理論',
    url: 'https://www.amazon.co.jp/',
  }
];	
let list = document.getElementById('list');
// 配列articlesの内容を順に<li>要素に整形
articles.forEach(function (a) {
  let li = document.createElement('li');
  let anchor = document.createElement('a');
  anchor.href = a.url;
  anchor.textContent = a.title + '(作・' + a.author + ')';
  li.appendChild(anchor);
  list.appendChild(li);
});

オブジェクト配列から記事リストを生成

上のコードは正しく動作しますが、望ましくありません。

というのも文書ツリーに<li>要素を追加のたび(list.appendChild(li);)、内部的にはコンテンツを再描画するからです。

再描画はメモリ内部での操作に比べると、格段にオーバーヘッドも高いので、頻繁に発生するのは避けるべきです。

このような状況では、DocumentFragmentオブジェクト(フラグメント)を利用します。

組み立てたノードを一時的に格納するための仮の器、と言い換えることもできます。

以下は、先ほどの例を、DocumentFragmentを使って置き換えたものです。

// フラグメントを生成
let flagment = document.createDocumentFragment();
articles.forEach(function (a) {
        ...中略...
// 生成されたノードは一時的にフラグメントに格納
flagment.appendChild(li);
});
// フラグメントをまとめてページに反映
list.appendChild(flagment);

フラグメントを利用することで、文書ツリーそのものの更新は「list.appendChild(flagment);」の一度となります。

これによって、再描画にかかるオーバーヘッドを最小限に抑えられます。

既存の要素を別の要素で置き換える【replaceChild】

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

replaceChildメソッド

node.replaceChild(new, old)
new    置き換えるノード
old    置き換え前の子ノード

たとえば以下は、「id="hat"」である要素を、新たな<li>要素で置き換える例です。

<ul id="coordinate">
  <li id="hat">ぼうし
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
let hat = document.getElementById('hat');
let list = document.getElementById('coordinate');
let cap = document.createElement('li');
cap.textContent = '野球帽';
list.replaceChild(cap, hat);

「id=

要素を複製する【cloneNode】

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

cloneNodeメソッド

node.cloneNode(deep)
deep    子孫ノードまで複製するか

たとえば以下は、最初の<li>要素を複製し、リスト末尾に追加する例です。

<ul id="coordinate">
  <li class="wear">ぼうし
  <li>ジャケット	
  <li>シャツ
  <li>ズボン
</ul>
let list = document.getElementById('coordinate');
let li = list.firstElementChild.cloneNode(true);
list.appendChild(li);

先頭のli要素を複製し、末尾に挿入

cloneNodeメソッドを利用した場合、文書内でid値が重複する可能性があります。

複製を挿入する際には、事前にid値をチェックしてください。

異なる要素同士を入れ替える【cloneNode/replaceChild】

cloneNodeメソッドで対象の要素を複製したうえで、replaceChildメソッドで既存の要素を置き換えます。

<ul id="coordinate">
  <li>ぼうし
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
let list = document.getElementById('coordinate');

// 先頭/末尾の<li>要素を複製
let first = list.firstElementChild.cloneNode(true);
let last = list.lastElementChild.cloneNode(true);

// 先頭(複製)を末尾(既存)と、末尾(複製)を先頭(既存)と入れ替え
list.replaceChild(first, list.lastElementChild);
list.replaceChild(last, list.firstElementChild);

先頭のli要素と末尾のli要素を入れ替え

既存の要素を削除する【removeChild】

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

removeChildメソッド

node.removeChild(child)
child    削除する子ノード

以下は、<li>リストの配下から最初の<li>要素を除去する例です。

<ul id="coordinate">
  <li>ぼうし
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
let list = document.getElementById('coordinate');
list.removeChild(list.firstElementChild);

ulリストの最初の子要素を削除

自分自身を削除する

removeChildメソッドで、自分自身を削除することもできます。

以下は<ul>要素全体を削除します。

<ul id="coordinate">
  <li>ぼうし
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
let list = document.getElementById('coordinate');
list.parentNode.removeChild(list);

removeChildメソッドの引数childには、現在の要素に対して子供の関係にあるノードしか指定できません。

そのため、ここでもいったん、現在の要素の親ノードを取得したうえで、引数childに現在の要素を引き渡しています。

要素の中身を破棄する

上記のソースコードは、あくまでも要素そのものを削除します。

もしも要素の中身を破棄したい(=要素そのものは残したい)場合には、textContentプロパティに空文字列を設定します。

<ul id="coordinate">
  <li>ぼうし
  <li>ジャケット
  <li>シャツ
  <li>ズボン
</ul>
let list = document.getElementById('coordinate');
list.textContent = '';

コメントを残す

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