#88 Dynamic Select Menus (revised)
- Download:
- source codeProject Files in Zip (126 KB)
- mp4Full Size H.264 Video (16.8 MB)
- m4vSmaller H.264 Video (8.8 MB)
- webmFull Size VP8 Video (10.8 MB)
- ogvFull Size Theora Video (20.8 MB)
下の画面は、Personに新規レコードを作成するフォームです。このフォームには2つのドロップダウンメニューがあり、1つはその人の国を選択し、もう1つは州を選択するためのものです。
この2番目のメニューにはすべての国の州(state, province, county)が含まれるためにとても長くなり、選択しにくくなります。そこで、ユーザが1つ目のメニューで国を選択したときに、2つ目のメニューにフィルターがかかってその国に関連する項目だけが表示されれば、このフォームはとても使いやすくなるでしょう。
州の選択肢をグループ化する
まずこのフォームを表示しているテンプレートを見てみましょう。
<h1>New Person</h1> <%= form_for @person do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :country_id %><br /> <%= f.collection_select :country_id, Country.order(:name), :id, :name, include_blank: true %> </div> <div class="field"> <%= f.label :state_id, "State or Province" %><br /> <%= f.collection_select :state_id, State.order(:name), :id, :name, include_blank: true %> </div> <div class="actions"><%= f.submit %></div> <% end %>
2つのドロップダウンメニューにはcollection_select
を使ってデータが表示されますが、 これはフォームでbelongs_to
の関連のデータを表示する場合の通常のやり方です。今回のアプリケーションでは、Person
はCountry
(国)とState
(州)の両方に属して(belongs_to)います。
選択された国によって表示される州にフィルターをかけたいのですが、これをすべてクライアント側でJavaScriptを用いて行います。しかし現状ではどの州が選択された国に属するのかをJavaScriptが知るすべはありません。州について少し追加の情報を与える必要がありますが、それにはグループ化されたメニューを使う方法がいいでしょう。
Railsが提供するgrouped_collection_select
メソッドを使えば、まさにこれをおこなうことができます。このメソッドは多くの引数をとるので、そのしくみを理解するためにドキュメントを見ておくことをお勧めします。ただし、注意しなくてはいけないのは、form_for
を使用していてこのメソッドをform builderから呼び出している場合、1つ目の引数は使われず空にしておく必要があります。
州のcollection_select
の代わりにgrouped_collection_select
を使います。
<%= f.grouped_collection_select :state_id, Country.order(:name), :states, :name, :id, :name, include_blank: true %>
ページをリロードすると州は国ごとにグループ化されています。
これはよりよいユーザ体験を提供するだけでなく、ドロップダウンリストでどの州がどの国に属するかをJavaScriptが知るための情報があるということを意味します。
JavaScriptを追加する
次にフィルター用のコードを書き始めますが、これはCoffeeScriptで書きます。
jQuery -> states = $('#person_state_id').html() console.log(states) $('#person_country_id').change -> country = $('#person_country_id :selected').text() options = $(states).filter("optgroup[label=#{country}]").html() console.log(options) if options $('#person_state_id').html(options) else $('#person_state_id').empty()
このスクリプトではまずDOMがロードされたことを確認します。DOMがロードされたら、州のドロップダウンをid
(今回はperson_state_id
)から取得し、HTML(すべての選択肢を含む)を変数にコピーします。これは、ドロップダウンが変更されるときにすべての内容のコピーを保持している必要があるからです。
ユーザが国のドロップダウンを変更したら、表示される州も変更しなければいけないので、変更のイベント発生時に実行される関数を追加します。この関数ではjQueryの:selected
セレクタを使って選択された項目のテキストを取得してcountry
変数に保持します。
次に州にフィルターをかけて、選択された国の州だけを表示させます。ここでfilter
を使って選択された国の州を取得します。states
変数はHTML文字列として国と州を保持します(その一部を下に示しています)。 そこでラベルによって正しいoptgroup
を見つけて内容を取得することで、希望通りにリストにフィルターをかけることができます。
<optgroup label="Australia"> <option value="173">Australian Capital Territory</option> <option value="174">Northern Territory</option> <option value="175">New South Wales</option> <option value="176">Queensland</option> <option value="177">South Australia</option> <option value="178">Tasmania</option> <option value="179">Victoria</option> <option value="180">Western Australia</option> </optgroup>
選択肢が見つかったら州のドロップダウンに値を埋め、見つからなかったらそれを空にします。
正しく動作するかを見るためにフォームを試してみます。1つ目のドロップダウンで国、例えばオーストラリア、を選択すると、2つ目にはオーストラリアの州だけが表示されるようになりました。州がない国を選択すると、2つ目のドロップダウンは空になります。
ユーザが州を持たない国を選択した場合には、2つ目のドロップダウンがまったく表示されなくした方がいいでしょう。これを組み込むのは簡単です。
jQuery -> $('#person_state_id').parent().hide() states = $('#person_state_id').html() console.log(states) $('#person_country_id').change -> country = $('#person_country_id :selected').text() options = $(states).filter("optgroup[label=#{country}]").html() console.log(options) if options $('#person_state_id').html(options) $('#person_state_id').parent().show() else $('#person_state_id').empty() $('#person_state_id').parent().hide()
2つ目のドロップダウンはデフォルトでは隠されていて、ユーザが州がある国を選択した場合のみ表示されるようになりました。
国名をエスケープする
国名に特殊文字、特にシングルクォート(’)が含まれる場合(例えばCôte d’Ivoire)、直接jQueryセレクタに名前を埋めこんでいるため、JavaScriptコードで問題が発生します。このような名前はエスケープ処理した方がいいので、CoffeeScriptコードを修正します。
jQuery -> $('#person_state_id').parent().hide() states = $('#person_state_id').html() console.log(states) $('#person_country_id').change -> country = $('#person_country_id :selected').text() escaped_country = country.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g, '\\$1') options = $(states).filter("optgroup[label=#{escaped_country}]").html() console.log(options) if options $('#person_state_id').html(options) $('#person_state_id').parent().show() else $('#person_state_id').empty() $('#person_state_id').parent().hide()
動的なメニューはどの国名を選択しても正しく機能するようになりました。
今回のエピソードは以上です。今回書いたコードを、オリジナルのエピソード88のJavaScriptを動的に生成するコードと比べてみると面白いでしょう。このソリューションはより控えめ(less obtrusive)で、ブラウザのJavaScriptが無効化されていても正しく動作します。