form_forでフォームを書く

久しぶりにRailsを触るぜ!

いやー, とある時期にRailsは触っていたけども, 最近めっきり触っていないので, Railsの進化にとまどっている次第です. なんせ触っていたのはバージョン1.2.3のだしね. 今はもう2.2.2…… とまどうのは仕方ないけど, ちょいと使ってみたかったので, リハビリも兼ねて追いかけるコトに. そんでココにメモっとく.

ひとまず本は使わず(というか買わず), ソースとWebでやっていこうと思う. まずは早速つまずいたフォームの書き方.

form_forヘルパー

Rails1.*のときはstart_form_tag, end_form_tag(form_tag)を使っていたけど, Rails2.0からform_forというヘルパーを使うようになったらしい. それに合わせて, text_fieldなどの記述方法も変更. 以下のようになる.

<% form_for :human, :url => {:action => 'create'} do |f| -%>
<p>
  <label for="human_name">Name</label>:
  <%= f.text_field 'name' %>
</p>
<p>
  <label for="human_age">Age</label>:
  <%= f.text_field 'age' %>
</p>
<%= f.submit 'Save' %>
<% end -%>

form_forによって(X)HTMLの階層とRubyのブロックによるインデントがある程度一致してわかりやすい感じ. 他にもfields_forなどがある.

あと以前は上記の場合必ず@human変数にモデルのインスタンスを入れておく必要があったけど, form_forでは第2引数にモデルのインスタンスを与えればいい(その場合オプションは第3引数)ので, 必ずしも@humanという名前の変数である必要はないし, もっと言えば上記のように省略できる = newのときにモデルのインスタンスを生成する必要がない.

ちなみにform_forに渡しているブロックのパラメータであるfにはFormBuilderというクラスのインスタンスが渡される模様. というコトで次はFormBuilder.

FormBuilder

コレは素敵. 惚れた. コレを使うとf.text_fieldなどの挙動を変えらるとか.

仕組みは単純で, まず下記のようにFormBuilderのサブクラスを作る.

# app/helpers/extended_form_builder.rb
class ExtendedFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(field, *args)
    (args[0] && args[0][:label] ? "#{label(field, args[0].delete(:label))}: " : '') + super
  end
end

使うときは下記のようにして指定すればOK.

<% form_for :human, :url => {:action => 'create'}, :builder => ExtendedFormBuilder do |f| -%>
<%= f.text_field 'name', :label => 'Name' %>
<% end -%>

こんな風に出ます(text_fieldのところだけ抜粋).

<label for="human_name">Name</label>: <input type="text" size="30" name="human[namae]" id="human_name"/>

ってな具合にフィールド周りのタグをFormBuilderの方に固めてしまうコトができるのかー.

実際には下記のように使ってみました(google:非同期アップロードなんてそう頻繁に使うのか, と言われればアレですが).

module ApplicationHelper
  def async_upload_form_for(record_or_name_or_array, *args, &proc)
    args << {} unless args[-1] && args[-1].class == Hash

    html_options = {
      :target    => AsyncUploadFormBuilder.target(record_or_name_or_array),
      :multipart => true
    }

    options = args.pop
    options[:html] = (options[:html] || {}).update html_options
    options[:builder] = AsyncUploadFormBuilder
    form_for record_or_name_or_array, *(args << options), &proc
  end
end
class AsyncUploadFormBuilder < ActionView::Helpers::FormBuilder
  def self.target(form_name)
    "#{form_name}_async_upload"
  end

  def file_field(field, *args)
    target = self.class.target(@object_name)

    options = {
      :id    => target,
      :name  => target,
      :style => 'width: 0; height: 0; position: absolute; left: -9999px;'
    }

    @template.content_tag(:iframe, '', options) + super
  end
end

enctype属性やtarget属性を勝手に補完して, file_fieldを呼び出したときにiframeタグも出力する, ってな感じです. 後はviewに下記のような感じで書くだけ.

<% async_upload_form_for :human, :url => {:action => 'upload'} do |f| -%>
<%= f.file_field 'file' %>
<%= f.submit 'Upload' %>
<% end -%>

すっきり!