14922


現在、railsのフォームで複数選択の項目をつろうとしているのですが、うまく実現できなくて困っています。

構成としては下記のようにモデルとモデルを１対多で結び、Enumで表現された選択項目のIDを選択してDBに保存するという形をとっています。

[code]

    Model User
    
    Model Select
    user_id, list_id
    
    Enum List
    a, 0
    b, 10
    
[/code]

nested_formを用いた実装の方法が一般的とのことですが、チェックを外した際にレコードをデリートするといった挙動がうまく表現できません。

何か参考となる情報やリンクをいただけないでしょうか？


CC BY-SA 3.0
2015-08-27T05:08:05.583
14891
2015-08-27T22:28:32.967
10201
question
ruby-on-rails ruby
railsのフォームでマルチチェックボックスを実現したい


画面の動きとしてはこういうイメージで合ってますか？  
（以下はユーザーが所有している車のメーカーを選択するサンプルアプリケーション）

[![画像の説明をここに入力](https://i.stack.imgur.com/CZWSU.gif)](https://i.stack.imgur.com/CZWSU.gif)

コードはこんな感じです。

[code]

    # user.rb
    class User < ActiveRecord::Base
      has_many :car_selections, inverse_of: :user
    
      accepts_nested_attributes_for :car_selections, allow_destroy: true
    
      validates :name, presence: true, uniqueness: true
    end
    
    # car_selection.rb
    class CarSelection < ActiveRecord::Base
      belongs_to :user
    
      validates :user, presence: true
      validates :maker, presence:true, uniqueness: { scope: :user_id }
    
      enum maker: %i(toyota nissan honda)
    end
    
    # users_controller.rb
    class UsersController < ApplicationController
      # 省略
    
      private
        def user_params
          params[:user][:car_selections_attributes].each { |index, hash| \ 
            hash[:_destroy] = hash[:maker].blank? }
          params.require(:user).permit(:name, car_selections_attributes: %i(id maker _destroy))
        end
    end
    
[/code]

[code]

    <% # _form.html.erb %>
    <%= form_for(@user) do |f| %>
      <% # 省略 %>
      <% car_selections = @user.car_selections.map {|record| [record.maker, record] }.to_h %>
      <% CarSelection.makers.each_with_index do |(maker, value), index| %>
        <label>
          <% car_selection = car_selections[maker] %>
          <% name_prefix = "user[car_selections_attributes][#{index}]" %>
          <%= hidden_field_tag "#{name_prefix}[id]", car_selection.try(:id) %>
          <% checked = car_selection.present? && !car_selection.marked_for_destruction? %>
          <%= check_box_tag "#{name_prefix}[maker]", maker, checked %>
          <%= maker %>
        </label>
      <% end %>
    
      <div class="actions">
        <%= f.submit %>
      </div>
    <% end %>
    
[/code]

こういう処理はRailsのaccepts_nested_attributes_forを活用してあげると良いです。  
accepts_nested_attributes_forとそのオプションについてはこちらの記事がよくまとまっていると思います。

[Rails4 - 複数の子レコードを作成・更新する. accepts_nested_attributes_for -
Qiita](http://qiita.com/hmuronaka/items/818c421dc632e3efb7a6)

ただ、選択されていない項目もフォームに表示しなければいけないので、チェックボックスを生成するviewのコードがちょっとゴチャゴチャしています。

また、チェックボックスが選択されていなければレコードを削除しなければいけないので、コントローラ側でむりやり `_destroy` に true/false
をセットしています。

あと、ビューで marked_for_destruction?
かどうかを調べているのは、バリデーションエラーでフォームが再表示されたときに、サブミット前の「選択/非選択」をそのまま保持するためです。

テストコードも書いたので、参考までにそちらも載せておきます。（Minitestを使用）

[code]

    require "test_helper"
    
    class UsersTest < Capybara::Rails::TestCase
      test "User form" do
        visit new_user_path
    
        # 新規作成時、バリデーションエラーが起きても選択値が残っていること
        check 'toyota'
        check 'honda'
        click_on 'Create User'
        assert_content "Name can't be blank"
        assert_checked_field 'toyota'
        assert_unchecked_field 'nissan'
        assert_checked_field 'honda'
    
        # 新規作成時、選択値が正しく保存されること
        fill_in 'Name', with: 'Alice'
        click_on 'Create User'
        assert_content "User was successfully created."
        assert_content 'toyota'
        refute_content 'nissan'
        assert_content 'honda'
        click_on 'Edit'
        assert_checked_field 'toyota'
        assert_unchecked_field 'nissan'
        assert_checked_field 'honda'
    
        # 更新時、バリデーションエラーが起きても選択値が残っていること
        fill_in 'Name', with: ''
        uncheck 'toyota'
        check 'nissan'
        click_on 'Update User'
        assert_content "Name can't be blank"
        assert_unchecked_field 'toyota'
        assert_checked_field 'nissan'
        assert_checked_field 'honda'
    
        # 更新時、選択値が正しく保存されること
        fill_in 'Name', with: 'Alice'
        click_on 'Update User'
        assert_content "User was successfully updated."
        refute_content 'toyota'
        assert_content 'nissan'
        assert_content 'honda'
    
        # すべて未選択にしても正しく保存されること
        click_on 'Edit'
        uncheck 'nissan'
        uncheck 'honda'
        click_on 'Update User'
        assert_content "User was successfully updated."
        refute_content 'toyota'
        refute_content 'nissan'
        refute_content 'honda'
    
        # 念のため、CarSelectionのデータが存在しないことを確認
        user = User.find_by_name 'Alice'
        assert_empty user.car_selections
      end
    end
    
[/code]

このサンプルアプリケーションはGitHubにアップしてあるので、わからないところがあれば手元で動かしてみることも可能です。

<https://github.com/JunichiIto/enum-checkbox-sandbox>

ちょっとややこしいところもありますが、じっくりと確認して実装の参考にしてみてください。


CC BY-SA 3.0
2015-08-27T22:20:46.923
14922
2015-08-27T22:28:32.967
2015-08-27T22:28:32.967
85
85
14891
answer