Rails2.1のrender :partialとform_forで可能な限り重複を排除していくと...

Rails2.0の頃から出来たようなのだが、つい最近まで自分は知らなかった...ということがたくさんある。ずいぶん損をしていた気がする。断片的な知識が増えてきたので、サンプルコードを作りながらの自分用のメモ。サンプルコードは前回に引き続きQandAプロジェクト。

リスト表示する時のforループは「render :partial=>...」で置き換えることができる

  • 「app/views/answers/index.html.erb」のforループ内を、partialファイル「_answer.html.erb」として抜き出しておいた。
<%# ---------- app/views/answers/_answer.html.erb ---------- %>
  <tr>
    <td><%=h answer.name %></td>
    <td><%=h answer.body %></td>
    <td><%=h answer.question_id %></td>
    <td><%= link_to 'Show', [@question, answer] %></td>
    <td><%= link_to 'Edit', edit_question_answer_path(@question, answer) %></td>
    <td><%= link_to 'Destroy', [@question, answer], :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
  • そうすると、<% for answer in @answers %>〜<% end %>ループは...
    • シンプルな1行<%= render :partial=>@answers %>に置き換えることができる。
    • 今までは<%= render :partial=>'answer', :collection=>@answers %>と書いていた。
<%# ---------- app/views/answers/index.html.erb ---------- %>
<h1>Listing answers</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Body</th>
    <th>Question</th>
  </tr>

  <%= render :partial=>@answers %>
</table>
...(中略)...


上記のようにしておくことで、以下のような場合に幸せを感じる。

行番号を表示したい
  • No.列を一つ増やして、<%= answer_counter %>を追記するだけでOK。answer_counterは1から順に増えていく。
  • もし、forループのまま自分でカウンターを用意するとなると、意外と面倒な気がする。
<%# ---------- app/views/answers/index.html.erb ---------- %>
<h1>Listing answers</h1>

<table>
  <tr>
    <th>No.</th>
    <th>Name</th>
    <th>Body</th>
    <th>Question</th>
  </tr>

  <%= render :partial=>@answers %>
</table>
...(中略)...
<%# ---------- app/views/answers/_answer.html.erb ---------- %>
  <tr>
    <td><%= answer_counter %></td>
    <td><%=h answer.name %></td>
    <td><%=h answer.body %></td>
    <td><%=h answer.question_id %></td>
    <td><%= link_to 'Show', [@question, answer] %></td>
    <td><%= link_to 'Edit', edit_question_answer_path(@question, answer) %></td>
    <td><%= link_to 'Destroy', [@question, answer], :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
関連先の情報も表示したい

関連するモデル同士の場合、それぞれのビューで関連先の情報も表示したいことはよくある。例えば、質問リストを表示する時に、その下に答えのリストも表示しておくとか。以下のようにやってみた。

  • _answer.html.erbをもう少し一般的に使えるようにするため、インスタンス変数@answerをローカル変数answerに置き換えた。
      • この変更によって上記までの<%= render :partial=>@answers %>は、<%= render :partial=>@answers, :locals=>{:question=>@question} %>とする必要がある。
<%# ---------- app/views/answers/_answer.html.erb ---------- %>
  <tr>
    <td><%=h answer.name %></td>
    <td><%=h answer.body %></td>
    <td><%=h answer.question_id %></td>
    <td><%= link_to 'Show', [question, answer] %></td>
    <td><%= link_to 'Edit', edit_question_answer_path(question, answer) %></td>
    <td><%= link_to 'Destroy', [question, answer], :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
  • そうすれば、<%= render :partial=>question.answers, :locals=>{:question=>question} %>を追記するだけでOK。
  • 上記は、<%= render :partial=>'answers/answer', :collection=>question.answers, :locals=>{:question=>question} %>と同等。
<%# ---------- app/views/questions/index.html.erb ---------- %>
<h1>Listing questions</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Body</th>
  </tr>

<% for question in @questions %>
  <tr>
    <td><%=h question.name %></td>
    <td><%=h question.body %></td>
    <td><%= link_to 'Answers >>', question_answers_path(question) %></td>
    <td><%= link_to 'Show', question %></td>
    <td><%= link_to 'Edit', edit_question_path(question) %></td>
    <td><%= link_to 'Destroy', question, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
  
  <%= render :partial=>question.answers, :locals=>{:question=>question} %>
<% end %>
</table>
...(中略)...
所感

今まで、関連する両方の情報を表示する時、partialファイル名の命名とか、置き場所(ディレクトリ)とか、利用するコントローラーはquestionsにするか、answersするかでよく悩んでいた。それがRails2.0の頃からルート設定とか、render :partialから自然と導かれるようになり、お手本にもなるし、ずいぶん楽にもなった。Railsの流儀に従う限り、当り前のことはデフォルトとしてどんどん省略できるようになってきた。

form_forの重複もpartialファイルで抜き出す

  • 以前はscaffoldで、newとeditの入力フォームが_form.rhtmlに抜き出されていた。ところが、Rails2.0からはpartialファイルが利用されなくなってしまった。なぜだろう?
  • 疑問を感じつつ、以前のように_form_html_erbを抜き出してみると以下のようになった。
<%# ---------- app/views/answers/_form.html.erb ---------- %>
  <%= form.error_messages %>

  <p>
    <%= form.label :name %><br />
    <%= form.text_field :name %>
  </p>
  <p>
    <%= form.label :body %><br />
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.label :question_id %><br />
    <%= form.text_field :question_id %>
  </p>
  • なぜ<%= render :partial=>f %>で_form.html.erbが呼び出されるのだ?という疑問を自然と感じた。調べてみると...
    • form_forのブロック変数fには、FormBuilderクラスのインスタンスが代入されている。
    • クラス名がFormBuilderなので、Builderを取り除いたformに「_」と「.html.erb」を付加して「_form.html.erb」が呼び出される仕組みのようだ。
    • 呼び出された_form.html.erbのローカル変数「form」には、form_forのブロック変数fの内容が代入されている。
<%# ---------- app/views/answers/new.html.erb ---------- %>
<h1>New answer</h1>

<% form_for([@question, @answer]) do |f| %>
  <%= render :partial=>f %>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', question_answers_path %>
<%# ---------- app/views/answers/edit.html.erb ---------- %>
<h1>Editing answer</h1>

<% form_for([@question, @answer]) do |f| %>
  <%= render :partial=>f %>
  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', [@question, @answer] %> |
<%= link_to 'Back', question_answers_path %>
  • ちなみに、form_forのオプションで:builder=>LabeledFormBuilderとしておけば...
    • 「_labeled_form.html.erb」が呼び出されるはず。
    • ローカル変数「labeled_form」が利用できるはず。

独自のLabeledFormBuilderを定義する

LabeledFormBuilderという以下のような書式のFormBuilderを作ってみた。(さらに詳しく>>form_forの使い方

  • label付きで、pタグで囲む。
  • form_forまたはfields_forのオプションをブロック内のtext_fieldのオプションとマージする。
# ---------- app/helpers/labeled_form_builder.rb ----------
class LabeledFormBuilder < BaseFormBuilder
  #  <p>
  #    <%= f.label 'number' %><br />
  #    <%= f.text_field 'number'  %>
  #  </p>
  (form_helpers - %w(label form_for field_for hidden_field error_messages_on)).each do |selector|
    define_method(selector) do |field, *args|
      @template.content_tag('p', 
        @template.label(@object_name, field, nil, :style=>"display:table") + #'<br/>' +
        super(field, *merge_form_options_with(args)))
    end
  end

  # hidden_fieldだけ特例扱い、以下理由
  #   見えないフィールドにpタグは設定したくない
  #   form_for等で設定した独自オプション(:index等)だけは有効にしたい
  def hidden_field(field, *args)
    super(field, *merge_form_options_with(args))
  end

  # form_forのoptionと無関係の場合
  def submit(value = "Save changes", options = {})
    @template.content_tag('p', super)
  end

  # form_forのoptionと共用する場合 
  #def submit(value = "Save changes", *args)
  #  @template.content_tag('p', super(value, *merge_form_options_with(args)))
  #end
end
  • 通化しそうなメソッドはBaseFormBuilderに抜き出して、LabeledFormBuilderはBaseFormBuilderを継承するようにした。
# ---------- app/helpers/base_form_builder.rb ----------
class BaseFormBuilder < ActionView::Helpers::FormBuilder
  class_inheritable_accessor :form_helpers
  self.form_helpers = ActionView::Helpers::FormHelper.instance_methods + 
                        ActionView::Helpers::FormOptionsHelper.instance_methods

private

  # 以下のオプションをマージする
  #   args ....... f.text_field等のオプション
  #   @options ... form_for,fields_forのオプション(:url, :html, :builderは除く)
  def merge_form_options_with(args)
    #args_hash = args.last.is_a?(Hash) ? args.pop : {}
    args_hash = args.extract_options!
    args << args_hash.merge(form_options)
  end

  # フォームに設定する独自オプションだけ取り出す
  def form_options
    _options = @options.dup
    [:url, :html, :builder].each {|key| _options.delete(key)}
    _options
  end
end
  • 独自定義した上記LabeledFormBuilderは、form_forのオプションで:builder=>LabeledFormBuilderと指定して利用できる。
  • さらに、labeled_form_forヘルパーとして定義しておけば、form_for自体をコントロールしたり、タグでラッピングできるという利点もある。
  • 以下のLabeledFormBuilderの例では、単にクラス名のdivタグで囲うだけだが、もしテーブルベースの入力フォームの時などは利用する価値が高い。(ヘルパに登録しておいて良かったと感じた。)
# ---------- app/helpers/application_helper.rb ----------
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
  def labeled_form_for(record_or_name_or_array, *args, &proc)
    options = args.extract_options!
    args << options.merge(:builder=>LabeledFormBuilder)
    
    concat('<div class="labeled_form">', proc.binding)
    form_for(record_or_name_or_array, *args, &proc)
    concat('</div>', proc.binding)
  end

LabeledFormBuilderを利用すると...

  • pタグやlabelが視界から消え、シンプルさに磨きがかかる。
    • 同じ書式の入力フォームが順に繰り返される場合はとても便利そう。
    • 複雑な書式の入力フォームで書式のルールが一定でない場合は、いつものform_forの方が全体を見通せて良い。
      • その場合は、form_forメソッドに置き換えるだけで、すぐに戻すことができるので気楽だ。
<%# ---------- app/views/answers/_labeled_form.html.erb ---------- %>
  <%= labeled_form.error_messages %>

  <%= labeled_form.text_field :name %>
  <%= labeled_form.text_area :body %>
  <%= labeled_form.text_field :question_id %>
<%# ---------- app/views/answers/new.html.erb ---------- %>
<h1>New answer</h1>

<% labeled_form_for([@question, @answer]) do |f| %>
  <%= render :partial=>f %>
  <%= f.submit "Create" %>
<% end %>

<%= link_to 'Back', question_answers_path %>
<%# ---------- app/views/answers/edit.html.erb ---------- %>
<h1>Editing answer</h1>

<% labeled_form_for([@question, @answer]) do |f| %>
  <%= render :partial=>f %>
  <%= f.submit "Update" %>
<% end %>

<%= link_to 'Show', [@question, @answer] %> |
<%= link_to 'Back', question_answers_path %>
  • さらに、showの書式にこだわらなければ、開発初期は以下のようにnewやeditと共用してしまっても良いのではないかと...
<%# ---------- app/views/answers/show.html.erb ---------- %>
<h1>Show answer</h1>

<% labeled_form_for([@question, @answer], :disabled=>true) do |f| %>
  <%= render :partial=>f %>
<% end %>

<%= link_to 'Edit', edit_question_answer_path(@question, @answer) %> |
<%= link_to 'Back', question_answers_path %>
  • FormBuilderのクラス名でスタイルシートも定義しておけば、デザインも調整可能だ。
/*--------- public/stylesheets/scaffold.css ----------*/
...(中略)...
.labeled_form label {
  width: 10em;
  float: left;
  text-align: right;
  margin-right: 0.5em;
  display: block;
}

デザインされたフォーム

以下のリンクでは素晴らしいデザインのフォームが紹介されている。そういう意味では、FormBuilderを利用するということは、重複の排除というよりは、統一されたデザインセットを簡単に切り替えられる利点の方が重要かもしれない。