#17 HABTM Checkboxes (revised)
- Download:
- source codeProject Files in Zip (93.3 KB)
- mp4Full Size H.264 Video (10.8 MB)
- m4vSmaller H.264 Video (6.44 MB)
- webmFull Size VP8 Video (8.22 MB)
- ogvFull Size Theora Video (14.4 MB)
下の画面は、商品のリストを表示するRailsアプリケーションのページです。
商品は複数のカテゴリーに属し、Product
モデルとCategory
モデルは多対多の関連を持っています。以下に示す商品はいずれのカテゴリーにも属しておらず、今のままでは画面から商品にカテゴリーを関連づける方法がありません。今回のエピソードでは、ユーザが商品情報を編集するときにチェックボックスのリストからカテゴリーを設定できる機能を付加します。
2つのモデルはhas_many :through
を使って関連づけられています。以下はProduct
のコードです。
class Product < ActiveRecord::Base has_many :categorizations has_many :categories, through: :categorizations end
商品は、Categorization
という結合用モデルを介して複数のカテゴリを持ちます。
class Categorization < ActiveRecord::Base belongs_to :category belongs_to :product end
Category
モデルは、同じようなhas_many :through
の関係をProduct
に対して持っています。
紹介するテクニックはhas_and_belongs_to_many
の関係に対しても機能しますが、has_many :through
の方がずっと柔軟性が高いので今回はそちらを利用することにしました。利点の一つは、商品のcategory_ids
にアクセスするメソッドを提供してくれる点です。これをコンソールで実際に試すために、商品を取得してcategory_ids
をリストアップしてみます。
> p = Product.first > p.category_ids => []
このmethodを介してcategory_idを設定することも可能です。
> p.category_ids = [1,2] => [1, 2]
これはカテゴリーを設定して、categorizationのレコードも作成し、 それによって商品に対応するカテゴリーを取得できます。
> p.categories => [#<Category id: 1, name: "Board Games", created_at: "2011-12-26 21:22:48", updated_at: "2011-12-26 21:22:48">, #<Category id: 2, name: "Clothing", created_at: "2011-12-26 21:22:48", updated_at: "2011-12-26 21:22:48">]
ブラウザで商品を見てみると設定したカテゴリーが表示されています。
商品情報編集フォームにカテゴリーを追加する
商品情報の編集フォームを修正して、カテゴリーの追加や削除ができるようにします。フォームのテンプレートは以下の通りです。
<%= form_for(@product) do |f| %> <% if @product.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2> <ul> <% @product.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :price %><br /> <%= f.text_field :price %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
フォームには現状2つのフィールドがあります。それぞれ、商品の名称と価格です。もう一つのフィールドをカテゴリー用に追加します。その方法は以下のとおりです。
<div class="field"> <% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id %> <%= category.name %><br/> <% end %> </div>
上のコードでは、カテゴリー内をループしてそれぞれ用にチェックボックスを表示します。一つのチェックボックスを表示するだけであれば、f.check_box :category_ids
を使うことができますが、ここではcategory_ids
が配列であるために各属性用に複数の属性を設定する必要があり、うまく動作しません。その代わりに今回はマニュアルによる方法をとり、check_box_tag
を使用しました。複数のチェックボックスを扱えるように、名前の末尾に空の角カッコを付けます。これがRailsに対して複数の値をまとめてparams
ハッシュ内の配列に入れるように指示します。商品の編集用フォームにアクセスしてみると、チェックボックスが表示されます。
いくつかを選択してフォームを送信すると、カテゴリーが追加されます。
しかし商品を再度編集するためにページに戻ると、選択したカテゴリーにチェックが入っていません。これを修正するためにcheck_box_tag
に3つ目の引数を追加して、商品のcategory_id
にそのカテゴリーが含まれている場合はチェックボックスがチェックされるようにします。
<% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id) %> <%= category.name %><br/> <% end %>
商品を編集すると、正しくチェックボックスにチェックが入っています。
しかしこのフォームにはまだバグが存在します。選択されているチェックボックのチェックをはずしてフォームを送信すると、以前選択されていたカテゴリーが削除されていません。これは、選択されていないチェックボックスがサーバに送信されるための値を持たないことが原因です。この問題に対処するために、フォームにチェックボックスと同じ名前の隠しフィールドを追加して値にnil
を設定します。
<div class="field"> <%= hidden_field_tag "product[category_ids][]", nil%> <% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id) %> <%= category.name %><br/> <% end %> </div>
これでフォームが送信されるときにチェックボックスが一つもチェックされていない場合は、サーバにpostされるフィールドに隠しフィールドが追加されて、商品のカテゴリーが削除されます。
クリック可能なチェックボックスのラベル
編集フォームにはもう一つ解決しなくてはいけない問題があります。チェックボックスの隣のラベルをクリックしたときには、そのチェックボックスをクリックしたのと同じように振る舞うべきです。label
タグの中に名前をラップするのは簡単ですが、ラベルをid
を介してチェックボックスに関連づける必要があるものの、現状では各チェックボックスが同じid
を持っています。これはよくない設定なので、まずビューテンプレートを修正して各チェックボックスが個別のidを持つようにします。
<% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id), id: dom_id(category) %> <%= label_tag dom_id(category), category.name %><br/> <% end %>
各チェックボックスのid
をユニークにするために、Railsが提供するdom_id
メソッドを使用してそれにカテゴリを渡します。これがcategory_n
のid
を生成します。ここでn
はカテゴリーのid
です。次にlabel_tag
を使用してカテゴリー名を表示しますが、ここでdom_id
も使用してラベルをチェックボックスと関連づけます。これでページをリロードすると、ラベルをクリックすることでチェックボックスのチェックを入れたり外したりできるようになりました。
コードは正常に動作するようになりましたが、少し複雑になってしまいました。特にビューがそうです。これをフォームビルダーに抽出することもできますが、それは今回のエピソードの範囲を超えています。代替案としてSimpleFormやFormtasticなどのgemを利用する方法があります。SimpleFormにはcollection_check_boxesメソッドがあり、今回おこなった処理をずっと簡潔に行う方法を提供します。Formtasticには似たようなcheck_boxesメソッドがあります。