ラベル付きエラー表示付きフォームの雛形が欲しい!

前回までにform_forの:builderオプションの基本が理解できたので、あとは自分の作りたいように作ってみる。前回までのフォームは以下のようになっている。

<% form_for :slip, :url=>{:action => 'create'}, :html=>{:autocomplete=>'off'} do |f| %>
  <p><label for="slip_number">伝票No.</label><br />
    <%= f.text_field 'number'  %>
    <%= error_messages_on 'slip', 'number' %>
  </p>

  <p><label for="slip_executed_on">実行日</label><br />
    <%= f.text_field 'executed_on'  %>
    <%= error_messages_on 'slip', 'executed_on' %>
  </p>

  <p><label for="slip_total_yen">合計金額</label><br />
    <%= f.yen_field 'total_yen'  %>
    <%= error_messages_on 'slip', 'total_yen' %>
  </p>
...(中略)...
  <%= submit_tag %>
<% end %>
  • text_fieldのオブジェクト名、フィールド名が決まれば、label、error_messages_onもルールに従って設定可能だ。
  • ラベル文字列は日本語になっているが、scaffold直後は英語だ。英語の状態なら、ルールに従って設定可能だ。
  • 以上のことを考えて、pタグ、labelタグ、error_messages_onでラッピングしたFormBuilderクラスを新たに設定してみる。

LabelFormWithErrorMessagesBuilderの作成

まずは、以下のようなコードにしてみた。

  • ファイルlabel_form_with_error_messages_builder.rbを新規作成した。
    • ファイル名とクラス名をこのように対応させておくことで、Railsのオートロードが機能して、requireすることなく定義したクラスを利用できるようになる。
  • 通常、ビュー環境でしか利用できないメソッドは、@template.error_messages_onのように@templateに対するメソッドとすることで、この環境でも利用できる。
# ビルダー: app/helpers/label_form_with_error_messages_builder.rb
class LabelFormWithErrorMessagesBuilder < ActionView::Helpers::FormBuilder
  %w(text_field text_area yen_field).each do |selector|
    src = <<-end_src
      def #{selector}(field, *args)
        @template.content_tag('p', 
          @template.content_tag('label', field.to_s.humanize, :for=>"\#{@object_name}_\#{field}") + '<br/>' + #......(注1)
          super + 
          @template.error_messages_on(@object_name, field))
      end
    end_src
    class_eval src, __FILE__, __LINE__
  end
end
  • (注1)何故、式展開をエスケープするのか?(:for=>"\#{@object_name}_\#{field}")
    • ヒアドキュメントend_src間の#{Ruby式}は、変数srcへ代入される時に式展開される。
    • ところが、class_eval で動的メソッド追加を行う時には "#{@object_name}_#{field}" であって欲しい。
    • (式展開はメソッドが実行される時に処理して欲しい。)
    • そのため、式展開の先頭でエスケープして "\#{@object_name}_\#{field}" とすることでそれを実現している。
  • 改善したいこと
    • text_field、text_area、yen_fieldしか対応していない。radio_buttonやcheck_box、selectファミリーなどにも対応しておきたい。(でも、文字列として全部列挙するのは面倒)
    • ラベル文字列を日本語化したい。


改善したい点を踏まえて、以下のようにしてみた。

  • instance_methodsによって、FormHelper、FormOptionsHelperに定義されているインスタンスメソッドの配列を得ている。(不要と思われるメソッドは引き算で除外した。)
    • Rails2.0.2のソースを読むとfield_helpersで必要なメソッドの配列を取得できそうだったが、タイミングの違いで自分の定義したyen_fieldが含まれていない。そのため、instance_methodsを利用する方式した。
  • @object_nameからそのオブジェクト名のクラスobject_classを得て、object_class.human_attribute_name(field.to_s)とすることで、ラベル文字列をテーブルのカラム名として取得している。こうしておくと、カラム名Ruby-GetTextの翻訳対象になるので、カラム名の日本語訳を設定しておけば、翻訳された日本語で表示されるのだ。
# ビルダー: app/helpers/label_form_with_error_messages_builder.rb
class LabelFormWithErrorMessagesBuilder < ActionView::Helpers::FormBuilder
  selectors = ActionView::Helpers::FormHelper.instance_methods - %w(label form_for field_for hidden_field)
  selectors += ActionView::Helpers::FormOptionsHelper.instance_methods
  selectors.each do |selector|
    src = <<-end_src
      def #{selector}(field, *args)
        object_class = Object.const_get(@object_name.to_s.classify)
        text_node = object_class.human_attribute_name(field.to_s)

        @template.content_tag('p', 
          @template.content_tag('label', text_node, :for=>"\#{@object_name}_\#{field}") + '<br/>' + 
          super + 
          @template.error_messages_on(@object_name, field))
      end
    end_src
    class_eval src, __FILE__, __LINE__
  end
end
  • form_forの:builderオプションに、上記LabelFormWithErrorMessagesBuilderを指定することで、今までのフォームが何だったのかと思うくらいシンプルに!素晴らしい仕組みだ。一度理解したら、もう以前のフォームには戻りたくない...。
<% form_for :slip, :url=>{:action => 'create'}, :html=>{:autocomplete=>'off'}, :builder=>LabelFormWithErrorMessagesBuilder do |f| %>
  <%= f.text_field :number  %>
  <%= f.text_field :executed_on  %>
  <%= f.yen_field :total_yen  %>
...(中略)...
  <%= submit_tag %>
<% end %>

余談、define_methodを利用した書き方

LabelFormWithErrorMessagesBuilderは、define_methodを使って以下のように書くことも可能だ。式展開のタイミングを考える必要がないので、(動的にコードを変える必要がなければ)この書き方の方が理解し易いかもしれない。

  • define_method do 〜 endブロック内のコードは、そのままの状態でメソッドとして定義される。(メソッド定義する時に"#{式展開}"されることはない。)
  • ブロック引数|field, *args|は、define_methodによってdef text_field(field, *args)のように定義される。
# ビルダー: app/helpers/label_form_with_error_messages_builder.rb
class LabelFormWithErrorMessagesBuilder < ActionView::Helpers::FormBuilder
  selectors = ActionView::Helpers::FormHelper.instance_methods - %w(label form_for field_for hidden_field)
  selectors += ActionView::Helpers::FormOptionsHelper.instance_methods
  selectors.each do |selector|
    define_method(selector) do |field, *args|
      model_class = Object.const_get(@object_name.to_s.classify)
      text_node = model_class.human_attribute_name(field.to_s)

      @template.content_tag('p', 
        @template.content_tag('label', text_node, :for=>"#{@object_name}_#{field}") + '<br/>' + 
        super + 
        @template.error_messages_on(@object_name, field))
    end
  end
end

slip_form_forの作成

さらに、<% form_for :slip, :url=>{:action => 'create'}, :html=>{:autocomplete=>'off'}, :builder=>LabelFormWithErrorMessagesBuilder do |f| %>は、いかにも長ったらしい...。form_forのデフォルト値を設定済みのslip_form_forヘルパメソッドを定義してみる。

# ヘルパー: app/helpers/application_helper.rb
module ApplicationHelper
...(中略)...
  def slip_form_for(name, *args, &block)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options.merge!(:html=>{:autocomplete=>'off'}, 
                   :builder=>LabelFormWithErrorMessagesBuilder)
    args << options
    form_for(name, *args, &block)
  end
end


slip_form_forまで定義しておくと、極限までシンプルになった感じだ!

<% slip_form_for :slip, :url=>{:action => 'create'} do |f| %>
  <%= f.text_field :number  %>
  <%= f.text_field :executed_on  %>
  <%= f.yen_field :total_yen  %>
...(中略)...
  <%= submit_tag %>
<% end %>


比較のため、以下はstart_form_tagの頃のコード。驚愕の差がある...。

<%= start_form_tag :action => 'create' %>
  <p><label for="slip_number">伝票No.</label><br />
    <%= text_field 'slip', 'number', :autocomplete=>'off'  %>
    <%= error_messages_on 'slip', 'number' %>
  </p>

  <p><label for="slip_executed_on">実行日</label><br />
    <%= text_field 'slip', 'executed_on', :autocomplete=>'off'  %>
    <%= error_messages_on 'slip', 'executed_on' %>
  </p>

  <p><label for="slip_total_yen">合計金額</label><br />
    <%= yen_field 'slip', 'total_yen'  %>
    <%= error_messages_on 'slip', 'total_yen' %>
  </p>
...(中略)...
  <%= submit_tag %>
<%= end_form_tag %>