改良中...心に優しいエラー表示を!

現状で、もしすべての行で検証エラーが発生したとすると、こんな感じになる...。

ずぶん派手に教えてくれる。何処がどんなエラーなのか明快と言えば明快なのだが...この表示、何度も見てると、なんだか気持ちを逆撫でされているようで、いつのまにか「言われなくても分かっとるわぃ!」とムキになってくるのだ。作っている本人がそうなのだから、ユーザーがこのエラーを見たら、おそらく、やる気が失せるだろう...。もう少し、心に優しいエラー表示が必要だ。

配色

赤過ぎると挑発的な感じになってしまう。もう少しトーンを抑えた色にしてみる。

  • エラー時の背景色をピンクに変更した。
  • paddingを0にした。
  • 控え目になるので、input、textarea、selectの背景色もピンクにしてみた。
/* スタイルシート: public/stylesheets/scaffold.css */
...(中略)...
  margin: 0;
  padding: 0;
  background-color: pink;
  display: table;
}
.fieldWithErrors * {
  background: pink;
}
...(中略)...

配置

エラーが発生すると、大した情報量でもないのに、スクロールしないとページを見渡せなくなってしまう。表示をコンパクトにまとめる必要がある。

  • エラーの看板リスト表示方式をやめて、個々のフィールドの直下にエラーメッセージを表示するようにしてみる。
  • error_message_onを利用して、伝票、明細行の_form.rhtmlを以下のように変更してみた。
  • add_to_baseを使ったフィールドを指定しない検証*1は、<%= error_message_on 'slip', 'base' %>で表示することができる。(<table class="journal">の直前に追加した。)
<%# ビュー: app/views/slips/_form.rhtml %>
<%#= error_messages_for 'slip' %>

<!--[form:slip]-->
  <p><label for="slip_number">伝票No.</label><br />
    <%= text_field 'slip', 'number', :autocomplete=>'off'  %>
    <%= error_message_on 'slip', 'number' %>
  </p>

  <p><label for="slip_executed_on">実行日</label><br />
    <%= text_field 'slip', 'executed_on', :autocomplete=>'off'  %>
    <%= error_message_on 'slip', 'executed_on' %>
  </p>

  <p><label for="slip_total_yen">合計金額</label><br />
    <%= yen_field 'slip', 'total_yen'  %>
    <%= error_message_on 'slip', 'total_yen' %>
  </p>

  <%= error_message_on 'slip', 'base' %>
  <table class="journal">
    <%= render :partial=>'journals/header' %>
    <%= render :partial=>'journals/form', :collection=>@journals %>
  </table>
<!--[eoform:slip]-->
<%# ビュー: app/views/slips/_form.rhtml %>
<% @journal = form %>
<% @journal_count += 1 rescue @journal_count = 1 %>

<!-- 削除
<tr>
  <td>
  </td>
  <td colspan="2">
    <%#= error_messages_for 'journal' %>
  </td>
</tr>
ここまで削除 -->

<!--[form:journal]-->
<tr valign="top">
  <th align="right">
    <%= @journal_count %>
    <%= hidden_field "journal", 'position', :index=>@journal.index, :value=>@journal_count  %>
  </th>
  <td align="_center">
    <%= text_field "journal", 'comment', :index=>@journal.index, :size=>40  %>
    <%= error_message_on 'journal', 'comment' %>
  </td>
  <td align="_center">
    <%= yen_field 'journal', 'yen', :index=>@journal.index  %>
    <%= error_message_on 'journal', 'yen' %>
  </td>
</tr>
<!--[eoform:journal]-->
/* スタイルシート: public/stylesheets/scaffold.css */
.formError {
  font-size: x-small;
  color: red;
  }


すると、以下のように、少しは心に優しい感じの表示になる。

問題

しかし、ここで問題発生!気になることは以下の3点。

  1. フィールドで複数のエラーが発生しているはずなのに、一つだけしかエラー表示されていない。
  2. エラーメッセージの主語が省略されてしまう。主語があることを前提としたメッセージなので「が合計金額と一致していません。」のようなおかしな表現になっている。
  3. エラーが発生すると、伝票No.・実行日・合計金額と、入力フィールドの間に不要なスペース行が入ってしまう。なぜ?
問題1: error_messages_onが必要!

Rails謹製のメソッドは以下の二つ。

error_messages_for
error_message_on

個々のフィールド直下にエラーメッセージを表示する為にerror_message_onを利用したが、どうやらこのメソッドは、指定したフィールドで発生しているエラーの最初の一つだけしか表示してくれない仕様のようだ。*2よくよく見ると、error_message(単数形)_onとなっており、メソッド名的には筋が通っているのか...。
それならここでは、フィールドごとにすべてのエラーを表示してくれるerror_messages_onが必要だ。error_message(単数形)_onのソースを参考にヘルパーに以下のように書いてみた。

# ヘルパー: app/helpers/application_helper.rb
module ApplicationHelper
...(中略)...
  def error_messages_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
    if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && (errors = obj.errors.on(method))
      errors_list = errors.map {|error| "#{prepend_text}#{error}#{append_text}<br />"}.join
      content_tag("span", errors_list, :class => css_class)
    else 
      ''
    end
  end
end

すべてのerror_message(単数形)_onを、新しく作ったヘルパメソッドerror_messages_onに変更して確認すると...エラーメッセージが複数表示されるようになった!

問題2: Ruby-GetTextを利用する

エラーメッセージの主語が省略されるのもerror_message_on(またはerror_messages_on)の仕様だが、オプションとしてprepend_textを設定できるようになっている。ここに主語にあたる文字列を設定しておけば良いのだ。が...個々のerror_messages_onでフィールド名を設定するのは面倒だ。デフォルトでerror_messages_forのように主語ありのエラーメッセージになれば良いのに...。
この問題はRuby-GetTextを利用することで解決する。既にインストールしてあれば、お決まりのコード2行と、オリジナルなvalidateのエラーメッセージには%{fn}を追記するだけでOK。

  • RailsRuby-GetTextを利用する、お決まりのコード2行は以下のように設定。(オレンジ色の箇所)
# 設定: config/environment.rb
...(中略)...
# Include your application configuration below

require 'gettext/rails'
# コントローラー: app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...(中略)...
  init_gettext 'test_slip'
end
  • environment.rbを変更したら、サーバーの再起動を忘れずに。
  • validateでオリジナルなエラーメッセージを設定している場合は、文字列中に%{fn}と書くと、Ruby-GetTextがそれをフィールド名に置き換えてくれる。(オレンジ色の箇所)
# モデル: app/models/slip.rb
class Slip < ActiveRecord::Base
  has_many :journals, :order=>:position, :dependent=>:destroy
  validates_presence_of :number, :executed_on, :total_yen

  def validate
    # 明細の入力チェック
    errors.add_to_base("明細が一行も入力されていません。") unless journals_valid?
    # 合計金額のチェック
    # nilが含まれると数値として取り扱えないので、to_iで数値に変換しておく必要あり
    errors.add(:total_yen, "%{fn}が明細の合計と一致していません。") unless total_yen.to_i == journals_total_yen
  end
...(中略)...
class Journal < ActiveRecord::Base
  belongs_to :slip
  validates_presence_of :comment, :yen
  validates_exclusion_of :yen, :in=>(0..0), :message=>"%{fn}0 は入力できません。"

  def validate
    errors.add(:yen, "%{fn}が合計金額と一致していません。") unless slip.total_yen.to_i == slip.journals_total_yen
  end
...(中略)...
  • ついでに、フィールド名も翻訳しておいた。

これでエラーメッセージも自然な日本語になった!

問題3: fieldWithErrorsのdivタグをspanタグに変更する

些細な空行のことだが、結構悩んでしまった...。(scaffoldが生成する入力フォームではすべて、検証エラーが発生するとラベルとフォームの間に余分な空行が入ってしまう...。)

  • scaffoldは以下のような形式の入力フォームを生成してくれる。
<!--[form:slip]-->
  <p><label for="slip_number">Number</label><br />
    <%= text_field 'slip', 'number'  %>
  </p>
...(中略)...
  • 上記は検証エラーが無ければ、以下のhtmlに変換される。
<!--[form:slip]-->
  <p><label for="slip_number">Number</label><br />
    <input id="slip_number" name="slip[number]" size="30" type="text" />    
  </p>
  • もし検証エラーが発生すれば、以下のhtmlが生成される。(<div class="fieldWithErrors">タグで囲まれる。)
<!--[form:slip]-->
  <p><label for="slip_number">伝票No.</label><br />
    <div class="fieldWithErrors"><input id="slip_number" name="slip[number]" size="30" type="text" /></div>
  </p>
  • 頭で考えると、これの何処に空行が入る要因があるのか、全く理解できなかった...。(もしかして<br />タグが怪しいと思ったりして、削除してみたが、エラー無しの状態ではラベルとフォームが改行無しの一行で表示され、エラーが発生するとやはり同じように空行が入った状態になってしまう。)
  • ところが、Firebugでinspectしてみて気付いた!pタグの中にあるはずのdivタグが、pタグの外に弾き出されてしまっている...。以下のような状況になっている。
<p>
  <label for="slip_number">伝票No.</label><br/>
</p>
<div class="fieldWithErrors">
  <input id="slip_number" type="text" size="30" name="slip[number]" autocomplete="off"/>
</div>
  • これはtableタグの直下にdivタグが存在する場合、同じようにtableタグの外に出されて表示される現象と同じではないか?と考えた。pタグの中にdivタグの存在は許されないのかもしれない。*3
  • そこで、検証エラーが発生したとき<span class="fieldWithErrors">タグで囲うように変更してみた。*4environment.rbで以下のように設定することで、すべてのfieldWithErrorsのタグがspanタグに変更される。
# 設定: config/environment.rb
...(中略)...
# Include your application configuration below

# fieldWithErrorsのタグを<span>に変更する。
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  "<span class=\"fieldWithErrors\">#{html_tag}</span>"
end
  • environment.rbを変更したら、サーバーの再起動を忘れずに。
      • なぜ上記コードでspanタグに置き換えられるのか、自分では理解できていない...。そもそも、検証エラー発生時にどのような仕組みでfieldWithErrorsクラスのタグが挿入されるのか、それさえも理解できていない。Railsのソースのどの部分を読むべきなのか?知りたい...。

以上で3つの問題が解決できた!サーバーを再起動して試してみる。

心に優しいエラー表示になっただろうか?

*1:例:errors.add_to_base("明細が一行も入力されていません。") unless journals_input?

*2:マニュアルのshow sourceで確認すると、コードの中に「errors.is_a?(Array) ? errors.first : errors」となっている箇所がある。それにしても、どうしてこんな仕様になっているんだろう?

*3:htmlで正しくは、divは複数の段落pなどブロックレベル要素をグループ化するために使うようだ。

*4:spanは段落内の特定の文字列などインラインレベル要素をグループ化するために使うようだ。