jQuery / HTMLタグを変更する

jQueryでHTMLタグを変えるには、目的のタグの新しい要素を作り、元の要素を置き換えます。tagNameへの代入で名前を変更することはできません。入力値と子のイベントを残す場合はHTML文字列をコピーせず、削除前に実際の子ノードを移します。

タグ名の変更と要素の置換

DOM(Document Object Model)は文書をノードオブジェクトとして扱います。Element.tagNameは読み取り専用です。.attr("tagName", "section")は要素の種類を変更せず、.prop()で読み取り専用値を書き換える方法も使えません。

document.createElement()でsectionを作り、.replaceWith()でdivを置換します。名前だけ変わったように見えても外側のオブジェクトは新品です。元のdivを指す変数は自動でsectionを指しません。

これはdivからsectionへの置換時に状態を比較する実習です。独立したsectionの意味設計を完成させる例ではありません。実サイトでは内容の意味と見出し構造で選びます。外観だけならclassとCSSの変更が適している場合があります。

子ノードを保持する完全な例

全コードをUTF-8のtag-demo.htmlに保存しブラウザーで開きます。インターネットで公式jQueryファイル4.0.0を読みます。サーバーやパッケージ導入は不要です。主要動作をWindows Edge、jQuery 3.7.1・4.0.0で実行しました。

divにはテキスト、コメント、入力欄、ボタンがあります。ボタンに直接処理、mainに委譲処理、元のdivに独自処理とjQueryデータを設定します。別々のカウンターで保持の違いを観察します。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Replace a container tag, keep its children</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; margin: 20px; max-width: 760px; }
    button, input { font: inherit; max-width: 100%; box-sizing: border-box; }
    button { margin: 4px; }
    .card { border: 3px solid teal; padding: 16px; margin: 16px 0; }
    #result { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <button type="button" id="change">Change div to section</button>
  <button type="button" id="reset">Reset</button>
  <main id="board"></main>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      let original, originalInput, originalNodes;
      let childClicks = 0, wrapperClicks = 0, delegatedClicks = 0;
      function report() {
        const card = document.getElementById("card");
        const sameChildren = originalNodes.every((node, i) => card.childNodes[i] === node);
        $("#result").text(`tag=${card.localName}\nsame wrapper=${card === original}\nsame children=${sameChildren}\nsame input=${card.querySelector("input") === originalInput}\ninput=${originalInput.value}\nold data=${$(original).data("lesson")}\nchild=${childClicks}; wrapper=${wrapperClicks}; delegated=${delegatedClicks}`);
      }
      function reset() {
        $("#board").empty();
        const card = document.createElement("div");
        card.id = "card"; card.className = "card";
        card.setAttribute("aria-label", "Editable note");
        card.append(document.createTextNode("Keep this text. "), document.createComment("keep this comment"));
        const input = document.createElement("input");
        input.type = "text"; input.value = "First note"; input.setAttribute("aria-label", "Note");
        const button = document.createElement("button");
        button.type = "button"; button.className = "inspect"; button.textContent = "Inspect";
        card.append(input, button); document.getElementById("board").append(card);
        original = card; originalInput = input; originalNodes = Array.from(card.childNodes);
        childClicks = wrapperClicks = delegatedClicks = 0;
        $(card).data("lesson", "old wrapper").on("click", function () { wrapperClicks++; });
        $(button).on("click", function () { childClicks++; });
        $(input).on("input", report);
        $("#change").prop("disabled", false); report();
      }
      $("#board").on("click.tagLesson", ".inspect", function () { delegatedClicks++; report(); });
      $("#change").on("click", function () {
        const old = document.getElementById("card");
        const replacement = document.createElement("section");
        for (const name of ["id", "class", "aria-label"]) {
          if (old.hasAttribute(name)) replacement.setAttribute(name, old.getAttribute(name));
        }
        // Move existing children BEFORE jQuery removes the old wrapper.
        $(replacement).append($(old).contents());
        $(old).replaceWith(replacement);
        $(this).prop("disabled", true); report();
      });
      $("#reset").on("click", reset);
      reset();
    });
  </script>
</body>
</html>

初期結果です。same childrenは要素だけでなくテキスト・コメントも同じオブジェクトと順序か確認します。

tag=div
same wrapper=true
same children=true
same input=true
input=First note
old data=old wrapper
child=0; wrapper=0; delegated=0
  1. First noteを編集します。<em>Edited note</em>も入力値のままでHTMLとして実行しません。
  2. Inspectを一度押すとchild=1、wrapper=1、delegated=1です。ボタン、元div、mainの処理が各一回動きます。
  3. Change div to sectionを押すとtag=section、same wrapper=false、same children=true、same input=trueです。編集値は残り、old dataはundefinedです。
  4. 再度Inspectでchild=2、wrapper=1、delegated=2です。子の直接処理とmainの委譲処理は残り、元divの処理は移りません。
  5. Resetはdiv、初期入力値、カウンター0を作り直します。比較をやり直すため編集値を消します。

保持する内容と明示的な設定

$(replacement).append($(old).contents())を先に実行する順序が重要です。.contents()は直接の子である要素・テキスト・コメントを含みます。appendで既存オブジェクトを移し、入力の現在valueと子の直接イベントを保持します。.children()だけでは要素外の文字とコメントが抜けます。

移動後の.replaceWith()は空になったdivを除去し、そのjQueryデータと処理を整理します。先に置換し後から子を戻そうとすると整理時に状態を失う場合があります。すべての状態を保つ汎用機能ではなく、フォーカス、文字選択、プラグインの参照は別途確認します。

例ではid・class・aria-labelを明示的にコピーしました。他の属性は自動で移りません。全属性の無差別コピーでは新タグに無効な属性、古いrole、インライン処理まで残り得ます。必要な属性を選びアクセシビリティ上の意味も見直します。jQuery .data()とHTML data-*属性は同じ保存場所ではありません。

.replaceWith()の戻り値は除去した元のjQuery集合で、新要素ではありません。続く操作にはreplacementか再選択した現在要素を使います。外側の処理が必要なら新要素へ明示的に設定するか、例のように安定した祖先から委譲します。

適用範囲と問題の確認

$(old).html()を新要素の.html()へ渡すと文字列を再解析します。見た目が同じでも子は新しいオブジェクトになり、直接イベント、既存参照、入力の現在状態はそのまま残りません。ユーザー入力をHTML文字列へ連結することも避けます。

通常のHTMLコンテナー間の例です。子を持てないinput・img、特殊な表構造、フォーム部品、SVG、カスタム要素に無条件適用しません。許可される子と意味を先に確認します。buttonからaでは役割やキーボード動作も変わります。

見た目が変わればdiv.cardのようなタグ依存CSSや旧タグの選択子を確認します。idを一意に保ち、ラベル・アンカーも維持します。入力値だけでなく直接処理、委譲、フォーカス、読み上げの意味、復元まで実画面で確認します。