ラベル付きエラー表示付きフォームの雛形が欲しい!
前回までに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}")
- 改善したいこと
- 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 %>