Rails の form_for でエラー出たときに、エラーはちゃんとエラーが出た項目の下に出したい。

Rails の form_for でエラー出たときに、エラーはちゃんとエラーが出た項目の下に出したい。

エラーは各フォームの近くに表示したいけど form_for の中身の記述を増やしたくはないのでカスタムフォームビルダーを作成する。

Rails Guide でやり方読む

フォームヘルパー — Rails ガイド

どうやってカスタムフォームビルダーをつくる

FormBuilder を継承してタグを書きだす部分を override する。

class LabellingFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(attribute, options={})
    label(attribute) + super
  end
end

どうやってカスタムフォームビルダーをつかう

ブロックはそのままで form_forbuilder を指定するだけ。

使用前

<%= form_for @model do |f| %>
  <%= f.text_field :name %>
<% end %>

使用後

<%= form_for @model, builder: LabellingFormBuilder do |f| %>
  <%= f.text_field :name %>
<% end %>

中身に手を加える必要がない(重要っぽい)。

素の FormBuilder はどんな感じ

つかえる変数

@objectでモデルのインスタンスにアクセスできることがわかる。

# 抜粋
      def initialize(object_name, object, template, options)
        @nested_child_index = {}
        @object_name, @object, @template, @options = object_name, object, template, options
        @default_options = @options ? @options.slice(:index, :namespace) : {}
        if @object_name.to_s.match(/\[\]$/)
          if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
            @auto_index = object.to_param
          else
            raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
          end
        end
        @multipart = nil
        @index = options[:index] || options[:child_index]
      end

各書きだしメソッド

タグを書きだすメソッドは Array から eval されている。今回特に使えそうな項目はないので単純に super のままで OK。

# 抜粋
      class_attribute :field_helpers
      self.field_helpers = [:fields_for, :label, :text_field, :password_field,
                            :hidden_field, :file_field, :text_area, :check_box,
                            :radio_button, :color_field, :search_field,
                            :telephone_field, :phone_field, :date_field,
                            :time_field, :datetime_field, :datetime_local_field,
                            :month_field, :week_field, :url_field, :email_field,
                            :number_field, :range_field]

      (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
        class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
          def #{selector}(method, options = {})  # def text_field(method, options = {})
            @template.send(                      #   @template.send(
              #{selector.inspect},               #     "text_field",
              @object_name,                      #     @object_name,
              method,                            #     method,
              objectify_options(options))        #     objectify_options(options))
          end                                    # end
        RUBY_EVAL
      end

カスタマイズする

まずためす

  class WithErrorFormBuilder < ActionView::Helpers::FormBuilder
    def pick_errors(attribute)
      return nil if @object.nil? || (messages = @object.errors.messages[attribute]).nil?
      lis = messages.collect do |message|
        %{<li>#{message}</li>}
      end.join

      %{<ul class="errors">#{lis}</ul>}.html_safe
    end

    def text_field(attribute, options={})
      return super if options[:no_errors]

      super + pick_errors(attribute)
    end
  end
<%= form_for @model, builder: WithErrorFormBuilder do |f| %>
  <%= f.text_field :name %>
<% end %>

いい感じに書きだされた。

素の FormBuilder にならって一気に定義する感じにする。

  class WithErrorFormBuilder < ActionView::Helpers::FormBuilder
    def pick_error(attribute)
      return nil if @object.nil? || (messages = @object.errors.messages[attribute]).nil?
      lis = messages.collect do |message|
        %{<li>#{message}</li>}
      end.join

      %{<ul class="errors">#{lis}</ul>}.html_safe
    end

    (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
      class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
        def #{selector}(attribute, options = {})
          return super if options[:no_errors]

          super + pick_error(attribute)
        end
      RUBY_EVAL
    end
  end

いけました。

除外されてる check_boxradio_buttonselect と同じ感じに一発で書けたら素敵なのでそうしたいですね。