importNode と adoptNode でドキュメント間の要素を扱う

DOM のノードは特定のドキュメントに所属しています。あるドキュメントのノードを別のドキュメントで使おうとすると、そのままでは動作しない場合があります。importNode と adoptNode は、ノードのドキュメント所有権を移すためのメソッドです。iframe の中身を親ページに取り込んだり、template 要素からノードを複製したりする場面で登場します。

ノードの所有ドキュメント

すべての DOM ノードは ownerDocument というプロパティを持ち、自分がどのドキュメントに所属しているかを示しています。

const div = document.createElement('div');
console.log(div.ownerDocument === document); // true

通常のページでは意識する必要がありませんが、iframe や XMLHttpRequest で取得した HTML、DOMParser で生成したドキュメントなど、複数のドキュメントが存在する状況では所有権が問題になります。異なるドキュメントに属するノードをそのまま appendChild しようとすると、ブラウザによってはエラーが発生する可能性があるためです。

importNode:コピーして取り込む

document.importNode は、別のドキュメントのノードを複製して現在のドキュメントに取り込みます。元のノードはそのまま残ります。

// 構文
const newNode = document.importNode(externalNode, deep);

第 2 引数の deep は真偽値で、true なら子孫ノードも含めて深い複製を行い、false なら対象ノードだけを複製します。

// iframe 内の要素を親ページにコピーする例
const iframe = document.getElementById('myFrame');
const iframeDoc = iframe.contentDocument;
const original = iframeDoc.getElementById('content');

// 子孫ノードごと複製して取り込む
const imported = document.importNode(original, true);
document.body.appendChild(imported);

// 元のノードは iframe 内にそのまま残る
console.log(original.ownerDocument === iframeDoc); // true
console.log(imported.ownerDocument === document);   // true

importNode は cloneNode と似ていますが、決定的な違いがあります。cloneNode は同じドキュメント内での複製を前提としているのに対し、importNode はドキュメントの境界を越える複製を行います。

adoptNode:移動して取り込む

document.adoptNode は、別のドキュメントのノードをそのまま移動させて現在のドキュメントの所有にします。複製は行わず、元のドキュメントからはノードが除去されます。

// 構文
const adoptedNode = document.adoptNode(externalNode);
// iframe 内の要素を親ページに移動する例
const iframe = document.getElementById('myFrame');
const iframeDoc = iframe.contentDocument;
const original = iframeDoc.getElementById('content');

// ノードを移動して取り込む
const adopted = document.adoptNode(original);
document.body.appendChild(adopted);

// 元のドキュメントからは除去されている
console.log(adopted.ownerDocument === document);            // true
console.log(iframeDoc.getElementById('content'));           // null
importNode

ノードを複製して取り込む。元のノードは元のドキュメントに残る。deep 引数で子孫の複製を制御する。

adoptNode

ノードをそのまま移動して取り込む。元のドキュメントからはノードが除去される。deep 引数は不要(子孫も一緒に移動する)。

template 要素での活用

importNode が最もよく使われるのは、template 要素の中身を複製して DOM に挿入する場面です。template 要素の content プロパティは DocumentFragment を返しますが、これは厳密にはメインドキュメントとは異なる文脈に属しています。

const template = document.getElementById('card-template');

// template の中身を複製して取り込む
const clone = document.importNode(template.content, true);

// 複製したノードを操作してから挿入
clone.querySelector('.title').textContent = 'DOM 操作入門';
clone.querySelector('.body').textContent = 'ノードの所有権について学びます';

document.getElementById('container').appendChild(clone);

実はモダンブラウザでは template.content.cloneNode(true) でも同じ結果が得られます。内部的にドキュメントの変換を自動で行ってくれるためです。ただし、仕様上は importNode を使うのが正式な手順であり、古いブラウザとの互換性を考慮する場合は importNode を明示的に使うほうが安全です。

DOMParser との組み合わせ

外部から取得した HTML 文字列をパースして、その一部をページに取り込む場面でも importNode や adoptNode が活躍します。

const htmlString = '<div class="article"><h2>見出し</h2><p>本文です。</p></div>';
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');

// パース結果から必要な部分を取り込む
const article = doc.querySelector('.article');
const imported = document.importNode(article, true);
document.getElementById('output').appendChild(imported);

DOMParser が返すドキュメントはメインページとは別のドキュメントオブジェクトです。そこから要素を取り出してメインページに追加するには、importNode か adoptNode でドキュメントの所有権を変更する必要があります。

HTML 文字列を DOMParser でパースする

別ドキュメントのノードツリーが生成される

importNode / adoptNode で現在のドキュメントに取り込む

innerHTML を使えばもっと手軽に HTML を挿入できますが、既存の DOM 構造を壊さずに特定の要素だけを抜き出して取り込みたい場合には、DOMParser と importNode の組み合わせが有効です。

実践:iframe 間のノード移動

iframe の中身を親ページに移動する動作を実際に確認してみましょう。

HTML
CSS
JavaScript
<div id="source" style="border:2px solid #4a90d9; padding:12px; margin-bottom:12px; border-radius:4px;">
  <p style="margin:0 0 8px; font-weight:bold; color:#4a90d9;">移動元</p>
  <div id="move-target" style="background:#eaf2fb; padding:8px; border-radius:4px;">この要素を移動します</div>
</div>
<div id="dest" style="border:2px dashed #999; padding:12px; min-height:40px; border-radius:4px;">
  <p style="margin:0 0 8px; font-weight:bold; color:#666;">移動先(ここに入る)</p>
</div>
<button id="adopt-btn" style="margin-top:12px; padding:6px 16px; cursor:pointer;">adoptNode で移動</button>
<button id="import-btn" style="margin-top:12px; padding:6px 16px; cursor:pointer;">importNode でコピー</button>
<button id="reset-btn" style="margin-top:12px; padding:6px 16px; cursor:pointer;">リセット</button>
const source = document.getElementById('source');
const dest = document.getElementById('dest');
const originalHTML = source.innerHTML;
const originalDest = dest.innerHTML;

document.getElementById('adopt-btn').addEventListener('click', () => {
  const target = document.getElementById('move-target');
  if (!target) return;
  const adopted = document.adoptNode(target);
  dest.appendChild(adopted);
});

document.getElementById('import-btn').addEventListener('click', () => {
  const target = document.getElementById('move-target');
  if (!target) return;
  const clone = document.importNode(target, true);
  clone.removeAttribute('id');
  clone.style.background = '#f0e6fb';
  clone.textContent = 'コピーされた要素';
  dest.appendChild(clone);
});

document.getElementById('reset-btn').addEventListener('click', () => {
  source.innerHTML = originalHTML;
  dest.innerHTML = originalDest;
});

adoptNode ボタンを押すと要素が移動元から消えて移動先に現れ、importNode ボタンを押すと元の要素を残したままコピーが移動先に追加されます。この挙動の違いが importNode と adoptNode の本質的な差です。

イベントリスナーの扱い

importNode と adoptNode では、元のノードに登録されていたイベントリスナーの扱いが異なります。

importNode のイベントリスナー

ノードを複製するため、addEventListener で登録したイベントリスナーはコピーされない。HTML 属性に記述したインラインイベント(onclick 属性など)は複製される。

adoptNode のイベントリスナー

ノードをそのまま移動するため、addEventListener で登録したイベントリスナーも維持される。

importNode で複製した要素にイベント処理が必要な場合は、複製後に改めてイベントリスナーを登録する必要があります。一方、adoptNode は既存のイベントリスナーごと移動するため、再登録の手間がかかりません。この点も、どちらのメソッドを選ぶかの判断材料になるでしょう。

CSS の適用に関する注意

ドキュメント間でノードを移動すると、適用される CSS が変わる可能性があります。元のドキュメントで有効だったスタイルシートが、移動先のドキュメントには存在しないかもしれません。

// iframe 内では .highlight { color: red; } が定義されている
// 親ページにはそのスタイルがない

const node = iframeDoc.querySelector('.highlight');
const adopted = document.adoptNode(node);
document.body.appendChild(adopted);
// → .highlight のスタイルが適用されず、見た目が変わる

インラインスタイル(style 属性)はノード自体に付随するため、ドキュメントを移動しても維持されます。しかしスタイルシートに依存したクラスベースのスタイリングは、移動先で同じ CSS が読み込まれていなければ失われます。ノードを移動する際は、必要なスタイルも一緒に移行するか、インラインスタイルを使うことを検討してください。

DOMParser でパースした HTML から要素を取り出してページに追加するとき、適切な方法はどれですか?

  • そのまま appendChild する
  • importNode で複製してから appendChild する
  • innerHTML に代入する
  • cloneNode で複製してから appendChild する
__RESULT__

DOMParser が返すドキュメントはメインページとは別のドキュメントです。importNode を使ってメインドキュメントに取り込んでから appendChild するのが正式な手順になります。