jQuery / チェックボックスをすべて選択・解除する

すべて選択するには対象集合に.prop("checked", true)、解除には.prop("checked", false)を使います。全体用チェックボックスがある場合は個別変更時にもその状態を再計算します。一部だけ選択された状態はindeterminateで表示します。

対象集合と全体の状態

.prop()は現在のcheckedを読み書きします。HTML checked属性の.attr()を現在の選択管理に使いません。文字列ではなく真偽値true・falseを渡します。

対象は#items内のitemクラスの有効なチェックボックスです。全体用#allは含めません。:enabledでdisabled項目とfieldsetから無効状態を受ける項目を除きます。CSSで非表示というだけでは除きません。

対象が1個以上で全選択なら全体をチェックします。一部選択はchecked=false、indeterminate=true、未選択なら両方falseです。0個なら全体も無効にします。0と0が等しいという理由で全選択にはしません。

一ファイルで動く完全な例

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

Alphaは選択済み、Betaは未選択です。Lockedは選択済みでも無効なので一括変更と集計に含めません。全体用にはnameがなく、項目値として送信しません。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Select all eligible checkboxes</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 20px; }
    label { display: block; padding: 6px; }
    button { font: inherit; margin: 4px; }
    fieldset { margin: 12px 0; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <form id="choices">
    <label><input type="checkbox" id="all"> Select all available items</label>
    <fieldset id="items"><legend>Items</legend></fieldset>
    <button type="button" id="add">Add item</button>
    <button type="button" id="clear">Remove items</button>
    <button type="button" id="reset">Reset example</button>
    <button type="submit">Inspect submission</button>
  </form>
  <pre id="result" aria-live="polite"></pre>
  <pre id="submission"></pre>
  <script>
    $(function () {
      const $form = $("#choices"), $all = $("#all");
      let next = 1, itemChanges = 0;
      function available() { return $("#items input.item:checkbox:enabled"); }
      function sync() {
        const $items = available(), total = $items.length;
        const count = $items.filter(":checked").length;
        $all.prop({ checked: total > 0 && count === total,
          indeterminate: count > 0 && count < total, disabled: total === 0 });
        const values = $items.filter(":checked").map(function () { return this.value; }).get();
        $("#result").text(`selected=${count}/${total}\nmaster checked=${$all.prop("checked")}\nmaster mixed=${$all.prop("indeterminate")}\nitem changes=${itemChanges}\nvalues=${values.join(",")}`);
      }
      function addItem(value, text, checked, disabled) {
        const label = document.createElement("label"), input = document.createElement("input");
        input.type = "checkbox"; input.name = "item"; input.className = "item";
        input.value = value; input.checked = checked; input.disabled = disabled;
        label.append(input, document.createTextNode(" " + text));
        document.getElementById("items").append(label);
      }
      $all.on("change.selectAllLesson", function () {
        available().prop("checked", this.checked);
        sync(); // Setting checked does not emit change on each item.
      });
      $("#items").on("change.selectAllLesson", "input.item:checkbox", function () {
        itemChanges++; sync();
      });
      $("#add").on("click", function () {
        addItem("extra" + next, "Extra " + next, false, false); next++; sync();
      });
      $("#clear").on("click", function () { $("#items label").remove(); sync(); });
      $("#reset").on("click", function () {
        $("#items label").remove(); $("#items").prop("disabled", false);
        addItem("alpha", "Alpha", true, false);
        addItem("beta", "Beta", false, false);
        addItem("locked", "Locked (unavailable)", true, true);
        next = 1; itemChanges = 0; $("#submission").empty(); sync();
      });
      $form.on("submit", function (event) {
        event.preventDefault();
        const params = new URLSearchParams(new FormData(this));
        $("#submission").text(params.toString() || "(no successful controls)");
      });
      $("#reset").trigger("click");
    });
  </script>
</body>
</html>

初期結果は有効2個中1個の選択です。全体は一部選択表示です。

selected=1/2
master checked=false
master mixed=true
item changes=0
values=alpha
  1. Select all available itemsでselected=2/2、master checked=true、master mixed=falseです。再度押すと0/2で、Lockedは選択のままです。
  2. Betaだけ選ぶと1/2とmaster mixed=trueです。Alphaも選ぶと全体がチェックされます。ラベルやフォーカス中のSpaceでも操作できます。
  3. 全選択からAdd itemで未選択Extra 1を足すと2/3の一部選択です。それも選ぶと全選択に戻ります。
  4. Remove itemsで0/0となり全体を無効にします。追加すれば再び使えます。
  5. AlphaとBetaを選びInspect submissionを押すとitem=alpha&item=betaです。無効なLockedは除外し、選択がなければ(no successful controls)です。
  6. Reset exampleは初期3項目と出力・カウンターを作り直します。通信せず、追加と変更を取り消します。

個別変更と動的項目の同期

全体のchange処理ではthis.checkedを有効項目に代入しsync()を一度呼びます。.prop()代入だけで各項目のchangeは起きません。item changesは個別変更で増え、一括選択では増えません。

維持される#itemsへ処理を委譲するため追加項目にも同じ処理が働きます。available()は毎回現在DOMを調べます。起動時の集合を使い続けると追加・削除を反映できない場合があります。

追加・削除コードも明示的にsync()を呼びます。委譲はDOMの構造変更を監視しません。別コードがdisabledやcheckedを変更するなら同じ同期関数へ接続します。プログラム変更の自動検出はしません。

個別変更で金額計算なども行うなら、一括変更時の実行も設計します。必要に応じ.trigger("change")を使えても、全体再集計や通信を各項目で重複させないようにします。例は一括代入後一度同期します。

部分選択・初期化・送信の注意

indeterminateはJavaScriptで設定する表示で、初期HTML属性や別の送信値ではありません。checkedから独立しており、sync()は両方を毎回設定して古い表示を残しません。実際のlabelとcheckboxで基本キーボード操作を保ちます。

Inspect submissionはFormDataを表示するだけで通信しません。有効かつチェック済みの項目が名前と値を持ちます。無効値も業務上必要なら別のデータ設計とサーバー検証を検討します。選択状態を理由にサーバー権限検査を省きません。

Reset exampleは独自の再作成ボタンです。標準form resetは既定値へ戻す別操作で、完了後に全体表示を再計算します。type="reset"へ変えるだけでは動的項目削除とカウンター初期化を再現しません。

対象は文書に存在する項目です。未読込のサーバーページまで選ぶには別のサーバー状態が必要です。グループごとに範囲を分け、絞り込みがある場合は表示項目だけか全有効項目かを決め、選択子と文言を一致させます。