form.collection_check_boxesを複数使ったフォームページを作成しようとしたらかな〜〜り苦戦したので、忘れない様に書いていこうと思います。
実現したいこと
現在問題投稿サイトを作成中です。上記画像は投稿した問題のeditページです。問題にどんなタグをつけるかをタググループ(部位、部位詳細)にそれぞれ属するタグから選んで更新でき、かつ、下記画像の様に更新したタグ(頭頸部、胸部、骨)がshowページでチェックがついた状態で表示されることが目標です。(画像のUIはPFの作成途中なので非常に見苦しいですが、ご容赦ください...)
開発環境
Mac OS Catalina: 10.15.7
ruby: 2.6.6
rails: 6.1.1
モデル
今回登場するモデルは以下の3つです。(中間テーブルを除く)
Quiz
Tag
TagGroup
それぞれの関係性は
Quiz 多対多 Tag
Quiz 多対多 TagGroup
TagGroup 一対多 Tag
になっています。
#何故複数のform.collection_check_boxesを複数使ったフォーム欄の作成が複雑なのか
理由は
・複数のinputタグに対して同じ属性は使えない
・仮想カラムを設定しないとeditページを開いた時にチェックボックスにチェックが入った状態にならない
以上二つが挙げられます。
複数のform.collection_check_boxesの実装
まず、ダメな実装方法を示します。以下の様にviewを設定します
<div class="bg-info">
<label for="body-regions">部位</label>
<div id="body-regions">
<%= form.collection_check_boxes :tag_ids, @body_regions, :id, :name %>
</div>
<label for="body-region-details">部位詳細</label>
<div id="body-region-details">
<%= form.collection_check_boxes :tag_ids, @body_region_details, :id, :name %>
</div>
</div>
form.collection_check_boxes
で設定している第一引数をみて欲しいのですが、どちらのinputタグも:tag_ids
となっています。上述した様に、複数のinputタグに対して同じ属性は使えません。ここが一緒だと更新時のParamaterに値が入らなかったり、ストロングパラメータに弾かれたりします。
*form.collection_check_boxes
の引数などを詳しく知りたい方はこちらの記事が参考になると思います。
http://l-light-note.hatenablog.com/entry/2017/10/16/153717
そこで、第一引数のメソッドを自分で作ります。第一引数のメソッドは更新対象のオブジェクトのメソッドであるため、今回だとQuizモデルで設定します。
ただし、ここでも注意点があります。Quizモデルの設定です。またまたダメな例を書きます。
has_many :details_ids, class_name: 'Tag'
<label for="body-regions">部位</label>
<div id="body-regions">
<%= form.collection_check_boxes :tag_ids, @body_regions, :id, :name %>
</div>
<label for="body-region-details">部位詳細</label>
<div id="body-region-details">
<%= form.collection_check_boxes :details_ids, @body_region_details, :id, :name %>
</div>
二つ目のinputタグの第一引数をmodelで設定しました。上記の様に設定すると更新はできる様になりますが、editページを開いた時にチェックボックスにチェックが入った状態になりません。(原因は最後までよくわかりませんでした...)
そこで仮想カラムを作ります。そうするとそのカラム名をメソッドの様に呼び出すことができる様になり、上記の問題も解決できました。以下にその設定を書きます。(仮想カラムの設定については以下の記事を参考にしました。)
http://mainichiaisatu.hatenablog.com/entry/2017/03/26/225336
attr_writer :body_region_tag_ids, :body_region_detail_tag_ids
#タググループ(部位)のタグを取得
def body_region_tag_ids
@body_region_tag_ids = self.tags.where(tag_group_id: 1).ids.sort if self.tag_ids.present?
end
#タググループ(部位詳細)のタグを取得
def body_region_detail_tag_ids
@body_region_detail_tag_ids = self.tags.where(tag_group_id: 2).ids.sort if self.tag_ids.present?
end
<label for="body-regions">部位</label>
<div id="body-regions">
<%= form.collection_check_boxes :body_region_tag_ids, @body_regions, :id, :name %>
</div>
<label for="body-region-details">部位詳細</label>
<div id="body-region-details">
<%= form.collection_check_boxes :body_region_detail_tag_ids, @body_region_details, :id, :name %>
</div>
def update
if @quiz.update(quiz_params) && @quiz.update(tag_ids: tag_params)
flash[:success] = '問題を更新しました。'
redirect_to @quiz
else
flash[:danger] = '問題は更新されませんでした'
render :edit
end
end
private
def tag_params
params.require(:quiz).permit(body_region_tag_ids: [], body_region_detail_tag_ids: [])
params[:quiz][:body_region_tag_ids] + params[:quiz][:body_region_detail_tag_ids]
end
(1)まずはモデルから設定します。attr_writer :body_region_tag_ids, :body_region_detail_tag_ids
で仮想カラム(インスタンスメソッド)を指定します。
その後、それぞれのインスタンスメソッドの処理を書きます。今回はタググループに属するタグのidを配列で取得したかったため、上記の様に書きました。
(2)ビューにて第一引数を(1)で設定したインスタンスメソッドに書き換えます。
(3)quizzes#updateにて更新処理を書きます。また、ストロングパラメータにてインスタンスメソッドで取得した複数の配列を一つの配列にまとめて取得できる様にします。この配列を使って更新します。
これで実装完了です。バリデーションなどはまだ書けていませんが、忘れないうちに書かせていただきました。
結語
簡単だと思っていましたが、想像以上にかなーり手こずりました。挫けずPF作成を続けていこうと思います。