form_forの使い方
Railsのバージョンが最新版は2.0.2となっている。しかし、自分の頭の中は依然、1.1.6の状態だ...。勉強のスピードよりRailsの進化の方が早い。1.2.6までは、1.1.6の機能でも利用できたが、Rails2.0以降はガラリと進化し、1.1.6までの書き方では通用しなくなっている部分もある。その一つが、start_form_tag、end_form_tagの廃止。その代わりにform_for do 〜 endブロック*1を利用することになっている。form_forは1.1.6の頃から存在するメソッドだが、専らシンプルなstart_form_tagばかり使っていた。(form_forはマニュアルを見ても複雑そうで、分かり難い印象があったので...。)そろそろform_forについて調べておかないと...。
基本形
今までは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 %>
上記をform_forを使って書くと...
<% form_for :slip, :url=>{:action => 'create'} do |f| %> <p><label for="slip_number">伝票No.</label><br /> <%= f.text_field 'number', :autocomplete=>'off' %> <%= error_messages_on 'slip', 'number' %> </p> <p><label for="slip_executed_on">実行日</label><br /> <%= f.text_field 'executed_on', :autocomplete=>'off' %> <%= 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 %>
- form_forはRubyのブロックを引数にとるので、<% form_for ... %>(=無し<%)と書く。<%= form_for ... %>(=あり<%=)ではエラーになる。
- 同様に、フォームの終了は<%= end_form_tag %>から、ブロックの終了を示す<% end %>になる。
- ブロック変数fに対するフォームヘルパメソッドとすることで、'slip'を省略することができる。
-
-
- ちなみにフォームヘルパの引数は、文字列、シンボルどちらで指定してもOK。(例: text_field 'slip', 'number' または text_field :slip, :number)
-
form_for対応のyen_fieldに
実は、form_forに変更して、いきなりエラーで悩んでしまった...。自分で追加したメソッドyen_fieldが、form_for対応になっていないようで、f.yen_fieldのところでエラーが発生していたのだ。試行錯誤の結果、以下のように書き直すことで、form_for対応のyen_fieldになった。(yen_fieldに関するコードはmodule ActionView以下。)
- RailsソースコードのFormHelperと同じネームスペースで書き直すことにした。
- f.yen_fieldに対応するためには、FormBuilderクラスにyen_fieldメソッドを追加しておく必要があるようだ。
# ヘルパー: app/helpers/application_helper.rb module ApplicationHelper def error_messages_on(object, method, prepend_text = "", append_text = "", css_class = "formError") if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && (errors = obj.errors.on(method)) errors_list = errors.map {|error| "#{prepend_text}#{error}#{append_text}<br />"}.join #+ "<br />" content_tag("span", errors_list, :class => css_class) else '' end end end module ActionView module Helpers module FormHelper def yen_field(object_name, method, options = {}) # object_nameに基づくオブジェクト(モデルのインスタンス)から、methodが示すフィールドの値を取得している。 # 例: yen_field 'slip', 'total_yen' --> @slip.total_yenがvalueに設定される。 object = self.instance_variable_get("@#{object_name}") value = object.send(method) # デフォルトのオプション設定 options.merge!(:value=>number_with_delimiter(value), :autocomplete=>'off', :style=>"text-align:right") InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) end end class FormBuilder def yen_field(method, options = {}) @template.yen_field(@object_name, method, options.merge(:object => @object)) end end end end
何が嬉しいのか?
基本形を見ただけでは、<%= start_form_tag %><%= end_form_tag %>が、<% form_tag %><% end %>ブロックに変わっただけ。'slip'は省略できるようになったが、たったそれだけのためにform_for?(この後、調べれば調べるほど、form_forの持つ拡張性に感動する...。)
自由なインスタン変数名
- オブジェクトを代入しておくインスタンス変数名は、<%= text_field 'slip', 'number' %>であれば必ず@slipにSlipモデルのインスタンスが代入されている必要があった。
- form_forを以下のように利用すると、インスタンス変数名は自由に決めることができる。(この例では@main_slipを利用している。)
<% form_for :slip, @main_slip, :url=>{:action => 'create'} do |f| %> <%= f.text_field 'number' %> <% end %>
htmlオプション
- :htmlオプションとして、html属性を指定しておけば、フォームヘルパーの同じオプションは省略できる。以下の例では、重複していたtext_fieldの:autocomplete=>'off'が不要になった!
<% 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 %>
builderオプション
:builderオプションは強力だ!:builderオプションとは、f.text_field 'number'のようなコードを書いた時、生成するフォームの雛形を指定するオプションらしい。省略している場合はデフォルトの雛形になり、通常シンプルなinputダグを生成してくれる。デフォルトの雛形は、Rails2.0.2のソースコードのf.text_fieldに関する部分を抜粋すると以下のようになっている。
require 'cgi' require File.dirname(__FILE__) + '/date_helper' require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers ...(中略)... class FormBuilder #:nodoc: # The methods which wrap a form helper call. class_inheritable_accessor :field_helpers self.field_helpers = (FormHelper.instance_methods - ['form_for']) attr_accessor :object_name, :object, :options def initialize(object_name, object, template, options, proc) @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc end (field_helpers - %w(label check_box radio_button fields_for)).each do |selector| src = <<-end_src def #{selector}(method, options = {}) @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) end end_src class_eval src, __FILE__, __LINE__ end ...(中略)...
雛形そのものは、上記の下から9行目(field_helpers - %w(label check_box radio_button fields_for)).each do |selector| 〜 endブロックで定義されている。:builderオプションで指定するのはこのクラス名になる。デフォルトでは上記FormBuilderクラスが設定されている。
そして、FormBuilderクラスを継承した独自の雛形を○○FormBuilderクラスとして定義しておき、それをform_forの:builderオプションで指定しておけば、独自の雛形を利用したフォームが生成されることになる。言葉で説明されてもピンとこないので、百聞は一見にしかず、実際に以下のようなコードで試してみる。
- app/helpers/test_form_builder.rbファイルを新しく追加して、以下のように編集してみた。
- 基本的にソースコード丸写しで、def #{selector}(field, *args) 〜 end間に雛形を生成するコードを書いた。(この例ではフォームを<p>タグで囲む。)
- content_tagはヘルパメソッドで通常ビュー環境でしか利用できないが、@template.content_tag('p', super)のように、@templateのメソッドとすることで、ここでも利用できるようになる。
- superは、親クラスのインスタンスメソッドを同じ引数で呼び出す。この例では、FormBuilderクラスのtext_field、text_area、yen_fieldメソッドを呼び出す。つまりシンプルなinputタグ、またはtext_areaタグが返ってくることになる。
- ヒアドキュメント<<-end_src 〜 end_src間のコードは、単なる文字列としてsrcに代入されて、その下のclass_evalによってTestFormBuilderクラスのメソッドとして動的に定義される。
# フォームビルダー: app/helpers/test_form_builder.rb class TestFormBuilder < ActionView::Helpers::FormBuilder %w(text_field text_area yen_field).each do |selector| src = <<-end_src def #{selector}(field, *args) @template.content_tag('p', super) end end_src class_eval src, __FILE__, __LINE__ end end
- 動的メソッド定義は頭が混乱してくるが、上記コードはつまり、以下のクラス定義と同等である。
# フォームビルダー: app/helpers/test_form_builder.rb class TestFormBuilder < ActionView::Helpers::FormBuilder def text_field(field, *args) @template.content_tag('p', super) end def text_area(field, *args) @template.content_tag('p', super) end def yen_field(field, *args) @template.content_tag('p', super) end end
- 以上の定義をして、:builderオプションにTestFormBuilderを指定しておくと...
<% form_for :slip, :url=>{:action => 'create'}, :html=>{:autocomplete=>'off'}, :builder=>TestFormBuilder do |f| %> <%= f.text_field 'number' %> <%= f.text_field 'executed_on' %> <%= f.yen_field 'total_yen' %> ...(中略)...
- 上記コードは、以下のhtmlソースを生成するようになる。(pタグで囲まれたフォームになる。)
- form_forで指定した:html=>{:autocomplete=>'off'}は、formタグのautocomplete="off"属性となることで、その中のinputタグすべてに有効な属性として機能しているようだ。(個々のinputタグに設定される訳ではない。)
- yen_fieldのautocomplete="off"、style="text-align:right"は、yen_fieldのデフォルト設定による。
<form action="/slips/create" autocomplete="off" method="post"> <p><input id="slip_number" name="slip[number]" size="30" type="text" /></p> <p><input id="slip_executed_on" name="slip[executed_on]" size="30" type="text" /></p> <p><input autocomplete="off" id="slip_total_yen" name="slip[total_yen]" size="30" style="text-align:right" type="text" /></p> ...(中略)...
以上で、pタグで囲まれたフォームを生成するようになった!FormBuilderクラスのすべてが理解できている訳ではないが、この仕組みを利用すれば、ビューでかなりシンプルなコーディングが出来そうだ。
*1:またはform_tag