JavaScript中級者への道!DOMを操作するの巻

JavaScript
JavaScript

3月は個人的にJavaScript集中強化月間としてお勉強していきます。
今回はJavaScriptでHTMLのDOM操作をする方法をまとめていきます。

こちらのJavaScriptロードマップも併せてご覧ください。

目指せ中級!JavaScript学習ロードマップ(非webエンジニア向け)
当ブログの筆者のinashunです。 私はJavaをメインの技術としてお仕事をしているのですが、最近お仕事でJavaScriptを扱う機会が増えてきました。 これまでも扱う機会は多々あったのですが、すべて「既存ソースの改修」か「実装...

DOMとは

DOMはDocument Object Modelの略です。DOMはJavaScriptからHTMLの内容を操作するための仕組みです。
ブラウザに表示される画面というのはHTMLで記載されます。その画面に何らかの動きをつけてあげるためには、HTMLを操作してあげる必要があるのです。つまり、JavaScriptで動的なWebページを作ろうとするときに必要になる仕組みがDOMなのです。

もう少し具体的に説明すると、DOMはHTMLやXMLのタグの階層構造をオブジェクトとして表現し、JavaScriptなどの言語から操作できるようにしたAPIです。DOMはJavaScriptの技術要素の一部ではありません。例えばPythonからDOM操作を行うことも可能ですし、DOMで操作するのはHTMLではなくXMLでも構いません。
当記事では、JavaScriptからHTMLのDOMを操作する方法について記載しています。

DOMツリーとノード

DOMの仕組みを理解するうえで重要になるのがDOMツリーノードです。

<html><h1>などのHTMLの各要素は、必ず入れ子の構造になっています。
例えば、下記のようなHTMLファイルであれば、すべての要素は<html>とその閉じタグの</html>の入れ子となっていることがわかります。

<html>
    <head>
        <title>DOM SAMPLE</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h1>HELLO DOM WORLD!!</h1>
        <div>
            <p>DOM is beautiful.</p>
        </div>
    </body>
</html>

この入れ子の構造を階層構造に見立てて、要素同士を親子関係としてとらえるのがDOMツリーの考え方です。
上記のHTMLは、このようなDOMツリーになります。

ある要素に入れ子になっている要素を、その要素の子として親子関係を作り、その階層をツリー構造で表現したものをDOMツリーと呼びます。また、このツリー上での一つ一つの要素をノードと呼びます。

一般的に、一つの画面を構成するHTMLは、必ず一つのDOMツリーとして表現することができるはずです。

DOMでは、このノード単位でオブジェクトが生成されます。そのノード自体の情報(タグの種類、値、属性など)と、そのノードと親子関係(兄弟も含む)にあるノードの情報をオブジェクトが保持しています。これをJavaScriptから操作することで、画面に動きをつけるプログラムを書くことができます。

この仕組みを頭に入れておいたうえで、DOMの操作方法について学んでいきましょう!

当記事ではDOM操作はすべてプレーンのJavaScriptの書き方を紹介しています。jQuery等のライブラリやフレームワークでも独自の記法でDOM操作ができますが、この記事では紹介しません。また、Reactなどで用いられる仮想DOMの考え方についても当記事では扱いません。

ノードを取得する

まずはノードの取得からです。ノードの取得はDOMを操作するための入り口となります。使用頻度も高いので、優先して覚えていきましょう!

getElementById

要素のIDを用いてノードのオブジェクトを取得する関数です。

<p id="foo">Hello!</p>
const element = document.getElementById("foo");

documentは、簡単に言うとDOMツリー自体を表すJavaScriptのオブジェクトです。実行時に画面に表示されているHTMLの情報を常に保持しているので、いつでもアクセスすることができます。
getElementByIdメソッドは、このDOMツリーの中から引数で指定したidのノードを返却します。
返却されるのはElementオブジェクトです。これがノードのオブジェクトとなります。

操作の対象となる要素に、ページ内で一意となるようにidを振っておくことで、どのノードでもこのメソッドで取得できるようになります。

querySelector

querySelectorは、CSSセレクタを用いてノードを取得するメソッドです。

CSSセレクタとは、CSSをHTMLのどの要素に適用するかを指定する際に用いられる指定方法です。タグやid、付与されているclass、属性など、指定する条件を細かく設定できる書き方です。
そのCSSセレクタを用いてノードを取得することができるのがquerySelectorです。条件を自由に指定することができるので、痒いところに手が届きます!

<h1>タグがH1です</h1>
<p id="foo">idがfooです</p>
<p class="bar">classがbarです</p>
<p name="baz">nameがbazです</p>
const h1 = document.querySelector("h1");            // タグを指定するときはそのままタグ名
const foo = document.querySelector("#foo");         // idを指定するときは{#id}
const bar = document.querySelector(".bar");         // classを指定するときは{.class}
const baz = document.querySelector("[name='baz']"); // 任意の属性を指定するときは{[属性=値]}

querySelectorメソッドでは、セレクタに該当するノードが複数あった場合には、最初のノードを返却します。
該当するノードをすべて取得したい場合は、querySelectorAllメソッドを使用します。このメソッドを使用すると該当する要素のノードHTMLElementの配列が返却されます。

const pList = document.querySelectorAll("p");
for (const p of pList) {
    console.log(p.innerText);
}

特定の要素のノードにまとめて同じ処理をする場合に有効ですね。

CSSセレクタの参考

CSSのセレクタとは?覚えておきたい25種類と書き方
意外と知らない!?CSSセレクタ20個のおさらい

紹介したノード取得メソッドのほかにもgetElementsByTagNamegetElementsByClassNamegetElementsByNameなどの取得メソッドがあります。それぞれタグ、class、name属性の値を条件にノードを取得します。使い方はgetElementByIdとほぼ同じですが、複数のノードが該当する可能性があるため、HTMLCollectionという配列オブジェクトに格納されて返却されます。
  • querySelectorAllメソッドもノードを配列で返却しますが、こちらの戻り値はNodeListというものになります。HTMLCollectionは動的であるのに対しNodeListは静的であるという違いがあります。オブジェクト生成後にDOMの状態に変更があった場合に反映されるかされないかの違いがあります。

ノードウォーキング

ここまではdocumentオブジェクトのメソッドによってノードを取得する方法を紹介してきましたが、続いては取得したノードに関連するノードを取得する方法を紹介します。
そのようなノードから別のノードを取得する方法を、ノードを渡り歩くような様からノードウォーキングと呼びます。

例として、下記のようなHTMLをウォーキングしてみましょう!

<div class="parent">
    <p>pre</p>
    <div id="target">
        <p>one</p>
        <p>two</p>
        <p>three</p>
    </div>
    <p>next</p>
</div>

ウォーキングの起点となるのはidがtargetのdiv要素です。

const target = document.getElementById("target");

親ノード

まずは親ノードを取得してみましょう。
親ノードはparentNodeで取得できます。

const parent = target.parentNode;
console.log(parent.classList[0]); // parent

起点となるノードのオブジェクトのparentNodeプロパティに親ノードのオブジェクトが入っています。
あるノードの親ノードは必ず1つなので、HTMLElementオブジェクトがそのまま取得されます。

子ノード

続いて子ノードです。親ノードと違い複数のノードが該当するので、取得の方法も複数あります。

const firstChild = target.firstElementChild;
const lastChild = target.lastElementChild;
const children = target.children;

console.log(firstChild.innerText); // one
console.log(lastChild.innerText);  // three

for(const child of children) {
    console.log(child.innerText);  // one two three
}

firstElementChildlastElementChildはそれぞれ最初と最後の子ノードを返却します。該当するのはそれぞれ1ノードなのでHTMLElementが直接取得できます。
childrenはそのノードの子ノードがすべてHTMLCollectionに格納された状態で取得できます。

兄弟ノード

最後は同じ階層にある前後の兄弟ノードを取得します。

const pre = target.previousElementSibling;
const next = target.nextElementSibling;

console.log(pre.innerText);  // pre
console.log(next.innerText); // next

一つ前のノードはpreviousElementSibling、一つ後のノードはnextElementSiblingで取得することができます。該当するのはそれぞれ1ノードなのでHTMLElementが直接取得できます。

Documentプロパティ

Documentオブジェクトのプロパティとして直接取得できるノードも多くあります。一部だけになりますが紹介します。

document.documentElement<html>タグのノードを取得
document.head<head>タグのノードを取得
document.body<body>タグのノードを取得
document.forms<form>タグのノードのHTMLCollectionを取得
document.images<img>タグのノードのHTMLCollectionを取得
document.linkshref属性を持つ<area><a>タグのノードのHTMLCollectionを取得
document.scripts<script>タグのノードのHTMLCollectionを取得

中でも使う機会が多いかなというのはdocument.formsです。これについて使用例を簡単に紹介します。

<form name="hoge" action="https://example.com/">
    <input type="text" name="fuga">
    <button name="piyo">piyo</button>
</form>
const form = document.forms.hoge;
form.submit();

フォームにname属性が付与されていれば、document.forms.{formName}で特定のフォームのノードのElementを取得することができます。JavaScriptでフォームを操作することは頻繁にあるシナリオですし、この書き方は一番スマートでわかりやすいので是非覚えておきましょう。

ノードの中身を取得・編集する

ノードを取得することができたら、今度はそのノードの中身を取得したり、編集したりしましょう!
ここができるようになると、HTML上の値を使ってJavaScriptの処理を実行したり、ノードを編集して画面表示を動的に変更したりすることができます。

テキストを取得・編集する

あるElementのテキストを取得、編集するために3つのプロパティを使用することができます。それは、

  • innerText
  • textContent
  • innerHTML

の3つです。
この3つには少しずつ挙動に違いがあります。簡単にですが、その挙動の違いを見ていきましょう。

まずはテキストの取得から。
例えばinnerTextであれば、const text = element.innerText;という形式で取得ができます。

<h2>innerText</h2>
<p id="no1">
    Hello    Hello <strong>DOM</strong> &lt;World!&gt; <br>World!
</p>
<h2>textContent</h2>
<p id="no2">
    Hello    Hello <strong>DOM</strong> &lt;World!&gt; <br>World!
</p>
<h2>innerHTML</h2>
<p id="no3">
    Hello    Hello <strong>DOM</strong> &lt;World!&gt; <br>World!
</p>
console.log(document.getElementById("no1").innerText);
// Hello Hello DOM <World!>
// World!
console.log(document.getElementById("no2").textContent);
// Hello    Hello DOM <World!> World!
console.log(document.getElementById("no3").innerHTML);
// Hello    Hello <strong>DOM</strong> &lt;World!&gt; <br>World!

取得方法によってかなり違いがあることがわかります。innerTextであればスペースが埋められた状態で取得されたり、改行タグ<br>が改行に変換されて取得されています。textContentであればスペースの数はHTMLで定義されたそのままの数で出力され、改行コードは無視されるようです。innerHTMLであれば、テキストだけでなく子要素となるHTMLタグもそのまま取得することができます。

続いてテキストの編集です。
element.innerText = "text";という形式で、任意の文字列をテキストとして設定することができます。

<h2>innerText</h2>
<p id="no1"></p>
<p id="no2"></p>
<p id="no3"></p>
<h2>textContent</h2>
<p id="no4"></p>
<p id="no5"></p>
<p id="no6"></p>
<h2>innerHTML</h2>
<p id="no7"></p>
<p id="no8"></p>
<p id="no9"></p>
document.getElementById("no1").innerText = "Hello DOM World!";     // 普通のテキスト
document.getElementById("no2").innerText = "Hello\nDOM\nWorld!";   // 改行コード(\n)入り
document.getElementById("no3").innerText = "Hello <strong>DOM</strong> Wolrd!";   // HTMLタグ入り

document.getElementById("no4").textContent = "Hello DOM World!";
document.getElementById("no5").textContent = "Hello\nDOM\nWorld!";
document.getElementById("no6").textContent = "Hello <strong>DOM</strong> Wolrd!";

document.getElementById("no7").innerHTML = "Hello DOM World!";
document.getElementById("no8").innerHTML = "Hello\nDOM\nWorld!";
document.getElementById("no9").innerHTML = "Hello <strong>DOM</strong> Wolrd!";

innerTextでは、改行コード(\n)がHTMLの改行タグ<br>に自動的に変換されます。
innerHTMLでは、文字列中のタグを解析して反映してくれます。上記の場合だと<strong>タグを認識して文字を太字で表示してくれています。

このように、複数の方法でテキストの編集ができます。要件によってルールを決め使い分けるといいと思います。

ちなみに、テキスト要素はプロパティ変数なので、代入演算子が使えます。例えば、element.innerText += "HogeHoge";とすることで既存のテキストに追記をすることができます。element.innerText = "";とするとテキストが空文字になり要素が消えるような動きを再現することができます。

  • ノード自体を削除する方法は後ほど紹介します。

属性を取得・編集する

テキストの次は属性を編集していきましょう!

HTMLの属性とは、簡単に言うとそのタグの設定値のようなものです。例えば、下記の例の場合、属性hrefの値がhoge.html、属性idの値がhoge-linkとなります。

<a href="hoge.html" id="hoge-link">hogeへ</a>

属性がないとHTMLでの画面作成は成り立ちません!というくらい重要な要素なので、属性の編集をする場面も多くあります。その方法を学んでいきましょう。

プロパティを使って操作

ノードオブジェクトのプロパティを使用して取得・設定の操作をするパターンです。
element.idのようにノードオブジェクト.属性名の形式でアクセスすることができます。

<a href="https://google.co.jp" id="google-link">Googleへ</a>
const a = document.getElementById("google-link");
console.log(a.id);   // google-link
console.log(a.href); // https://google.co.jp

属性の値を書き換える場合は、a.href = "https://yahoo.co.jp";のようにすると変更できます。

メソッドを使って操作

ノードオブジェクトのメソッドを使用して属性の操作をする方法です。プロパティで操作するより少し煩雑な記法になりますが、存在チェックや削除などより多くの操作ができるようになります。

<a href="https://google.co.jp" id="google-link">Googleへ</a>
const a = document.getElementById("google-link");
// 属性値の取得
console.log(a.getAttribute("id"));    // google-link
console.log(a.getAttribute("href"));  // https://google.co.jp
// 属性の設定
a.setAttribute("style", "color: red;");
console.log(a.getAttribute("style")); // color: red;
// 属性の存在チェック
console.log(a.hasAttribute("id"));    // true
console.log(a.hasAttribute("class")); // false
// 属性の削除
a.removeAttribute("id");
console.log(a.getAttribute("id"));    // null

getAttributehasAttributeremoveAttributeはそれぞれ引数に属性名の文字列を指定することで、属性の取得、存在チェック、削除ができます。setAttributeは属性名と設定する値を引数として渡します。属性がすでにある場合はその値を上書きし、そのノードに存在していない属性名の場合は新規に属性を追加して値を設定します。

classを取得・編集する

classは、主に特定の要素にCSSを適用させるために付与する、いわば「HTMLタグにつけるタグ」のようなものです。このclassを活用することで効率よくCSSを適用していきます。
画面のデザインを動的に変更したいという場面はとても多くあると思います。そんなときにはこのclassの編集をしてそれを実現していきます。

classも属性の一つではあるのですが、設定される属性値のclassは複数になることがほとんどであるため、属性操作の方法では不便となってしまいます。このため特別なclass操作用のプロパティとメソッドが用意されています。こちらを使うようにしましょう。

ノードのクラスを操作するには、element.classListプロパティを使用します。
下記のHTMLとCSSを例に使い方を紹介していきます。

<div class="red-div" id="target">
    <p>Hello World!</p>
</div>
.red-div {
    background-color: #ff80ff;
    color: red;
}
.blue-div {
    background-color: aqua;
    color: blue;
}
.border {
    border:3px solid black;
}

JavaScriptの処理を一切挟まない状態だとこうなります。

まずは、classList.add()メソッドを使って枠線を付与するclassを追加してみます。

const div = document.getElementById("target");
div.classList.add("border");

クラス名を引数に指定することで、新しくクラスを追加することができます。

続いて、classList.remove()メソッドで、赤テーマのクラスを削除してみます。

const div = document.getElementById("target");
div.classList.add("border");
div.classList.remove("red-div");

追加したborderクラスのスタイルは削除されず、red-divクラスのスタイルだけが削除されたことがわかります。

続いて、classList.contains()メソッドでクラスの存在チェックをしてみましょう。

const div = document.getElementById("target");
div.classList.add("border");
div.classList.remove("red-div");

console.log(div.classList.contains("red-div")); // false
console.log(div.classList.contains("blue-div")); // false
console.log(div.classList.contains("border")); // true

メソッド呼び出し時点でclassListの中に引数で指定されたクラスが存在するかどうかを真偽値で返却します。削除されたクラスもfalseとなります。

最後に、classList.toggle()を紹介します。これはとても優れもので、引数で指定したクラスが存在すればそれを削除し、存在しなければそれを追加します。まるでスイッチをON/OFFするように使用できる便利なメソッドです。

const div = document.getElementById("target");
div.classList.add("border");

div.classList.toggle("red-div");
div.classList.toggle("blue-div");

これを使うことで、例えばボタンを押下するたびに表示を切り替えるような処理を簡単に作成することができます。
ちなみに、toggle()メソッドを使用せずに同様の処理を実装しようとするとこのようになります。

div.classList.toggle("red-div");

if (div.classList.contains("blue-div")) {
    div.classList.remove("blue-div");
} else {
    div.classList.add("blue-div");
}

5行の処理が1文に収まることになります。最高ですね!

ノードを作成・追加・削除する

いよいよ最後はノードを新しく作成してDOMツリーに追加して画面表示させます。また、ノードの削除も併せて紹介します。
ここまで覚えられればDOMを使いこなせると公言できるかなと思います。頑張りましょう!

ノードを作成する

ノードを作成するにはdocument.createElement()メソッドを使用します。

const p = document.createElement("p");

p.id = "js-create-p";
p.setAttribute("style", "color: green;");

p.textContent = "New PPP!!";

console.log(p); // <p id="js-create-p" style="color: green;">New PPP!!</p>

引数に作成するノードのHTMLタグを指定することで、そのタグのelementオブジェクトを取得できます。属性やテキスト、classの操作が可能です。

なお、作成をしただけの現段階ではこのノードは画面には反映されません。DOMツリーにこのノードを追加することで画面に反映されるようになります。

ノードを追加する

続いては狙った位置に作成したノードオブジェクトを追加して、画面に作成したノードを表示させましょう。

まずはelement.appendChild()メソッドです。これはメソッドを呼び出したelementオブジェクトの子要素の末尾に引数で指定したノードオブジェクトを追加します。

<div id="parent">
    <p id="child1">one</p>
    <p id="child2">two</p>
    <p id="child3">three</p>
</div>
const p = document.createElement("p");

p.id = "js-create-p";
p.setAttribute("style", "color: green;");
p.textContent = "New PPP!!";

const parent = document.getElementById("parent");
parent.appendChild(p);

作成したノードが末尾に追加されました!

ですが、末尾にしか追加できないのでは融通が利きません。任意の位置に追加するためにはelement.insertBefore()メソッドを使用します。これは引数で指定した子要素の前にノードを追加するメソッドです。

const p = document.createElement("p");

p.id = "js-create-p";
p.setAttribute("style", "color: green;");
p.textContent = "New PPP!!";

const parent = document.getElementById("parent");
const child1 = document.getElementById("child1");
parent.insertBefore(p, child1);

先頭に要素を追加できました。このいずれかを使用すれば、任意の位置に要素を追加できるようになります!

appendChild()insertBefore()メソッドで引数に指定する追加するノードのelementオブジェクトに、すでにDOMツリーに含まれていて画面に表示されているオブジェクトを指定することでDOM要素の移動をすることができます。この場合ノードは複製されるのではなく完全に移動します。元の位置にあったノードは削除され、新たにメソッドで指定した位置にノードが追加されるイメージです。

ノードを削除する

ノードを削除する場合には、element.removeChild()メソッドを使用します。親メソッドに対してメソッドを呼び出し、引数に削除するノードのelementオブジェクトを指定します。

const parent = document.getElementById("parent");
const child2 = document.getElementById("child2");
parent.removeChild(child2);

あったはずのtwoのpタグノードが削除されました。

親ノードのオブジェクトを取得していない状態で要素を削除する場合は下記のように書くと削除できます。

const child2 = document.getElementById("child2");
child2.parentNode.removeChild(child2);

ノードの削除ができるようになりました!これでDOM初心者は脱出ですね!

innerHTMLプロパティを使用することでここまでのノードの作成・追加・削除の方法を代替することも可能です。
例えばノードの作成・追加はparent.innerHTML = "<h2>Title</h2>";のようにすることで簡単に行うことができます。ノードの削除だとparent.innerHTML = "";とすることで子要素を一括で削除することが可能です。
単純な作成・追加・削除を簡単に記述することが可能な代わりに、複雑な要素の追加となると一気にコードが煩雑になるというデメリットもあります。状況に応じて使い分けることをオススメします。

JavaScriptでのDOM操作について学んできました。
DOM操作+イベント処理までを身につけられると、Web画面上で様々なアクションをプログラミングできるようになります。是非そこまで足を止めないように頑張って勉強していきましょう!

参考

JavaScriptによるDOM操作入門
【DOM基礎】ノードの取得/属性の取得・設定 – Qiita
【DOM基礎】要素内容の取得・設定/ノードの作成・挿入・削除 – Qiita
Document – Web API | MDN

コメント

タイトルとURLをコピーしました