Help us understand the problem. What is going on with this article?

初心者による DOM 要素の追加/置換/削除

概要

DOMの役割は既存のノードを参照するばかりではなく、文書ツリーに対して、新規のノードを追加したり、既存のノードを置換/削除することもできる。

innerHTMLを使うかどうか

innerHTMLプロパティを使うことで、HTMLを編集することができる。しかし、以下のような問題点がある。

  1. コンテンツが複雑になった場合に、コードの見通しが悪くなる。
  2. ユーザからの入力値に基づいてコンテンツを作成した場合、任意のスクリプトを実行されてしまう可能性がある。

これを踏まえたうえで、新しい方法は

  1. オブジェクトツリーとして操作できるので、対象となるコンテンツが複雑になった場合でも、コードの可読性が劣化しにくい。
  2. 要素/属性とテキストを区別して扱うので、ユーザからの入力によってスクリプトが混入するようなリスクを回避しにくい。
  3. ただし、少しのコンテンツの埋め込みにも、ある程度の操作が必要になるので、コードは冗長になりがちになる。

よってまとめると、

シンプルなコンテンツの編集 -> innerHTMLプロパティ
複雑なコンテンツの編集 -> 新しいアプローチ 

新規にノードを作成する

新規にノードを追加する流れは次のようになる。

1. 要素/テキストノードを作成する
2. ノード同士を組み立てる
3. 属性ノードを追加する

2の追加したいノードを固めることを省いてはいけない。一つずつ追加していくと一回ずつ再描画がかかるため、パフォーマンスの面から望ましくない。一回で追加することで、パフォーマンスを下げない。

要素/テキストを作成する

コンテンツを作成するにはまず、createElement/createTextNodeメソッドを利用して、新たに挿入すべき要素/ノードを生成します。

sample.js
document.createElement(name)
document.createTextNode(text)
 //name : 要素名 text : テキスト

また、create...には様々な種類がある。

sample.js
document.createAttribute(属性名)
document.createCDATASection(テキスト) //CDATAセクション
document.createComment(テキスト) //コメント
document.createEntityReference(実体名) //実体参照ノード
document.createProccesingInstruction(ターゲット名、データ) //処理命令ノード
document.createDocumentFragment() //ドキュメント 

これらによって作られた要素/ノードはまだドキュメントツリーには属していないため次は、これらをドキュメントツリーに接続させる。

ノード同士を組み立てる

この作業を行うのが、appendChildです。appendChildメソッドは、指定された要素を現在の要素の最後の子要素として追加します。

sample.js
elem.appendChild(node)
 //elem : 要素オブジェクト node : 追加するノード

appendChildメソッドは、insertBeforeメソッドで置き換えることもできます。

sample.js
list.appendChild(anchor)
list.insertBefore(anchor, null)

insertBeforeメソッドは、第一引数で指定したノードを、第二引数で指定した子ノードの直前!に挿入します。appnedChild同様に、最後尾に追加したい場合には、第二引数にnullを指定します。

属性ノードを追加する

属性ノードの設定は、属性と同名のプロパティを設定するだけですが、より汎用的なコードを記述するためにcreateAttributeメソッドを使う。

sample.js
anchor.href = url.value

let href = document.createAttribute('href')
href.value = url.value
anchor.setAttributeNode(href)

属性ノードの値を設定するには、valueプロパティを使用します。また、属性ノードを要素ノードに関連づけるには、appendChildやinsertBeforeメソッドではなく、setAttributeNodeメソッドを使用する点に注意。
なぜならば、属性ノードは要素ノードの子要素ではなく「属性」という扱いであるからである。

まとめ1

まとめると、

sample.html
<form>
 <div>
  <label for="name">サイト名:</label><br />
  <input id="name" name="name" type="text" size="30" />
 </div>
 <div>
  <label for="url">URL:</label><br />
  <input id="url" name="url" type="url" size="50" />
 </div>
 <div>
  <input id="btn" type="button" value="追加" />
 </div>
</form>
<div id="list"></div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 document.getElementById('btn').addEventListener('click', function() {
  //テキストボックスの取得
  let name = document.getElementById('name')
  let url = document.getElementById('url')

  //<a>要素を生成
  let anchor = document.createElement('a')
  //<a>要素のhref属性を設定
  anchor.href = url.value
  //テキストノードを生成し、<a>要素の直下に追加
  let text = document.createTextNode(name.value)
  anchor.appendChild(text)
  //<br>要素を生成
  let br = document.createElement('br')
  //<div id="list">を取得
  let list = document.getElementById('list')
  //<div>要素の直下に<a>/<br>の順に追加
  list.appendChild(anchor)
  list.appendChild(br)
 }, false)
}, false)

既存のノードを置換/削除する

ノードを置換する

子ノードを置き換えるのは、replaceChildメソッドを使う。

sample.js
elem.replaceChild(after, before)
 //elem : 要素オブジェクト after : 置き換え後のノード before : 置き換え対象ノード

注意点としては、置き換え対象のノードは現在のノードに対する子ノードでなければならない。

ノードを削除する

子ノードを削除するのは、removeChildメソッドです。

sample.js
elem.removeChild(node)
 //elem : 要素オブジェクト node : 削除対象ノード

削除対象ノードは、現在のノードに対するノードである必要があります。

属性を削除する

属性を削除する場合には、removeAttributeメソッドを使います。

sample.js
elem.removeAttribute(attribution)
 //elem : 要素オブジェクト attribution : 属性値

カスタムデータ属性を設定する

data-xxxxはカスタムデータ属性と呼ばれ、自由に値を設定できる特別な値です。これを使うメリットは、可変な情報(パラメータ)と機能(イベントリスナー)とを切り離しておくことで、後からコードを再利用しやすくなることだ。

まとめ2

まとめると

sample.html
<ul id="list">
 <li><a href="JavaScript:void(0)" data-isbn="987-4-7981-3547-2">独習PHP 第三版</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7741-8030-4">Javaポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7741-7984-1">Swiftポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-7981-4402-3">独習ASP.NETポケットリファレンス</a></li>
 <li><a href="JavaScript:void(0)" data-isbn="978-4-8222-9644-5">アプリを作ろう!Android入門</a></li>
</ul>
<input id="del" type="button" value="削除" disabled />
<div id="pic"></div>
sample.js
document.addEventListener('DOMContentLoaded', function() {
 let list = document.getElementById('list')
 let del = document.getElementById('del')
 let pic = document.getElementById('pic')

 //<ul id="list">配下をクリックしたときの配下
 list.addEventListener('click', function(e) {
  //data-isbn属性からアンカータグに紐付いたisbn値を取得
  let isbn = e.target.getAttribute('data-isbn')
  //isbn値が取得できた場合にのみ処理を実行
  if(isbn) {
   //img要素を生成
   let img = document.createElement('img')
   img.src = 'http://windows./img/' + isbn + '.jpg'
   img.alt = e.innerHTML
   img.height = 150
   img.width = 108
   //<div>要素配下に<img>要素が存在するか(画像表示中か)を確認
   if(pic.getElementByTagName('img').length > 0){
    //<img>要素が存在する場合、あらたな<img>要素で置換
    pic.replaceChild(img, pic.lastChild)
   }else{
    //<img>要素が存在しない場合、新たに追加し、[削除]ボタンを追加
    del.disable = false
    pic.appendChild(img)
   }
  }
 }, false)

 //[削除]ボタンがクリックされたときの処理
 del.addEventListener('click', function() {
  //<div id="pic">配下の子要素を削除し、[削除]ボタンを無効に
  pic.removeChild(pic.lastChild)
  del.disable = true
 }, false)
}, false)

HTMLCollection / NodeList の注意点

getElementsByTagName / getElementsByName / getElementsByClassNameメソッドは、戻り値としてHTMLCollectionまたはNodeListオブジェクトを返します。この二つは、「オブジェクトが文書ツリーを参照しており、文書ツリーへの変更がHTMLCollection / NodeListオブジェクトへもリアルタイムに反映される。」ということです。

たとえば、appendChildメソッドで

要素を追加すると、HTMLCollectionオブジェクトlistの内容がリアルタイムで変化します。

これは一見便利にも見えますが、注意しなければいけないのがul.lengthの値が変化することからfor文等で使うときにはループが終わらないなどのバグが出やすい。これは、for文で一度初期化してから使うとよし。

参考資料

山田祥寛様 「javascript本格入門」

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away