jQuery / 特定の子要素を持たない要素を選ぶ

特定の子孫がない親は、候補から.has(".badge")の集合を.not()で除外して選びます。直接子だけなら各親の$(this).children(".badge").length === 0を調べます。特定の子がないことと完全に空であることは別条件です。

対象と深さを決める

.has(".badge")は深さを問わず一致子孫のある候補を残します。それを.not()で除くと内部にbadgeがない候補になります。

.children(".badge")は一段だけです。中に入ったbadgeなら直接なしを通過しても子孫なしは通過しません。非表示でもDOM(Document Object Model)に存在すれば対象ありです。

親は検査対象コンテナーの意味で、.parent()で上に移動する操作ではありません。無関係な祖先や内部要素を混ぜないよう候補を限定します。

深さと内容を比べる完全な例

全コードをUTF-8のmissing-child-demo.htmlとして保存し開きます。インターネットで公式jQueryファイル4.0.0を読みます。サーバーや導入は不要です。初期結果、追加、移動、復元をWindows Edge、3.7.1・4.0.0で実行しました。

aは直接badge、bは中のbadge、cは別の子、dは文字だけです。eは空、fはコメントだけ、gは空白一個、hは非表示badgeです。a:などの名前はCSS生成でDOM内容ではありません。e・f・g内部の空白変更は:empty結果を変え得ます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Select containers without a badge</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 { font: inherit; margin: 4px; }
    .card { border: 2px solid #aaa; margin: 10px 0; padding: 10px; min-height: 28px; }
    .card::before { content: attr(id) ": "; font-weight: bold; }
    .card.chosen { border-color: teal; background: #e4f6f0; }
    .badge { border: 1px solid #777; padding: 2px; }
    .badge[hidden] { display: none; }
    #result { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <p>Green cards have no .badge descendant. Names are CSS-generated labels.</p>
  <button type="button" id="add">Add badge to c</button>
  <button type="button" id="move">Move b badge to direct child</button>
  <button type="button" id="reset">Reset</button>
  <div id="cards">
    <div class="card" id="a">Direct <span class="badge">A</span></div>
    <div class="card" id="b"><div class="wrapper">Nested <span class="badge">B</span></div></div>
    <div class="card" id="c"><span class="note">Other element</span></div>
    <div class="card" id="d">Text only</div>
    <div class="card" id="e"></div>
    <div class="card" id="f"><!-- comment only --></div>
    <div class="card" id="g"> </div>
    <div class="card" id="h"><span class="badge" hidden>Hidden</span></div>
  </div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      const initial = $("#cards").html();
      function ids($set) { return $set.map(function () { return this.id; }).get().join(",") || "(none)"; }
      function report() {
        const $cards = $("#cards > .card");
        const $withoutDescendant = $cards.not($cards.has(".badge"));
        const $withoutDirect = $cards.filter(function () { return $(this).children(".badge").length === 0; });
        const $empty = $cards.filter(":empty");
        const $withoutElements = $cards.filter(function () { return $(this).children().length === 0; });
        $cards.removeClass("chosen"); $withoutDescendant.addClass("chosen");
        $("#result").text([
          "no descendant badge=" + ids($withoutDescendant),
          "no direct badge=" + ids($withoutDirect),
          "empty=" + ids($empty),
          "no element children=" + ids($withoutElements)
        ].join("\n"));
      }
      $("#add").on("click", function () {
        $("<span>").addClass("badge").text("New").appendTo("#c");
        $(this).prop("disabled", true); report();
      });
      $("#move").on("click", function () {
        $("#b .badge").appendTo("#b"); $(this).prop("disabled", true); report();
      });
      $("#reset").on("click", function () {
        $("#cards").html(initial); $("#add, #move").prop("disabled", false); report();
      });
      report();
    });
  </script>
</body>
</html>

初期結果です。緑は一行目の子孫badgeなしだけを強調し、他の行は別条件の比較です。

no descendant badge=c,d,e,f,g
no direct badge=b,c,d,e,f,g
empty=e,f
no element children=d,e,f,g
  1. c,d,e,f,gは全階層でbadgeなしです。cにはnote要素があっても含みます。
  2. 二行目はbも含みます。badgeはwrapper内で直接ではありません。
  3. Add badge to cで一行目d,e,f,g、二行目b,d,e,f,gです。
  4. Move b badge to direct childで二行目もd,e,f,gです。同じノードを動かし、子孫存在は同じで直接関係だけ変わります。
  5. Resetは八つと元構造を復元します。非表示hも存在として扱います。

候補除外と空を混同しない

Reset後に開発者ツールで実行します。最初は候補自身、次は内部を調べます。

const $cards = $("#cards > .card");
console.log($cards.not(".badge").map(function () { return this.id; }).get().join(","));
console.log($cards.filter(function () {
  return $(this).find(".badge").length === 0;
}).map(function () { return this.id; }).get().join(","));

八つのcard自身にbadgeクラスはないので最初は全部です。次が目的のc,d,e,f,gです。

a,b,c,d,e,f,g,h
c,d,e,f,g

:emptyは特定種類の欠如ではなく、要素・文字の子がない条件です。eとコメントだけのfが該当します。コメントは妨げませんがgの空白文字は妨げ、dの文字も同じです。

.children().length === 0は直接子要素なしでd,e,f,gです。文字・空白は数えません。CSS ::beforeの名前もDOM子ではなくe・fの:emptyを変えません。外観とノード構造を分けて確認します。

動的変更と適用確認

結果を保存してもbadge追加で自動更新しません。例は追加・移動後report()で現在候補を再取得します。他コードの変更完了後にも再評価をつなぎます。

.not()・.filter()は集合を作り、他cardを削除しません。必要なclassや空状態表示は別操作です。例は古い強調を消して現在だけ付けます。

対象タグ・クラス、直接か全子孫か、非表示・無効も存在とするか決めます。似た選択子へ無条件に変えず初期と変更後を比較します。深い反復検索は候補を限定して減らします。