独自のValidationを実装する(1.2.6)

RailsでのValidationはModelに対して実装する(その賛否については記事にしたい)が、標準のValidatorというかvalidates_*ヘルパーメソッドがいくつか用意されている。しかし、意外にアプリを作っているとかゆいところに手が届かないことが多いのだ。

ということで、独自にValidationを実装する方法を整理しておこう。独自のValidationはModelにvalidateメソッドをオーバライドする形で実装すればよい。

もし、Validationに失敗した場合、つまり有効な入力で無かった場合は、ユーザにその旨を通知しなければならない。そのメッセージはerrors.addメソッドを利用して設定すればよい。

class OneModel < ActiveRecord::Base
  # 独自のValidationを実装するポイント
  def validate
    # 指定された時刻の範囲がおかしいとき
    unless start_time < end_time
      errors.add(:end_time, "は、開始時刻よりも未来を設定してください。")
    end
  end
end

このように、独自のValidationは、validateメソッドに自由に実装することができるので、標準のvalidats_*ヘルパーメソッドにない検証ロジックは、このメソッドにどんどん実装することになる。もちろん、独自にvalidates_*ヘルパーメソッドを追加する方法がベストなのは言うまでもない。

# ヘルパーメソッドの追加方法を書く予定

基本的には、これで終わり。もし、scaffold.cssを使ってればValidationに失敗した項目が赤くハイライトしてくれる。どうもActionView::Baseが"fieldWithErrors"というclassを指定してdivタグで失敗した項目を囲んでくれるため、cssで.fieldWithErrorsの定義を変更すれば自由にハイライトの色を変えたりすることができる。

ただ、これはActiveRecordの1カラム=1タグ(タグというか、Railsの1Viewコンポーネントという感じ)で定義されたものしか自動的にdivタグで囲むようなことはしてくれない。例えば、時刻を表すend_timeというカラムに対して、自前でselectボックスを時間、分で2つセットにした場合、end_timeに対してValidationが失敗しても上手くハイライトしてくれない。

<%= select :end_time, :hour, ("00".."23").collect {|h| [h, h]} %>
 :
<%= select :end_time, :minute, ("00".."59").collect {|m| [m, m]} %>

なので、Validationに失敗したときに、上手くハイライトさせる一番アドホックな方法がは次のようなものだろう。

<% "<div class='fieldWithErrors'>" if @oneModel.errors.invalid(:end_time) %>
<%= select :end_time, :hour, ("00".."23").collect {|h| [h, h]} %>
 :
<%= select :end_time, :minute, ("00".."59").collect {|m| [m, m]} %>
<% "<div>" if @oneModel.errors.invalid(:end_time) %>

ただ、これではさすがになんなので、ヘルパーメソッドに切り出してみると次のようになる。

module ApplicationHelper
  def field_with_errors_if(invalid, &block)
    concat("<div class='fieldWithErrors'>", block.binding) if invalid
    block.call
    concat("</div>", block.binding) if invalid
  end
end

ブロック引数を取るhelperメソッドの書き方 - Hello, world! - s21gを参考にさせていただいた。

そして、Viewは次のように修正する。

<% field_with_errors_if @oneModel.errors.invalid?(:end_time) do %>
<%= select :end_time, :hour, ("00".."23").collect {|h| [h, h]} %>
 :
<%= select :end_time, :minute, ("00".."59").collect {|m| [m, m]} %>
<% end %>

ちょっとはきれいになっただろうか。