Rails3 で selectbox (combobox) の中身を動的に入れ替える処理が必要になった。
ググってみると大概 observe_field を利用した Ajax.update を行うものだったが、Rails3 には
observe_field が削除されている。(正確には Rails2.3.9 で削除されたっぽい)
なので無い脳ミソをフル稼働させて考えてみた結果をメモ。
■仕様(やりたい事・目指した事)
- selectboxA を変更したら、その内容で selectboxB の内容が置き換わる。
- n段に対応。(selectboxA → selectboxB → selectboxC → ... → selectbox(N))
今回は大分類・中分類・小分類の 3 段で考える。
- 全ての selectbox の先頭に :inclide_blank => true を入れる。
- DRY を徹底する。(同じコードはなるべく書かない)
調べてみるとやり方はかなり豊富。仕様と照らし合わせてどのやり方をチョイスするか選んだ方がよさそう。
今回の仕様から、RJS つかって動的にコンテンツを更新する方法にした。
■考え方
まず selectbox のデータを格納する model を考える。今回は kind(分類) という model を作成する。
kind |
PK | id : integer |
| name : string |
FK | kind_id : integer |
kind_id が外部キーとなっており、自分の親のキーを保持する。
(中分類のデータなら所属する大分類のIDを保持する)
次に html タグの動的入れ替えだが、以下の範囲を入れ替えるように考えた。
<p id="middle_select">
<select id="kind_id_middle">
<option></option> *黄色の部分を Ajax で入れ替える
</select>
</p>
理由は、selectbox の先頭に blank option をつけたかったから。直に <option value=""></option>
タグを書けば option 範囲の入れ替えでもいけるのだが、それはあまりやりたくなかった。(要こだわり)
■実装
model は作ってあり、データも適当にいれているものとする。
大分類の kind_id は 0 を指定しておく。(TOP レベルで親はいませんという意味で)
必要な routes.rb は記述してあるものとする。
参考データ
id | name | kind_id |
1 | 大分類1 | 0 |
8 | 中分類1 | 1 |
22 | 中分類2 | 1 |
43 | 小分類1 | 8 |
44 | 小分類2 | 8 |
65 | 小分類3 | 22 |
初期表示時の view。
# select.html.erb
<%= label "kind_id", "large", "大分類:" %>
<%= select "kind_id", "large",
Kind.where("kind_id = 0").map{|p| [p.name, p.id]},
{:include_blank => true},
{:onchange => remote_function(:url => {:action => "change_select"},
:with => "'kind_id[large]=' + escape(this.value)")} %>
<%= render :partial => "middle_select", :locals => {:middle_kinds => @middle_kinds} %>
<%= render :partial => "small_select", :locals => {:small_kinds => @small_kinds} %>
ポイントは
- render 使って動的入れ替えする部分を外だしにする。(RJS でも render すれば DRY になる)
- observe_field がないので、onchange イベントに remote_function ひっつける。
- remote_function :with で大分類の値を POST する。
- 複数のコンテンツをいっぺんに変えるため (大分類 → [中分類, 小分類]) remote_function :update は未指定。
中分類の view。select.html.erb と同じ階層に作成する。
# _middle_select.html.erb
<%= label "kind_id", "middle", "中分類:" %>
<%= select "kind_id", "middle",
middle_kinds.map{|p| [p.name, p.id]},
{:include_blank => true},
{:onchange => remote_function(:url => {:action => "change_select"},
:with => "'kind_id[middle]=' + escape(this.value)")} %>
小分類の view。select.html.erb と同じ階層に作成する。
# _small_select.html.erb
<%= label "kind_id", "small", "小分類:" %>
<%= select "kind_id", "small",
small_kinds.map{|p| [p.name, p.id]},
{:include_blank => true} %>
中分類には選択したら小分類を入れ替える必要があるので onchange イベントを追加するが、小分類は入れ替え処理が不要なので、onchange は省略する。
次は controller。
# controller.rb
# 初回表示時 action
def select
@middle_kinds = []
@small_kinds = []
end
# onchange 時のイベント
def change_select
if params[:kind_id][:large]
@middle_kinds = params[:kind_id][:large] != "" ?
Kind.where("kind_id = #{params[:kind_id][:large]}") :
[]
@small_kinds = []
else
@middle_kinds = nil
@small_kinds = params[:kind_id][:middle] != "" ?
Kind.where("kind_id = #{params[:kind_id][:middle]}") :
[]
end
end
初回表示時は大分類だけに値が設定されているシチュエーションなので、中分類、小分類は空にしておく。
change_select イベントは大分類、中分類で共同で利用するイベント。params[:kind_id][:large]が存在すれば(nil でなければ)大分類の change イベントと判別できるので、大分類変更時は、中分類を読み込んで小分類をリセットする。
中分類変更時は (params[:kind_id][:middle] != nil) 小分類のみ書き換えするため、あえて @middle_kinds に nil を設定して書き込み対象外の判別が行えるようにした。(後述 RJS 参照)
最後に RJS。select.html.erb と同じ階層に作成。
# change_select.js.rjs
if @middle_kinds
page.replace_html "middle_select", :partial => "middle_select", :locals => {:middle_kinds => @middle_kinds}
page.visual_effect :highlight, "middle_select"
end
page.replace_html "small_select", :partial => "small_select", :locals => {:small_kinds => @small_kinds}
page.visual_effect :highlight, "small_select"
@middle_kinds == nil 時は更新不要なので、更新するかしないかを最初に判定している。
一応、視覚的効果があるほうがインターフェース的に親切らしいので、動的入れ替え後は effect した。
■最後に
今回は複数更新することを念頭に入れていたため RJS を利用したが、1個のみの更新 (大分類 → 中分類) だけだったら、remote_finction :update => "middle_select" を指定して、controller で render :partial => "middle_select" ってする方が簡単。けど、こっちの方が応用 & 拡張性に優れている気がするので、RJS 積極的に使っていこうかな。
以上。長文でわかりづらく、失礼しました。