DOMはHTMLの木構造(あとXMLも)やイベント等をプログラムから操作するためのAPIです。DOMの実装者として有力なのがウェブブラウザであり、ブラウザが提供するDOMのAPIを用いてJavaScriptプログラムからウェブページを制御するというのがDOMの極めてメジャーな使い道です。
近年知名度を増してきたReactやVueなどのライブラリも、もちろんこのDOMを用いて実装されています(Reactだとreact-dom
というあからさまな名前のパッケージがDOMを扱う部分を担当しています)。
この記事では、DOMで提供されるAPIを用いて木構造にノードを挿入する方法全38種類を列挙します。
ノードの挿入とは
DOMにおいて基本的な操作のひとつがノードの挿入です。ノードというのは木構造の最小構成単位であり、要素ノードやテキストノードなどの種類があります。例えば<u>は</u>
というのはu
要素を表すひとつの要素ノードであり(ここでは書きやすさのためHTMLタグで表現していますが、実際はタグの文字列ではなく木構造中のDOMオブジェクトのことをノードと呼びます)、は
というテキストノードを子として持っています。
例えば以下のdiv要素もひとつのノードであり、子ノードとして<b>こん</b>
と<i>にち</i>
の2つを持っています(改行文字のことはここでは考えません)。
<div>
<b>こん</b>
<i>にち</i>
</div>
では、このdiv要素の末尾に<u>は</u>
というノードを子ノードとして追加してみましょう。すると以下のようになります。
<div>
<b>こん</b>
<i>にち</i>
<u>は</u>
</div>
このとき<u>は</u>
がdiv要素の木構造に追加(挿入)されています。典型的には、Webページの木構造にノードを追加することでそのノードをページに表示することができますから、DOM操作においてノードの挿入はたいへん重要です。
なお、挿入という言葉に惑わされるかもしれませんが、木構造にノードが追加されていればそれは挿入です。中間的な場所でなくとも、木構造の一番最初や最後に追加するものも挿入と呼びます。では本題に入りましょう。
この記事では、このような操作を行う方法を可能な限り列挙します。その数は記事冒頭で述べた通り38種類(DOM仕様から14種類、HTML仕様から21種類、その他3種類)です。
DOM力に自信がある方は、ぜひご自身で考えてみてください。38種類全て思いついたらあなたはきっとDOMマスターです。
レギュレーション
上記の38種類というのは以下のレギュレーションに従って数えたものです。
-
HTMLおよびDOMの仕様によって定義されたものが対象です。
- すなわち、jQueryやReactなどのライブラリは今回の記事とは関係ありません。
- 仕様とは、WHATWGにより策定されているHTML Living Standard及びDOM Living Standardを指します。仕様がアップデートされて記事の内容が古くなってしまうことも考えられますが、そのときはぜひご指摘ください。
- また、これら2つの仕様がdependenciesとして指定している諸仕様についても含みます。
- これらの仕様でobsolete featuresと指定されているものは対象外とします。
- DOMを参照するその他の周辺仕様(W3Cが策定しているJavaScript APIとか)が子ノードを追加できる可能性もありますが、主に筆者が追いきれていないという理由でこの記事では対象外とします。
-
ノードを挿入する能力を持つ関数(メソッド)ひとつにつき1種類と数えます。
- あるメソッドがオプションによって異なる挿入結果になったり挿入されなかったりするようなことも考えられますが、同じメソッドを使っているならばそれは1種類と数えます。
- より厳密には、DOMやHTMLの仕様書において関数が呼び出された際の挙動は基本的にthe
someMethod()
method(, when invoked, ) must ~というような文によって定義されます。この定義文1つを1種類と数えるものとします。 - 関数呼び出し以外の操作の結果としてノードが挿入されることがあるかもしれませんが、その場合の数え方も上記に準じるものとし、その際もその操作によって起こることを定義する文ひとつを1種類と数えます。
-
ノードを挿入できる状況に制限があるものも数に含めます。
- 「特定の種類のノードしか挿入できない」や「特定の位置にしか挿入できない」といった制限を持つ方法が存在するかもしれませんが、こういったものも1種類と数えます。何らかの前提条件のもとでノードを木構造に挿入できるならばカウント対象となります。
- 「確かにノードを挿入できるがそれ以外の副作用が発生する」(別のノードが消えるとか)という方法もカウント対象です。
-
ノードを新規作成して同時に追加するものも数に含めます。
- ものによっては「ノードを新規に作成してそれを即座に木構造に追加する」という挙動をするものがあるかもしれません。この場合も「ノードを挿入する」という目的は達成しているので数に含めます。
-
木構造ごと生成されるものは数えません。
- 例えば「子ノードを最初から持った親ノード」を新規作成する方法があり、新規作成した親ノードに新規作成した子ノード追加したとも言えるかもしれませんが、それは数に数えません。あくまで既存の木構造に(既存または新規の)子ノードを追加するものをカウント対象とします。
-
document.write
は数えないでください。- あれはパースされる前の文字列を追加するものなのでノードを追加した扱いではないことにします。もちろん
document.writeln
もだめです。
- あれはパースされる前の文字列を追加するものなのでノードを追加した扱いではないことにします。もちろん
では、以下ではいよいよノードを挿入する方法を列挙します。皆さんは38種類全部言うことができましたか?
ノードを挿入する方法一覧
DOM編
DOM Living Standardで定義されているものをまずは列挙します。上記の通りDOM編は14種類です。
Node#appendChild
DOMでノードを挿入と聞いて一番にこれを思いつく方も多いでしょう。appendChild
がNode
が持つメソッドであり、与えられたノードを子ノードして親ノードの末尾に追加します。
同時に追加できる要素はひとつだけあり、追加できる場所は末尾だけです。扱えるノードの種類という観点では最も汎用的なメソッドのひとつであり、親ノードはDocument
, DocumentFragment
, そしてElement
の3種類、子ノードは DocumentFragment
, DocumentType
, Element
, Text
, ProcessingInstruction
, Comment
の6種類に対応しています。
const parent = document.createElement("div");
const child = document.createElement("span");
parent.appendChild(child);
console.log(parent); // <div><span></span></div>
Node#insertBefore
insertBefore
は子ノードを挿入する位置を指定できる、appendChild
の強化版です。第2引数に既存の子ノードを指定することにより、「そのノードの直前」という形で挿入位置を指定できます。
// parentの先頭にchildを追加する
parent.insertBefore(child, parent.firstChild);
Node#replaceChild
replaceChild
は既存の子ノードを別のノードで入れ替えるメソッドです。これも含めるのかとお思いかもしれませんが、既存の子ノードを取り除くという副作用がありつつも新しいノードを子ノードにする機能を持っているためこれもカウント対象です。
// parentの先頭子ノードをchildで置き換える(parent.firstChildが無い場合はエラー)
parent.replaceChild(child, parent.firstChild);
Node#textContent
これはメソッドではなく文字列プロパティです。textContent
の値を読んだときはそのノードに含まれる全文字列をつなげた文字列が得られますが、textContent
に新しい文字列を代入したときは、ノードの子がその文字列を表すテキストノード一つで置き換えられます。これもテキストノードを追加できたことになります。
const div = document.createElement("div");
div.textContent = "hi";
console.log(div); // <div>hi</div> ←divの子にテキストノードが追加された!
ParentNode#append
これはいわゆるDOM 4で新しく追加されたメソッドなので、最近追っていない方は聞き覚えが無いかもしれません。これはappendChild
と同様に好きな子ノードを末尾に追加できるメソッドですが、appendChild
に比べて2つの点で強化されています。
一つは引数を何個でも取れる点で、与えたノードたちがその順番で全て追加されます。
もう一つはノードだけでなく文字列を引数に与えることができる点で、文字列は自動的にテキストノードに変換できます。appendChild
でテキストノードを追加したい場合はdocument.createTextNode
でテキストノードを生成する必要があったのに比べてだいぶ便利ですね。
const parent = document.createElement("div");
const child1 = document.createElement("b");
const child2 = "hi"
const child3 = document.createElement("i");
parent.append(child1, child2, child3);
console.log(parent); // <div><b></b>hi<i></i></div>
ParentNode#prepend
prepend
も同様にDOM4で追加されたメソッドで、要素の末尾ではなく先頭に追加します。引数に渡した順番は保たれます(逆順になったりはしません)。
const parent = document.createElement("div");
const child1 = document.createElement("b");
const child2 = "hi"
const child3 = document.createElement("i");
parent.append(child1);
parent.prepend(child2, child3);
console.log(parent); // <div>hi<i></i><b></b></div>
ChildNode#before
これまたDOM4で追加されたメソッドで(これを含めてあと3個くらい続きます)、自身の兄弟を追加できるという特徴を持ちます。このbefore
は、自身の直前に兄弟ノードを追加します。
兄弟を追加ということですが、親から見れば新たな子ノードが追加されたことになりますね。複数の引数を与えることができるのはappend
やprepend
と同じです。
なお、兄弟という概念は親がいなければ成立しませんので、親の存在しないノードに対してこのメソッドを呼び出した場合はエラーとなります。
const parent = document.createElement("div");
const child1 = document.createElement("b");
parent.append(child1);
const child2 = document.createElement("i");
child1.before(child2, "hi");
console.log(parent); // <div><i></i>hi<b></b></div>
ChildNode#after
お察しとは思いますが、これは自身の直後に兄弟ノードを追加するメソッドです。
const parent = document.createElement("div");
const child1 = document.createElement("b");
parent.append(child1);
const child2 = document.createElement("i");
child1.after(child2, "hi");
console.log(parent); // <div><b></b><i></i>hi</div>
ChildNode#replaceWith
replaceWith
は、自分自身を引数で渡された別のノードで置き換えます。このメソッドもbefore
やafter
と同様、親ノードが存在しないと呼び出せません。
const parent = document.createElement("div");
const child1 = document.createElement("b");
parent.append(child1);
const child2 = document.createElement("i");
child1.replaceWith(child2, "hi");
console.log(parent); // <div><i></i>hi</div>
Element#insertAdjacentElement
要素を指定した位置に追加するメソッドです。位置は文字列で指定し、"beforebegin"
, "afterend"
, "afterbegin"
, "beforeend"
の4種類があります。意味はそれぞれ前の兄弟ノード、 後の兄弟ノード、最初の子要素、最後の子要素です。
やや古いメソッドであり、後方互換性のために残されています。そのため、いま使う機会はあまり無いかもしれません(ここまでで挙げたbefore
, after
, prepend
, append
の4つで事足りますしね)。
const parent = document.createElement("div");
const child1 = document.createElement("b");
parent.append(child1);
const child2 = document.createElement("i");
child1.insertAdjacentElement("afterend", child2);
console.log(parent); // <div><b></b><i></i></div>
Element#insertAdjacentText
上記のinsertAdjacentElement
と似たようなメソッドですが、引数として要素を受け取る代わりに文字列を受け取り、それを中身とするテキストノードを指定の位置に追加します。これもinsertAdjacentElement
と同様、今から使う理由はあまり無いでしょう。
const parent = document.createElement("div");
const child1 = document.createElement("b");
parent.append(child1);
child1.insertAdjacentText("afterbegin", "hi");
console.log(parent); // <div><b>hi</b></div>
ここまで読んだ方は「え? HTMLを構文解析してくれるinsertAdjacentHTML
の話は?」と思ったかもしれませんが、実はそれはDOM Parsing and Serializationという別の仕様の管轄となっているため紹介を後回しにします。
Text#splitText
これはちょっと難易度が高いです。知っていたらDOM通ですね。
splitText
はテキストノードを指定した位置で分割して2つに分けるメソッドです。親ノードがある状態でこのメソッドを使った場合は1つだった子ノードが2つに分割されたことになり、子ノードの追加が発生しています。
const parent = document.createElement("div");
// テキストノードを1つ追加
parent.append("Hello, world!");
// 6文字目でテキストノードを分割
parent.firstChild.splitText(6);
// 前半のあとに別のテキストノードを追加
parent.firstChild.after(" my");
console.log(parent); // <div>Hello, my world!</div> ← "Hello," " my" " world!" の3つのテキストノードを子に持つ
Range#insertNode
Range
はDOMが提供するオブジェクトのひとつで、木構造上の範囲を表したり操作したりできる機能を持ちます。あまり馴染みがないかもしれませんが、ユーザーがマウス等でテキストを選択した範囲を取得するときなどはRange
のお世話になっています。
insertNode
は、Range
の開始点に指定したノードを挿入できるメソッドです。
const parent = document.createElement("div");
parent.append("Hello, world!");
// rangeを新規作成
const range = document.createRange();
// 開始点をparentの最初の子要素("Hello, world!" テキストノード)の6番地に設定
range.setStart(parent.firstChild, 6);
// 開始点に<b></b>を挿入
range.insertNode(document.createElement("b"));
console.log(parent); // <div>Hello,<b></b> world!</div>
Range#surroundContents
こちらもRange
のメソッドで、Range
の範囲を与えられたノードで囲むという動作をします。Range
が表す範囲によってはノードで囲むという操作が不可能な場合がありますが、その場合はエラーとなります。
const parent = document.createElement("div");
parent.append("Hello, world!");
// rangeを新規作成
const range = document.createRange();
// rangeがparentの中身全体を囲むように設定
range.selectNodeContents(parent);
// rangeが表す範囲をb要素で囲む
range.surroundContents(document.createElement("b"));
console.log(parent); // <div><b>Hello, world!</b></div>
以上でDOM編は終了です。全14種類のうち何個分かりましたか?
HTML 編
HTML編はさらに魔境です。筆者の見逃しも普通にあるような気がするのでぜひ筆者よりもHTML仕様書を読み込んでばしばし指摘しましょう。
HTML編は全部で21個です。
document.title
document.title
は文字列プロパティで、現在のページタイトル(title
要素の中身)を取得したり変更したりできます。
HTML文書においてtitle
要素が存在しない状態でdocument.title
に文字列が代入された場合、新しいtitle要素が生成されたhead要素に挿入されるのです。
// 一旦title要素を削除
document.querySelector("head > title").remove()
console.log(document.querySelector("head > title")); // null
// document.titleに代入
document.title = "hi";
console.log(document.querySelector("head > title")); // <title>hi</title>
document.body
document.body
はbody要素を取得できるプロパティですが、実は新しいbody要素を代入できます。これはhtml
要素の子となりますから、body要素を木構造に挿入したといえます。
ちなみに、document.body
よりも後に作られたdocument.head
は代入不可の仕様になっています。
HTMLElement#innerText
HTML要素のinnerText
プロパティに文字列を代入すると、要素の中身が与えられた文字列で書き換えられます。ということは、新しいテキストノードが作られて挿入されるということですね。
ちなみに、textContent
との違いは、親切にも改行がbr要素に変換されるという点です。
const parent = document.createElement("div");
parent.innerText = "foo\nbar";
console.log(parent); // <div>foo<br>bar</div>
HTMLTitleElement#text
title
要素を表すノードであるHTMLTitleElement
は、text
プロパティを通じて中身のテキストを得ることができます。また、テキストノードの書き換えも行えます。
中身が空などの状態では、text
に代入することによって新しいテキストノードをtitle要素内に挿入できます。このタイプは今後も何個か出てくるので覚悟しておきましょう。
HTMLAnchorElement#text
a
要素を表すHTMLAnchorElement
も、text
プロパティで中身のテキストを書き換えられます。
HTMLTableElement#createCaption
, HTMLTableElement#createTHead
, HTMLTableElement#createTFoot
, HTMLTableElement#createTBody
似ているので4種類まとめて紹介します。これらはtable
要素を表すHTMLTableElement
に存在するメソッドです。読んで字のごとく、これらはtable要素内にそれぞれcaption要素、thead要素、tfoot要素、tbody要素を作って返します。tbody要素以外は一つしかtable要素内に存在できないため、すでに存在する場合は新しく作るのではなく既存のものが返されます。
HTMLTableElement#insertRow
これはtable
要素内にtr
要素を作成して挿入するメソッドです。引数の数値で挿入位置を指定できるのが特徴です。
必要に応じてtr
要素だけでなくtbody
要素も一緒に作られることがあります。
HTMLTableSectionElement#insertRow
table
要素だけでなくtbody
・thead
・tfoot
要素もinsertRow
を持ちます。なお、これらの要素は定義上ではHTMLTableSectionElement
という共通のインターフェースを持っていますので、レギュレーションに従ってこのinsertRow
は1種類と数えます。
HTMLTableRowElement#insertCell
tr
要素を表すHTMLTableRowElement
はinsertCell
メソッドを持っており、指定した位置に新規作成したtd
要素を挿入できます。
HTMLOptionsCollection#add
select
要素等の子option
要素の一覧を表すHTMLOptionsCollection
オブジェクトは、add
メソッドを用いてselect
要素にoption
要素(またはoptgroup
要素)を追加できます。insertBefore
と似ていますが、挿入位置を数値で指定できるのが特徴です。
HTMLOptionsCollection
の数値プロパティへの代入
HTMLOptionsCollection
は、add
メソッドを使うほかにも、配列のように数値を名前とするプロパティに直接option
要素を代入することで要素を挿入するという操作をサポートしています。
const select = document.createElement("select");
const option = document.createElement("option");
option.textContent = "hi";
select.options[2] = option;
console.log(select); // <select><option></option><option></option><option>hi</option></select>
HTMLSelectElement#add
, HTMLSelectElement
の数値プロパティ
これらはHTMLOptionsCollection
と同じ挙動をしますが、レギュレーションは別扱いとなります。
HTMLOptionElement#text
option
要素を表すHTMLOptionElement
のtext
プロパティは、例によって自身の子をテキストノードで置換することができます。
HTMLTextAreaElement#defaultValue
textarea
要素は子に書かれたテキストが入力欄の初期値になるという特徴を持ちます。この初期値はdefaultValue
プロパティで表され、これを変更するとDOM上の子要素がしっかりと変更されます。
HTMLOutputElement#defaultValue
, HTMLOutputElement#value
output
要素(使ったことありますか?)も同様です。ただし、値を表示するという特性上defaultValue
とvalue
の両方がDOM上の子要素に影響を及ぼします。
HTMLFormElement#reset
form
要素のreset
メソッドはフォームをリセットして初期状態に戻すメソッドです。このメソッドを通じてoutput
要素をリセットする際にテキストノードが生成される可能性があります。
const form = document.createElement("form");
const output = document.createElement("output");
output.defaultValue = "hi";
form.append(output);
console.log(form.innerHTML); // <form><output>hi</output></form>
output.value = "" // outputの子要素が消去される
console.log(form.innerHTML); // <form><output></output></form>
// formをリセットする
form.reset();
// optionの中身が復元される
console.log(form.innerHTML); // <form><output>hi</output></form>
HTMLScriptElement#text
script
要素を表すHTMLScriptElement
ノードはtext
プロパティを持ち、これは中身のスクリプトが文字列で入っています。これに文字列を代入することでDOMの書き換えを発生させることができます。
ただ、中身を書き換えたからといってスクリプトが再実行されたりとかそういういうことはないようです。
DOM Parsing and Serialization
HTMLとDOMの本体以外にも細々した仕様はいろいろありますが、今回の話に関わってくるのはこのDOM Parsing and Serializationくらいです。この仕様からは3種類のノード挿入方法が提供されています。
InnerHTML#innerHTML
非常に有名なinnerHTML
です。HTML文字列を代入するとそれをパースしてDOMノードを構築して子要素にしてくれます。当然ながら子要素をノードに追加することができます。
Element#outerHTML
outerHTML
も似た機能で、これはouterHTML
がセットされた要素自身を新しくパースして作られた要素で置換します。
Element#insertAdjacentHTML
上のほうで出てきたinsertAdjacentElement
などと似ていますが、こちらはやはりHTML文字列を解析して要素を作ってくれます。
まとめ
ということで、HTMLとDOMで木構造にノードを挿入する方法全38種類を列挙しました。全部言えたらまさにあなたはDOMマスターと言えるでしょう。
ただ筆者の調査にも間違いや抜け漏れがあるかもしれません。筆者を超えるDOM力を持つ方からのご指摘をお待ちしています。
Q. DOMマスターになったら実務で役に立つんですか?
A. さあ……