form_forブロックの中のrender :partialの悩みを解決

この悩みとは、次のようなことではないかと思います。

  • newとeditは似たページであるので、似た部分を一つの記述ですませたい
  • 基本的には共通部分をパーシャルテンプレートにすればよい
  • ボタンやテキストフィールド等の要素の配置は共通だが、要素の生成方法が微妙に違う
  • 生成方法の違いを、テンプレート間で受け渡す記述が必要になるが、:localsによる受け渡しがもう一つしっくり来ない

Amrita2を使えば、この問題は解決します。

実際のアプリケーションでは、これと同じパターンでもう少し複雑な問題になると思うのですが、それに柔軟に対応しつつDRYに書けると思います。

というのは、上の例ではnewとeditの違いがボタン一つだったので、受け渡すパラメータも一つだけで済みます。パーシャルテンプレートが複数のページから使用されて、それぞれに微妙に違う要求があったような場合に、Amrita2は有効です。

Amrita2のsample/depotのadminコントローラから引用します。

(view/admin/new.html.a2)

<h1>New product</h1>

  <%
    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:create }
           ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Create")
    end
  %>

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

(view/admin/edit.html.a2)

<h1>Editing product</h1>

  <%
    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end
  %>

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

(view/admin/_form.html.a2)

%= error_messages_for 'product'

<< :form <
  <<p<
    <label for="product_title">Title</label><br/>
    <<:title>>

  <<p<
    <label for="product_description">Description</label><br/>
    <<:description>>

  <<p<
    <label for="product_image_url">Image url</label><br/>
    <<:image_url>>

  <<p<
    <label for="product_price">Price</label><br/>
    <<:price>>

  <<:submit>>

まずパーシャルテンプレートの _form.html.a2 から説明すると、ここでは、title description 等の要素をどういう形で、どこに配置するかだけ指定しています。個々の要素の生成は一切行なっていません。

それを生成しているのが、newとeditの中にある次の部分です。

    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.text_field :title
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end

amrita_define_formは、form_forのラッパーで、同じパラメータを同じ順番で受け取ります。そして、こちらでは、逆に「どこに配置するか(WHERE)」ということは後回しにして、「何を配置するか(WHAT)」についての記述だけを行います。

fのメソッドも、form_forの時とほぼ同じですが、

  • add_field_elementというメソッドが追加されている
  • それ以外は、FormHelperと同じだが、一つ目のパラメータがフィールドのIDとなり、後程、そのIDに対応した場所に配置される

ということになります。

例えば、修正時には、titleが変更できなくて、テキストフィールドの代わりに単なる文字列で表示するとしたら、次のようにします。

    form = amrita_define_form(
             :product,
             @product,
             :url=>{:action=>:update, :id=>@product}
             ) do |f|
      f.add_field_element :title, @product.title # ← ココ
      f.text_area :description
      f.text_field :image_url
      f.text_field :price
      f.add_field_element :submit, submit_tag("Update")
    end

こうすると、テキストフィールドが表示されていた位置に、その内容の文字列がそのまま表示されます。

それで、親子で受け渡しされる変数は常に一個で同じ記述ですみます。

  << :form | AcceptData[:hook] <
    %= render :partial => 'form', :object=>$_

$_ は、Amrita2のコンテキストバリューと言って、その時点で評価に使われている値です。この場合は、フィールドと要素の対応を示すハッシュになっています。:object を使っているので、このハッシュがパーシャルテンプレートに(テンプレート名と同じ名前の変数として)渡されます。

ここは、Amrita2の中でも最もトリッキーな所なので、かえってわかりにくいと感じるかもしれませんが、

  • ベーステンプレートでWHATを指定する
  • パーシャルテンプレートでWHEREを指定する

という機能の分離はきれいにできていると思います。WHATの部分(amrita_define_form)はヘルパーメソッドにすることもできるので、

  • 一つのWHATを複数のWHEREで共有する
  • 一つのWHEREを複数のWHATで共有する

という両方のDRYが実現できます。