ActiveAdminで、全てのチェックボックスを選択するチェックボックスを追加する

## 動機
[前回の記事](https://qiita.com/Erga-mion/items/5ba48dbeb10573dd70c2)では、ActiveAdminで、複数のレコードを関連づけるために、フォームにチェックボックスを追加していました。 その続きになります。

そこに、選択できるレコードの数が膨大になることが考えられるので、それらを全て選択できるチェックボックスを追加して欲しい、という要望がありました。

ActiveAdminのドキュメント等を確認しましたが、そういった機能は用意されていないようですので、JavaScriptで実装することにしました。（もしありましたらご指摘お願いします:bow:）
## 動作環境
- ruby 3.2.2
- rails 7.0.8
- activeadmin 2.14.0

## 実装
前回の記事のものと同じものになります。

`User` の作成・更新ページに、複数の`Book` を選択できるチェックボックスを追加しています。

```mermaid
erDiagram
User ||--o{ BookReservation : user_id
BookReservation o{--|| Book : book_id
```
### モデル
```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :book_reservations, dependent: :destroy
  has_many :books, through: :book_reservations
end
```
```ruby:models/book.rb
class Book < ApplicationRecord
  has_many :book_reservations, dependent: :destroy
  has_many :users, through: :users

  validates :name, presence: true
end
```
```ruby:models/book_reservation.rb
class BookReservation < ApplicationRecord
  belongs_to :user
  belongs_to :book

  validates :user_id, uniqueness: { scope: :book_id }
end
```
### ActiveAdmin
```ruby:app/admin/user.rb
ActiveAdmin.register User do
  actions :all
  permit_params :name, :book_ids
  
    form do |f|
      inputs do
        f.input :books,
                as: :check_boxes,
                collection: Book.pluck(:name, :id),
                include_blank: false,
                input_html: { multiple: true }

        div class: 'check-all-books-container' do
          div class: 'check-all-books-inner-container' do
            check_box_tag('check_all_books', '', false, id: 'check_all_books') +
              label_tag('check_all_books', 'Select all Books')
          end
        end
      end
      
      f.actions
    end
    
 controller do  
   def update
      super
      
      return if resource.invalid?
      
      book_ids = params[:user][:book_ids].uniq.compact_blank

      if book_ids.present?
        book_ids.each do |book_id|
          resource.book_reservations.find_or_create_by!(book_id:)
        end
      end
      
      remove_reservations = resource.book_reservations.where.not(book_id: book_ids)
      remove_reservations.each(&:destroy!) if remove_reservations.present?
    end

    def create
      super
      
      return unless build_resource.persisted?
      
      book_ids = params[:user][:book_ids].uniq.compact_blank
      
      return if book_ids.blank?
      
      book_ids.each do |book_id|
        build_resource.book_reservations.create!(book_id:)
      end
    end
  end
end
``` 
今回変更した点を抜き出してみます。

全チェックボックスにチェックを入れるチェックボックスを追加します。
JavaScriptでDOM操作を行うため、各要素にクラス・IDを指定します。
```ruby
div class: 'check-all-books-container' do
    div class: 'check-all-books-inner-container' do
        check_box_tag('check_all_books', '', false, id: 'check_all_books') +
        label_tag('check_all_books', 'Select all Books')
    end
end
```
### JavaScript
そのチェックボックスで、全チェックボックス選択できるようにするため、以下のJavaScriptを追加します。
```js:app/assets/javascripts/active_admin.js
//= require active_admin/base

document.addEventListener('DOMContentLoaded', function() {
  const checkAllCheckboxes = document.querySelector('#check_all_books');

  if (checkAllCheckboxes) {
    const checkboxes = document.querySelectorAll('input[name="user[book_ids][]"]:not([type="hidden"])');

    if (Array.from(checkboxes).every(checkbox => checkbox.checked)) {
      checkAllCheckboxes.checked = true
    }

    checkboxes.forEach(function(checkbox) {
      checkbox.addEventListener('change', function() {
        if (Array.from(checkboxes).every(checkbox => checkbox.checked)) {
          checkAllCheckboxes.checked = true
        }
        else {
          checkAllCheckboxes.checked = false
        }
      })
    });

    checkAllCheckboxes.addEventListener('change', function() {
      checkboxes.forEach(function(checkbox) {
        checkbox.checked = checkAllCheckboxes.checked;
      });
    });
  }
});
```
まず、全て選択するチェックボックスの要素を取得します。
```js
const checkAllCheckboxes = document.querySelector('#check_all_books');
```
次に、表示されている各チェックボックスの要素は、以下のようにして取得します。
実際に表示されているチェックボックスの状態判定を行うために、`type="hidden"` の要素を除外して取得しています。
```js
const checkboxes = document.querySelectorAll('input[name="user[book_ids][]"]:not([type="hidden"])');
```
フォームを表示した時点で、全てのチェックボックスが選択されている（更新対象の`User`に全ての`Book` が関連づいている）場合には、全て選択するチェックボックスに自動的にチェックが入ります。
```js
if (Array.from(checkboxes).every(checkbox => checkbox.checked)) {
      checkAllCheckboxes.checked = true
    }

```
各チェックボックスの状態が変わったとき、チェックボックス全てにチェックが入っているかを確認します。
全てのチェックボックスが選択されていれば、全て選択するチェックボックスに自動的にチェックが入ります。それ以外の場合は、全て選択するチェックボックスのチェックが外れます。
```js
checkboxes.forEach(function(checkbox) {
      checkbox.addEventListener('change', function() {
        if (Array.from(checkboxes).every(checkbox => checkbox.checked)) {
          checkAllCheckboxes.checked = true
        }
        else {
          checkAllCheckboxes.checked = false
        }
      })
    });
```
全て選択するチェックボックスの状態が変わると、それに応じて全てのチェックボックスの状態が一括して変更されます。
チェックを入れると、全てのチェックボックスが選択され、チェックを外すと、全てのチェックが外れます。
```js
checkAllCheckboxes.addEventListener('change', function() {
      checkboxes.forEach(function(checkbox) {
        checkbox.checked = checkAllCheckboxes.checked;
      });
    });
```
### SCSS
今回追加したチェックボックスには、ActiveAdminのチェックボックスのようなデザインが適用されないため、それと同様のデザインを適用します。
```scss:app/assets/stylesheets/active_admin.scss
.check-all-books-container {
  padding: 10px;
}

.check-all-books-inner-container {
  padding: 0 0 0 20%;
}

input#check_all_books {
  margin-right: 0.2em;
}

label[for='check_all_books'] {
  float: none; 
  width: 100%; 
  font-size: 1.0em; 
  font-weight: bold; 
  color: #5E6469;
}
```
## 動作確認
全てのチェックボックスを選択するチェックボックスが動くようになりました。
![gamen_test1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542845/ef32c3df-4870-9be0-7144-09f9abda42e1.gif)