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