ユーザ登録時に併せて言語の習得レベルを設定する、みたいな場合を考えます。

= form_for :user do |f|

  ..(snip)..

  # ここから
  .form
    = f.fields_for :user_languages do |form|
      %p
        = form.label :language_id, '言語'
        = link_to 'Remove', 'javascript:void(0)'
        = form.collection_select :language_id, Language.all, :id, :name

      %p
        レベル
        - Level.all.each do |level|
          = form.radio_button :level_id, level.id
          = form.label "level_id_#{level.id}", level.name
  # ここまで

  = link_to 'Add', 'javascript:void(0)'

これ自体は Rails を使っていれば fields_for 使ってさくっと処理できるから良いんですが、こういうときにありがちなのが、複数の言語の習得レベルをまとめて設定するための

「 .form 要素を追加したり削除したり出来るようにしてほしい」

みたいな要望です。これ、fields_for を使ってるとつらいと思うんですよね。。なぜなら、生成される要素の id が user_user_languages_attributes_0_level_id_1 とか、name が user[user_languages_attributes][0][level_id] とかなってるからです。.form 要素が複雑だったら泣けます。

パッと思いつくのは「この .form 要素を clone してきて、id とか name を良い感じに書き換える」みたいな方法じゃないでしょうか。まぁこれでももちろんうまくいくと思います。が、めんどくさいです。

2個目以降には削除ボタン付けたりとか、3個まで作って真ん中を消したときにインデックス部分が0と2になっちゃわないようにしたりとか5個になったら追加ボタン出さないようにしたりとか(以下略 しかもデザイン変更とかで DOM が変わったらすぐ動かなくなったりして生きるのが辛い感じになることが容易に想像できます。(´*ω*`)

そこで、今回は Google の作っている AngularJS というフレームワークを利用してみました。AngularJS とは簡単に言うと、何か変更を加えたときにそれを検知して、画面を再度 render してくれるものです。こんな感じ(※これは AngularJS じゃないけどこんなイメージw)

AngularJS

# html
%div{ ng_controller: 'LanguageCtrl' }
  %div{ ng_repeat: 'language in languages' }
    .form
      = f.fields_for :user_languages, child_index: '{{$index}}' do |form|
        %p
          = form.label :language_id, '言語'
          = link_to 'Remove', 'javascript:void(0)', ng_click: 'remove($index)', ng_show: '!$first'
          = form.collection_select :language_id, Language.all, :id, :name

        %p
          レベル
          - Level.all.each do |level|
            = form.radio_button :level_id, level.id, id: "user_user_languages_attributes_{{$index}}_level_id_#{level.id}"
            %label{ for: "user_user_languages_attributes_{{$index}}_level_id_#{level.id}" }= t("learning_level.#{level.name}")

    = link_to 'Add', 'javascript:void(0)', ng_click: 'add()'

# coffeescript
window.LanguageCtrl = ($scope) ->
  $scope.languages = [{}]

  $scope.add = ->
    $scope.languages.push {}

  $scope.remove = (index) ->
    $scope.languages.splice index, 1

たったこれだけです。説明は省きますが、追加ボタンを押すと要素が追加されて、それを AngularJS が検知し、画面を再度 render してくれます。書くのは若干手間かもしれないですが、DOM に依存しないしバグが出にくそう。

fields_for のオプションとして child_index を使うと生成される要素のインデックスが指定できるっていうのがポイントですかね。こんなイメージ。

child_index: 3
# => user_user_languages_attributes_3_level_id_1

child_index: 8
# => user_user_languages_attributes_8_level_id_1

ラジオボタンの id とか、そのラベル部分で $index がうまく置換されなかったので直接指定したりしてごにょごにょしたりしてます。あと、AngularJS の module とか使ったりした方が良いんだろうと思いますがとりあえず公開します。参考になれば!><

なんか名字と名前入れたらフルネームがすぐに表示されるみたいな簡単なサンプルはいっぱいあるんだけど、複雑な例になると全然見つからないのが辛い。。実用的なサンプルが見たいよー。
このエントリーをはてなブックマークに追加