jQuery / 子要素の数で親要素を選ぶ

親候補へ.filter(function () { ... })を適用し、各親で$(this).children(".item").lengthを比較します。ちょうど2個は=== 2、2個以上は>= 2です。深い子孫も数える場合だけchildren()をfind()へ変えます。

親ごとに数える対象

.filter()は元の集合から条件を通る要素を残します。thisは検査中の親DOM要素で、trueならその親を含めます。結果は子ではなく親の集合です。他の親を文書から削除しません。

.children(".item")は直下一段の一致要素だけです。選択子なしなら見出しなど他の直接子要素も数えます。どちらもテキスト・コメントは含みません。

.find(".item")は深い子孫を含みます。Betaは直接1個の中に2個があり、子孫なら3個です。非表示要素もDOMにあれば数えます。表示中だけなら別条件を定義します。

指定数と最小数を比べる完全な例

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

直接数はAlpha 2、Beta 1、Gamma 0、Delta 3です。非表示D3も含め、見出しと一般文字は.itemではないため除きます。候補は#board直下.groupで、内部要素を混ぜません。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Filter parents by child count</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; }
    label { display: block; margin: 8px 0; }
    select, input, button { font: inherit; max-width: 100%; }
    .group { border: 2px solid #aaa; padding: 12px; margin: 12px 0; }
    .group.chosen { border-color: teal; background: #e5f5f1; }
    .item { display: inline-block; padding: 4px; border: 1px solid #777; margin: 3px; }
    .item[hidden] { display: none; }
    .group h2 { font-size: 20px; margin: 0; }
    #result { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <label>Count scope <select id="scope"><option value="direct">Direct children</option><option value="descendant">All descendants</option></select></label>
  <label>Rule <select id="rule"><option value="exact">Exactly</option><option value="minimum">At least</option></select></label>
  <label>Required count <input id="count" type="number" min="0" step="1" value="2"></label>
  <button type="button" id="add">Add direct item to Alpha</button>
  <button type="button" id="reset">Reset</button>
  <div id="board">
    <section class="group" id="alpha"><h2>Alpha</h2><span class="item">A1</span><span class="item">A2</span> plain text <!-- comment --></section>
    <section class="group" id="beta"><h2>Beta</h2><div class="item">B1 <span class="item">B2</span><span class="item">B3</span></div></section>
    <section class="group" id="gamma"><h2>Gamma</h2>Text without an item</section>
    <section class="group" id="delta"><h2>Delta</h2><span class="item">D1</span><span class="item">D2</span><span class="item" hidden>D3 hidden</span></section>
  </div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    $(function () {
      const initial = $("#board").html();
      let added = 0;
      function apply() {
        const $groups = $("#board > .group");
        const scope = $("#scope").val(), rule = $("#rule").val();
        const field = document.getElementById("count"), target = Number(field.value);
        $groups.removeClass("chosen");
        if (field.value === "" || !field.checkValidity() || !Number.isInteger(target) || target < 0) {
          $("#result").text("Enter a nonnegative integer."); return;
        }
        function count(group) {
          return scope === "direct" ? $(group).children(".item").length : $(group).find(".item").length;
        }
        const $selected = $groups.filter(function () {
          const n = count(this);
          return rule === "exact" ? n === target : n >= target;
        });
        $selected.addClass("chosen");
        const ids = $selected.map(function () { return this.id; }).get();
        const counts = $groups.map(function () { return this.id + "=" + count(this); }).get();
        $("#result").text(`scope=${scope}; rule=${rule}; count=${target}\nselected=${ids.join(",") || "(none)"}\n${counts.join("; ")}`);
      }
      $("#scope, #rule, #count").on("change input", apply);
      $("#add").on("click", function () {
        added++; $("<span>").addClass("item").text("Added " + added).appendTo("#alpha"); apply();
      });
      $("#reset").on("click", function () {
        $("#board").html(initial); added = 0;
        $("#scope").val("direct"); $("#rule").val("exact"); $("#count").val(2); apply();
      });
      apply();
    });
  </script>
</body>
</html>

初期は直接itemが2個のAlphaだけを強調します。

scope=direct; rule=exact; count=2
selected=alpha
alpha=2; beta=1; gamma=0; delta=3
  1. At leastでAlpha・Deltaです。同じ2でも「ちょうど」と「以上」は異なります。
  2. All descendantsでBetaも含み、1から3へ変わります。
  3. Exactly 3は子孫ならBeta・Delta、Direct childrenへ戻すとDeltaです。
  4. Exactly 0はGamma、At least 0は0も許して四つ全部です。
  5. Reset後にAdd direct item to Alphaを押すと3個になり、ちょうど2は(none)です。親を削除したのではありません。
  6. 空・負数・小数はエラー表示と強調解除です。Resetは追加を消し元の構造と条件を復元します。

集合全体の合計と親別条件

Reset後、開発者ツールで実行します。子全体の集合と親ごとの判定を比較できます。

const $groups = $("#board > .group");
console.log($groups.children(".item").length);
console.log($groups.filter(function () {
  return $(this).children(".item").length === 0;
}).map(function () { return this.id; }).get().join(","));

6は2+1+0+3の合計で、どの親が2個かは表しません。次の行は各親を調べ、直接.itemなしのGammaを返します。

6
gamma

通常functionでthisを現在親として受けます。同じthisのまま矢印関数へ置き換えると前提が変わります。矢印ならコールバック第二引数の要素を受け明示的に調べます。

保存したフィルター結果は条件を常時再評価しません。Alpha追加後も過去の結果にはAlphaが残り得ます。例は追加後apply()で候補と数を再取得します。.filter()はDOM変更監視ではありません。

対象範囲とよくある誤解

全要素かカード・li・入力など特定対象か決めます。.itemだけなのでGammaはh2と文字があっても0です。完全に空のコンテナーという意味ではありません。可視性・有効性・データ条件も必要なら明示します。

一致子孫が一つでもあるかだけなら.has()が簡単な場合があります。指定数や最小数は数値比較が明確です。nth-childの兄弟位置と親全体の数を混同しません。

親が大量で深い検索を繰り返すなら候補を絞り不要な再計算を減らします。一方、古い結果で構造変更を見逃さないようにします。初期HTMLと変更後の両方を確認します。