Chapter 08

エレメント

OJK
OJK
2021.08.18に更新

準備

勉強用フォルダの下に「08」という名前で作業フォルダを作成して、以下のファイルを作成し、ライブプレビュー画面(ブラウザ)とコンソールを準備してください。

雛形コード
index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chapter 08: エレメント</title>
</head>
<body>
  <h1>DOMとは</h1>

  <p id="dom" class="explain">Document Object Modelの略です。</p>

  <ul id="list">
    <li>D … Document</li>
    <li>O … Object</li>
    <li>M … Model</li>
  </ul>

  <label><input type="radio" name="dom">D</label>
  <label><input type="radio" name="dom">O</label>
  <label><input type="radio" name="dom">M</label>

  <p class="explain">
    画像も要素ノードの一種です。
    <a id="link" href="https://zenn.dev/ojk" target="_blank" rel="noopener">
      <img src="https://github.com/ojklab.png" alt="画像" width="200">
    </a>
  </p>

  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

console.log('Hello World!!');

JavaScript の基本的な文法は前チャプターまでに押さえましたので、本チャプターからは JavaScript で HTML ドキュメントを操作する方法を学んでいきます。

DOM

DOM とは「Document Object Model」の略称で、HTML または XML[1] ドキュメントにプログラミング言語から操作するため関数やオブジェクトのセット(API:Application Programming Interface)です。
JavaScript だけでなく、主要なプログラミング言語には DOM を扱うための API が用意されています。

DOM では、HTML ドキュメントは木構造(階層構造)で表現されており、DOM ツリー/DOM tree といわれます。例えば、本チャプターの雛形コード(index.html)の head 要素以下は次のように内部表現されています。

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chapter 08: DOMの基礎</title>
</head>
<head>
 ├ <meta> - @charset
 |  └ 空白
 ├ <meta> - @name - @content
 |  └ 空白
 └ <title>
    └ "Chapter 08: DOMの基礎"

要素・属性・テキストが木構造の接点(ノード/node)として表現されています。meta 要素の子ノードになっている「空白」というのは meta 要素の空要素を表しています。

これまで getElementById メソッドなどで JavaScript 側に呼び出していた「エレメント」というのは、この DOM ツリーでいう 要素ノード のことです。要素ノードは、JavaScript では Element オブジェクト として実装されています。ややこしいのですが、DOM ツリーや要素ノードというのは特定のプログラミング言語に依存しないモデル(概念)で、JavaScript で実際に扱っているのは Element オブジェクトです。

JavaScript で HTML ドキュメントを操作するというのは、DOM ツリーをオブジェクトを介して操作する行為です。DOM ツリーの要素ノード(≒Element オブジェクト)を getElementById メソッドで取得したり、createElement メソッドで生成したり、appendChild メソッドで DOM ツリーにつなげたりすることで HTML ドキュメントを変化させています。

インタフェースという概念を本書では扱っていないので、上記の説明はかなり端折られています。詳しく知りたい人はJavaScript とインタフェースを読んでみてください。

複数のエレメントを同時に呼び出す

本書でこれまで紹介してきたエレメント(Element オブジェクト)の呼び出し方法は、getElementById メソッドと querySelector メソッドの二つでした。

各 id 属性の値は HTML ドキュメント毎に固有であるため、同じ要素が 複数あっても、getElementById メソッドを使えば必ずいずれか一つの要素を呼び出せます。
雛形の index.html には以下のように p 要素が二つありますが、id 属性さえ付いていれば、

HTML
<p id="dom" class="explain">Document Object Modelの略です。</p>

<!-- 略 -->

<p>
  画像も要素ノードの一種です。
  <!-- 略 -->
</p>

getElementById メソッドで狙ったエレメントを呼び出せます。

JavaScript
const p1 = document.getElementById('dom');
console.log (p1.textContent);  // → Document Object Modelの略です。

しかし、querySelector メソッドの場合は CSS セレクタ表現を使って呼び出すので、複数の要素にマッチしてしまうこともあります。例えば、要素名 p で呼び出すと、

const p2 = document.querySelector('p');
console.log (p2.textContent);  // → Document Object Modelの略です。

有無を言わさず、一つ目の p 要素を呼び出しました。
二つ目の p 要素を取得するには document.querySelector('p:nth-of-type(2)') などと引数(CSS セレクタ)を工夫すれば可能ですが、HTML 側に id 属性を付けたほうが素直ですね。

querySelectorAll メソッド

しかし、複数の要素を一度に呼び出して一括処理をしたいような場合には、CSS セレクタにマッチした要素を全て取ってきて、「エレメントの配列」として扱えたほうが便利です。
それを実現するのが querySelectorAll メソッド です。このメソッドを使って、今度は li 要素を呼び出してみたいと思います。

HTML
<ul id="list">
  <li>D … Document</li>
  <li>O … Object</li>
  <li>M … Model</li>
</ul>
JavaScript
const lis = document.querySelectorAll('li');
console.log (lis);  // → NodeList(3) [li, li, li]

console.log (lis[0].textContent);  // → D … Document
console.log (lis[1].textContent);  // → O … Object
console.log (lis[2].textContent);  // → M … Model

querySelectorAll メソッドの戻り値を受け取った定数 lis をそのままコンソールに出力すると、NodeList(3) [li, li, li] と表示されたかと思います。これは NodeList オブジェクト を意味しています。NodeList オブジェクトはその名前のとおり、ノードのリスト(配列のようなもの)です。

NodeList オブジェクトは配列ではないのですが、配列と同じように NodeList[インデックス] による値の取得ができます。
また、for-of 文や forEach メソッドなどの繰り返し構文が使えます。

const lis = document.querySelectorAll('li');

for (const li of lis) {
  console.log(li.textContent);  // → D … Document → ...
  li.textContent = "DOM!!";  // HTMLが書き換わる
}

NodeList オブジェクトでは push/pop/shift/unshift や filter/map といった配列用のメソッドは使用できません。しかし、Array.from メソッド を使うことで普通の配列に変換することができます。そうすれば、配列の全てのメソッドが使用できます。

const lis = document.querySelectorAll('li');

const lisArray = Array.from(lis);  // NodeListを配列に変換
const newLis = lisArray.map((li) => li.textContent + '!!');
console.log(newLis);

よりシンプルなメソッド

複数のエレメントを同時に呼び出すメソッドは他にもいくつかあります。機能的には querySelectorAll メソッドだけで事足りるのですが、このメソッドは処理が重いので、実行速度が気になるときはシンプルなメソッドを使うとよいでしょう。

例えば、エレメントを要素名(タグ名)で呼び出すなら、getElementsByTagName メソッド が使えます。getElement ではなく、複数形の getElements なので注意してください(以下で紹介する全てのメソッドで同様です)。

const lis = document.getElementsByTagName('li');
console.log(lis);  // → HTMLCollection(3) [li, li, li]

for (const li of lis) {
  console.log(li.textContent);
}

const lisArray = Array.from(lis);  // 配列に変換
lisArray.forEach((li) => li.textContent = 'DOM!!');

戻り値が HTMLCollection オブジェクトとなっていますが、NodeList オブジェクトとほぼ同じものと考えておいて構いません。NodeList と同じように for-of 文が使えて、Array.from メソッドで配列に変換できます。

なお、該当するエレメントが一つしかない場合でも、戻り値は HTMLCollection オブジェクトで返ってきます。一つしかないことがわかっている場合は次のようにして直接 Element オブジェクトを取り出すとよいでしょう(NodeList でも同様です)。

const img = document.getElementsByTagName('img')[0];
console.log(img);  // → <img>

上記コードでは getElementsByTagName のメソッド呼び出しの後ろに直接 [0] を付けています。JavaScript ではこのような書き方も許されます。
次のような処理のイメージです。

const img = document.getElementsByTagName('img')[0];const img = HTMLCollection[0];const img = Element;

さて、id 属性でエレメントを呼び出せる getElementById メソッドがあるなら、class 属性で呼び出せるメソッドもありそうですね。getElementsByClassName メソッド がそれです。
class 属性は同じ名前が複数の要素に付けられるので、戻り値は(単体の Element オブジェクトではなく)HTMLCollection オブジェクトになります。

const ps = document.getElementsByClassName('explain');
console.log(ps);  // → HTMLCollection(2) [p, p]

さらに、同じ name 属性が付けられている要素を呼び出すときには getElementsByName メソッド が使えます。こちらは NodeList オブジェクトで返ってきます。
ラジオボタンやチェックボタンなどは必ず同じ name 属性が付けられているので、一括取得するのに便利です。

const inputs = document.getElementsByName('dom');
console.log(inputs);  // → NodeList(3) [input, input, input]

親子/兄弟ノードへのアクセス

ここまではエレメントを HTML 側から直接呼び出してくる方法を紹介してきましたが、エレメント(Element オブジェクト)を一つ呼び出せば、そこから親要素や子要素、兄弟要素を辿っていくこもできます。

親子ノードへのアクセス

雛形の index.html の次の部分に着目します。a 要素からみると、p 要素が親要素、img 要素が子要素に当たります。

HTML
<p class="explain">
  画像もエレメントノードの一つです。
  <a id="link" href="https://zenn.dev/ojk" target="_blank" l="noopener">
    <img src="https://loremflickr.com/300/300" alt="画像">
  </a>
</p>

a 要素を呼び出して、そこから親要素・子要素にそれぞれアクセスしてみましょう。

JavaScript
const a = document.getElementById('link'); // a要素
console.log(a);

const p = a.parentElement;  // 親要素
console.log(p);

const img = a.firstElementChild;  // 最初の子要素
console.log(img);

エレメントの parentElement プロパティ から親要素にアクセスすることができます。メソッドではなくプロパティなので、a.parentElement の後ろに ( ) は付きません。

同様に、firstElementChild プロパティ で最初の子要素にアクセスできます。親要素とは違って子要素は複数存在する場合があるため、最初と最後の子要素しか指定できません。最後の子要素を呼びたいときは lastElementChild プロパティ を使います。

なお、「子要素」ではなくて「子ノード」であれば、NodeList オブジェクトとして一括取得する childNodes プロパティ が用意されています。しかし、その値(NodeList オブジェクト)には要素ノードだけでなくテキストノードなども含まれるため、そこからさらに要素ノードだけを抽出する処理が必要になります。

HTML
<ul>
  <li>AAA</li>
  <li>BBB</li>
  <li>CCC</li>
</ul>
JavaScript
const ul = document.querySelector('ul');
console.log(ul.childNodes);
// → NodeList(7) [text, li, text, li, text, li, text]
// 改行文字がテキストノードとして li 要素の間に含まれる

兄弟ノードへのアクセス

現在の要素の次の兄弟要素には nextElementSibling プロパティ でアクセスできます。「Sibling」とは(性別を指定しない)兄弟姉妹を意味します。

HTML
<ul id="list">
  <li>D … Document</li>
  <li>O … Object</li>
  <li>M … Model</li>
</ul>
JavaScript
const ul = document.getElementById('list');  // 親要素 → ul

const li1 = ul.firstElementChild;  // 最初の子要素 → li1
console.log(li1.textContent);      // → D … Document

const li2 = li1.nextElementSibling;  // li1の次の兄弟要素 → li2
console.log(li2.textContent);        // → O … Object

const li3 = li2.nextElementSibling;  // li2の次の兄弟要素 → li3
console.log(li3.textContent);        // → M … Model

上記コードのように、firstElementChild プロパティで最初の子要素を取得できれば、そこから順番に兄弟要素を辿っていくことができます。

一つ前の兄弟要素には previousElementSibling プロパティ でアクセスします。ul 要素の lastElementChild プロパティ(最後の li 要素)から逆に辿ってみましょう。

JavaScript
const ul = document.getElementById('list');  // 親要素 → ul

const li3 = ul.lastElementChild;  // 最後の子要素 → li3
console.log(li3.textContent);     // → M … Model

const li2 = li3.previousElementSibling;  // li3の前の兄弟要素 → li2
console.log(li2.textContent);            // → O … Object

const li1 = li2.previousElementSibling;  // li2の前の兄弟要素 → li1
console.log(li1.textContent);            // → D … Document

ただ、この書き方では li 要素が多いときには破綻します。for 文を使って短く書くこともできるのですが、ちょっと複雑ですね。

const ul = document.getElementById('list');

for (let li = ul.firstElementChild; li != null; li = li.nextElementSibling) {
  console.log(li.textContent);
}

一応説明しておくと、ループカウンタ li を ul.firstElementChild で初期化して(最初の子要素 li が入る)、li = li.nextElementSibling という更新式でループカウンタ li を次の兄弟要素に置き換えていきます。nextElementSibling プロパティは次の兄弟要素がないときに null(ヌル) という値になるので、繰り返し条件を li != null として、li が null になるまで繰り返しています。

for-of 文にすっかり出番を奪われた感のある for 文ですが、このように初期化式と更新式を工夫することでコードを簡潔に書くことができます。ループカウンタは必ずしも数値でなくてもよく、更新式も演算代入演算子である必要はありません。

上記の for 文は現時点で理解できなくても構いません。今回の例のように兄弟要素を一気に取得したい場合は、通常、querySelectorAll('#list li') などとして li エレメントの NodeList を取得します。

エレメントの操作

DOM ツリーの操作は、既存のエレメント(要素ノード)を呼び出して変更するだけでなく、新しいエレメントを生成して追加したり、既存のエレメントを削除したりといったことも行われます。

追加

これまでも、createElement してから appendChild するという流れは何度か取り上げましたね。まずは復習から入ります。

HTML
<ul id="list">
  <li>D … Document</li>
  <li>O … Object</li>
  <li>M … Model</li>
</ul>
JavaScript
const ul = document.getElementById('list');  // 親要素 ul
const newLi = document.createElement('li');  // li要素を生成
newLi.textContent = 'DOM!!';

ul.appendChild(newLi);  // ul要素の最後尾にliエレメントを追加

上記のコードを実行すると、新しく生成した li 要素が ul 要素の最後の子要素として追加されます。appendChild メソッドは、引数で指定されたエレメントを「親要素の一番最後の子要素」として追加するメソッドです。

appendChild

この生成した要素を一番最後以外に追加するには、insertBefore メソッド を使います。「手前に挿入する」という名前のメソッドです。

// 参照エレメントの一つ前にエレメントを挿入する
親エレメント.insertBefore(追加エレメント, 参照エレメント);

2 番目の引数である参照エレメントの “一つ前” に追加エレメントが挿入されます。なお、参照エレメントは、必ず親エレメントの子要素でなければなりません。
具体例を見てみましょう。

const ul = document.getElementById('list');
const newLi = document.createElement('li');
newLi.textContent = 'DOM!!';

const li1 = ul.firstElementChild;  // ul要素の先頭の子要素

ul.insertBefore(newLi, li1);  // ul要素の先頭の子要素の手前に挿入

insertBefore

今度は生成した li 要素が ul 要素の先頭の子要素として追加されました。挿入位置の基準となる参照エレメントを ul.firstElementChild、つまり先頭の子要素としたからです。先頭の子要素の “一つ前” に挿入されたので、「DOM!!」が先頭に来ました。

では、2 番目の子要素(O … Object)を参照エレメントとすれば 2 番目の位置に挿入されます。

const li1 = ul.firstElementChild;   // 1番目のli要素
const li2 = li1.nextElementSibling; // 2番目のli要素
ul.insertBefore(newLi, li2); // 2番目のli要素の手前に挿入

ところで、このコードはわざわざ定数 li2 を用意せず、定数 li1 を使って次のように書くこともできます。

const li1 = ul.firstElementChild;   // 1番目のli要素
ul.insertBefore(newLi, li1.nextElementSibling);

これは見方によれば、「参照エレメント li1 の “一つ後” に挿入している」ともいえます。JavaScript には insertAfter というメソッドは存在しないので、ある要素の後ろに挿入したければ「参照エレメント.nextElementSibling」とすると覚えておけばよいでしょう。

ちなみに insertBefore メソッドの引数では「参照エレメント.nextSibling」というように nextElementSibling プロパティの “Element” の文字を抜いても構いません。

// 参照エレメントの一つ後にエレメントを挿入する
親エレメント.insertBefore(追加エレメント, 参照エレメント.nextSibling);

説明すると長くなるので、興味のある人は以下の詳細を見てください。

興味のある人へ:nextSibling で構わない理由

nextSibling プロパティは要素ノード以外のノード(テキストノードなど)も含めた「次のノード」を表すので、必ずしも「次の要素ノード」ではありません。事実、上記コードの insertBefore メソッド内の li1.nextSibling は 2 番目の li 要素ノードではなく、空白のテキストノードを指しています(li 要素のあとの “改行” です)。そのため、DOM ツリー上では微妙な位置にエレメントが挿入されてしまうのですが、DOM ツリーが HTML ドキュメントとして描画されるときには改行による空白テキストノードは無視されるので、結果として nextSibling でもうまく動作します。

移動

エレメントを生成せず、既存要素のエレメントを呼び出して appendChild や insertBefore するとどうなるでしょうか。

const ul = document.getElementById('list');

const li1 = ul.firstElementChild; // 1番目の要素
const li2 = li1.nextElementSibling; // 2番目の要素
const li3 = li2.nextElementSibling; // 3番目の要素

ul.appendChild(li1);  // 先頭要素が最後尾に移動する

先頭の子要素エレメント(定数 li1)を改めて appendChild すると、先頭から最後尾に場所が移動します。

既存要素をappendChild

insertBefore メソッドも同様です。挿入したエレメントが指定された場所に移動します。複製されて追加・挿入されるということはありません。

ul.insertBefore(li3, li2);  // 2番目と3番目の要素が入れ替わる

置換

エレメントを挿入や移動するのではなく、置き換えることもできます。

親エレメント.replaceChild(新しいエレメント, 古いエレメント);

置き換えられる古いエレメントは親エレメントの子要素でなくてはいけません。

const ul = document.getElementById('list');
const li1 = ul.firstElementChild;  // 1番目の子要素
const newLi = document.createElement('li');  // 新しいli要素
newLi.textContent = 'DOM!!';

ul.replaceChild(newLi, li1);  // 1番目の子要素と新しい要素を置換

削除

最後にエレメントの削除です。

親エレメント.removeChild(削除したい子エレメント);

また、削除については親要素を仲介しないシンプルなメソッドもあります。

削除したいエレメント.remove();

実例を見てみましょう。

const ul = document.getElementById('list');
const li1 = ul.firstElementChild;   // 1番目の子要素
const li2 = li1.nextElementSibling; // 2番目の子要素

ul.removeChild(li1); // 1番目の子要素を削除
li2.remove();        // 2番目の子要素を削除

なお、removeChild メソッドと replaceChild メソッドは、削除したり置き換えたりしたエレメントを戻り値として返します(remove メソッドは戻り値が undefined となります)。あとで再利用したいときには定数で受け取っておくとよいでしょう。

以下のサンプルコードは上記コードの最終行からの続きです。

const removedLi1 = ul.removeChild(li1);

removedLi1.textContent = 'Remoooved!!'; // 削除されてもまだ使える
document.body.appendChild(removedLi1);  // 再利用

削除されてもまだエレメントが使えるのは、削除という操作が「DOM ツリーから切り離す」ことを意味するからです。

脚注
  1. XML とは HTML とよく似たマークアップ言語で、ウェブに限られない汎用的な用途で使えるように W3C(ウェブに関するルールや枠組みを整備する国際団体)が規定したものです。 ↩︎