simple_formとTwitter bootstrapで作る俺流鉄板Railsアプリ(その2)


前回に引き続き、Railsアプリにて simple_formと twitter bootstrapを使用する際のtipsをご紹介したいと思います。まずは前回作成した画面を見てましょう。

New Book

前回はTitleに必須バリデーションを付加しました。今回はReview項目を編集してみたいと思います。

このReviewという項目には、本の評価を1〜5の数字で入力してもらうと仮定しましょう。さっそく、models/book.rbにバリデーションを記述します。

📄app/models/book.rb
class Book < ActiveRecord::Base
  validates :title, presence: true

  validates :review, numericality: {
                      only_integer: true,
                      greater_than_or_equal_to: 1,
                      less_than_or_equal_to: 5 }
end

バリデーションとしてはこれでOKなのですが、フォーム画面はどうでしょうか。1〜5の数値しか登録できないのなら、他の値は入力できない方が良いですね。また、その仕様が不明記ではユーザーを混乱させてしまう可能性があります。これはヒントとして表示することにします。

app/views/books/_form.html.erbを編集します。

📄app/views/books/_form.html.erb
<%= simple_form_for @book, :html => { :class => 'form-horizontal' } do |f| %>
  <%= f.input :title %>
  <%= f.input :author %>
  <%= f.input :review,
                hint: "※1~5の数値を入力",
                input_html: { min: 1, max: 5, class: "review" } %>
  <%= f.input :description %>
  <div class="form-actions">
    <%= f.button :submit, :class => 'btn-primary' %>
    <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                books_path, :class => 'btn' %>
  </div>
<% end %>

simple_formでは『hint』というオプションで、ヒント文を表示することができます。また、HTMLの属性を記述する場合は、『input_html』というオプションの中にハッシュで記述します。ここでは min, max, classを指定しています。『review』CSSクラスは・・・たいした定義ではありませんが、LESSの勉強も兼ねて一応見ておきましょう。

📄books.css.less
input {
  &.review {
    width: 30px;
    text-align: right;
  }
}

LESSは階層的に定義を指定できるのが便利ですね。

編集が完了したところで、実際に画面を表示させて確認してみましょう。

review

グレート! ちゃんと1〜5の数値しか入力できなくなっています。ヒント文も表示されていて分かりやすいですね。ふぅーっ、ひと仕事終えた気分です。

しかし、もしあなたが善良で怠惰なプログラマなら、ここで少し立ち止まってみましょう。このReviewインプットは他の画面でも使用されることになっていたらどうでしょう? 他の画面のerbファイルにも同じコードを書かなければなりません。もちろん、これは喜ばしいことではありません。

では、どうすべきなのでしょうか? 真っ先に思いつくのは、当該コードをヘルパーに追いやることです。そうすれば、複数のViewから共有できるという目的は果たせそうです。しかし、railsのヘルパーはあまりオブジェクト指向的ではありません。幸い simple_formにはそれを解決する方法が用意されています。

カスタムインプットを作成する

表題の通り、simple_formでは独自のカスタムインプットを定義することができるのです。Reviewインプットのために独自のカスタムインプットを作成してみましょう。まずは appの直下に inputsというディレクトリを作成します。この中にカスタムインプットに配置すれば、simple_formが勝手に読み込んでくれるようになっています。続いて、その真新しい inputsディレクトリの下に 『review_input.rb』というファイルを作成します。

📄review_input.rb
class ReviewInput < SimpleForm::Inputs::NumericInput
  def input
    input_html_classes.push('review')
    input_html_options.deep_merge!({ min: 1, max: 5 })
    options.merge!(hint: "※1~5の数値を入力")
    super
  end
end

『ReviewInput』というSimpleForm::Inputs::NumericInputのサブクラスを定義しています。クラス名とファイル名は重要です。クラス名は『XxxInput』、ファイル名は『xxx_input.rb』にする必要があります。

コード自体は単純です。inputメソッドをオーバーライドして、必要なオプション値を差し込んでいるだけです。(詳しくはsimple_formのドキュメントを参照してください)

Reviewインプットが完成したところで、これを呼び出すView側のコードも変更します。

📄_form.html.erb
<%= simple_form_for @book, :html => { :class => 'form-horizontal' } do |f| %>
  <%= f.input :title %>
  <%= f.input :author %>
  <%= f.input :review, as: :review %>
  <%= f.input :description %>
  <div class="form-actions">
    <%= f.button :submit, :class => 'btn-primary' %>
    <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                books_path, :class => 'btn' %>
  </div>
<% end %>

4行目の記述がポイントです。as: :review というオプションでReviewインプットを指定しています。このように非常にスッキリと記述することができるようになるのがカスタムインプットの良い所です。修正が完了したら、画面での動作も確認しておきましょう。

完成画面

相変わらずグレートォ! 先程と同様、上手く動作しています。嬉しくて、全ての画面にReviewインプットを配置してしまいそうですよ!

simple_formの設定を変更する

完璧に仕上がった・・・と思いたいのですが、まだ『臭う』箇所があります。それはReviewInputクラス内で min, max の値を直接指定していることです。この値は、Bookモデルのバリデーション値と等しくなるはずなので、万が一バリデーションの値が変更された場合、このReviewInputクラスも変更しなければなりません。つまりDRYではありません。

この問題に対する解決策もsimple_formには用意されています。設定ファイルを変更することで、自動的にバリデーションのmin/max値をinputに反映することができるのです。設定ファイルを開きましょう。config/initializers/simple_form_bootstrap.rbというファイルです。

📄simple_form_bootstrap.rb
# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
  config.wrappers :bootstrap, tag: 'div', class: 'control-group', error_class: 'error' do |b|
    b.use :html5
    b.use :placeholder
    b.use :label
    b.use :min_max  # この行を追加
    b.wrapper tag: 'div', class: 'controls' do |ba|
      ba.use :input
      ba.use :error, wrap_with: { tag: 'span', class: 'help-inline' }
      ba.use :hint,  wrap_with: { tag: 'p', class: 'help-block' }
    end
  end
...
...
  # Wrappers for forms and inputs using the Twitter Bootstrap toolkit.
  # Check the Bootstrap docs (http://twitter.github.com/bootstrap)
  # to learn about the different styles for forms and inputs,
  # buttons and other elements.
  config.default_wrapper = :bootstrap
end

b.use :min_max という記述を追加しました。これでバリデーションで指定している最小値・最大値が自動的にinputタグに反映されるようになります。

続いて、ReviewInputクラスも変更します。

📄review_input.rb
class ReviewInput < SimpleForm::Inputs::NumericInput
  def input
    input_html_classes.push('review')
    min, max = %i(min max).collect {|sym| input_html_options[sym] }
    options.merge!(hint: "※#{min}~#{max}の数値を入力")
    super
  end
end

inputタグの属性min, maxの値は、simple_formが自動的にバリデーションから取得して設定してくれます。ここでは、input_html_optionsからその値を取得して、ヒント文に使用しています。これでDRYなコードになりました。

試しに、Bookモデルのバリデーションを変更してみましょう。1〜3の数値しか受け付けいように変更します。(簡単だと思うのでコードは割愛)

全ての変更が完了したら、再び画面を開いて確認してみましょう。感動の瞬間が待っているはず・・・!

最後のフォーム

2度あるグレートは3度ともグレートォ!! というわけで、最高の気分です! 誰も僕を止めることはできない。

プロジェクトの当初からカスタムインプットを適切に作り続けていれば、View周りの開発効率は二次曲線的にアップしていくはずです。simple_formではそれが容易に実現できるので、使ったことがない方は、導入を検討してみてはいかがでしょうか?

次回は twitter bootstrapのJavascriptを絡めたtipsなんかを紹介したいと思います。

関連する記事