経緯

親モデルと同時に子モデルを保存するときに超便利なnested_formというgemがあります。

https://github.com/ryanb/nested_form

たまたま、以下のような仕様を実装する機会がありました。

  • 自由に子の数は変えられる
  • 最大でも5個に抑える
  • 何番目のフィールドかを表示する

その方法をば。

nested_formの使い方

[Rails] nested_attributesの使い方

こちらをご参照ください。

上限の決め方

実はやり方が公式に載っています。

How To: Limit max count of nested fields

ここにあるとおり、

$(function() {
  $(document).on('nested:fieldAdded', function(e) {
    var link = $(e.currentTarget.activeElement);
    if (!link.data('limit')) {
      return;
    }
    if (link.siblings('.fields:visible').length >= link.data('limit')) {
      link.hide();
    }
  });

  $(document).on('nested:fieldRemoved', function(e) {
    var link = $(e.target).siblings('a.add_nested_fields');
    if (!link.data('limit')) {
      return;
    }
    if (link.siblings('.fields:visible').length < link.data('limit')) {
      link.show();
    }
  });
})

をAssetとして読み込んでやるだけです。

個人的に、coffeeで全部書いてるのに一つだけjsってのも気持ちが悪いので、書き直したのがこちらです。

$ ->
  do window.nestedFormLimitter = ->
    $(document).on 'nested:fieldAdded', (e) ->
      $link = $(e.currentTarget.activeElement)
      if (!$link.data('limit'))
        return
      if ($link.siblings('div.fields:visible').length >= $link.data('limit'))
        $link.hide()

    $(document).on 'nested:fieldRemoved', (e) ->
      $link = $(e.target).siblings('a.add_nested_fields')
      if (!$link.data('limit'))
        return
      if ($link.siblings('div.fields:visible').length < $link.data('limit'))
        $link.show()

あとはViewファイルの追加ボタンに以下のようにdata-link属性にリミットの個数を入れてやるだけです。

f.link_to_add '追加' :members, data: { limit: 5 }

フィールドの順番を表示する

フィールドの順番を表示するだけであれば、特に表示Ruby側で使えるようにする必要がないので、上記のCoffeeScriptに追記しました。

$ ->
  do window.nestedFormLimitter = ->
    # フィールドの番号を振って、表示する関数
    do setFieldNum = ->
      $('div.fields:visible').each ->
        $('h3 span.order-num', $(@)).text($('div.fields:visible').index(this) + 1)

    $(document).on 'nested:fieldAdded', (e) ->
      setFieldNum() # 追記
      $link = $(e.currentTarget.activeElement)
      if (!$link.data('limit'))
        return
      if ($link.siblings('div.fields:visible').length >= $link.data('limit'))
        $link.hide()

    $(document).on 'nested:fieldRemoved', (e) ->
      setFieldNum() # 追記
      $link = $(e.target).siblings('a.add_nested_fields')
      if (!$link.data('limit'))
        return
      if ($link.siblings('div.fields:visible').length < $link.data('limit'))
        $link.show()

解説

nested_formではフォームが追加されたときはnested:fieldAdded、削除されたときはnested:fieldRemovedというイベントが発火します。

$linkはこのイベントを発火させたボタンのDOMが入っていて、一定数を超えているとき、非表示にして、同時に番号を表示させています。

ただ、ここで、一点注意なのは、クライアント側でDOMを操作してやるようなことをされてしまっては、必ずしも上限の個数のみのデータが飛んで来るわけではありません。

ちゃんと、モデル側でもバリデーションをかけてやりましょう。

以下は個数が大きすぎたときに弾くバリデーションの一例です。

class Group < ActiveRecord::Base
  validate :member_presence

  private
  def member_presence
    unless self.members.size <= 5
      errors.add(:base, "メンバー多すぎ!エグザイルか!")
    end
  end
end

仮にDOMを操作されても「メンバー多すぎ!エグザイルか!」と表示されます。1

まとめ

  • 公式をよく見れば意外とちゃんと載ってる
  • JSのバリデーションに頼りきらず、ちゃんとModelでもバリデーションを書く

以上で個数の上限を決めると同時にフィールドの番号を表示できるようになります。

公式のWikiはひとまず、チェックしてみる癖を付けてもいいかもしれませんね。


  1. 「Choo Choo TRAIN」を歌いながら女の子に「チューしてよ!」って叫んでた友達を思い出しました。