263: クライアント側での検証
Other formats:
ウェブサイトの長い入力フォームを埋めて送信した後に検証エラーがいくつも返されてくるのはイライラするものです。フィールドに対するエラーをそのフィールドへの入力が終わる都度表示できれば、ずっと優れたユーザ体験を提供できるでしょう。しかしそのためには、JavaScriptでチェック用コードを大量に書く必要があります。
今回のエピソードでは、クライアント側の検証を自動的に設定してくれるgemを紹介します。たとえば、入力フィールドが4つある、次のような新規ユーザ用の登録フォームを持つアプリケーションがあるとしましょう。
Userモデルにはいくつかの検証機能があります。その内の一つが必須入力フィールドのチェックで、対象フィールドのうちのどれかが未入力の場合に、その分の検証エラーが表示されます。
冒頭で述べたように、あるフィールドに対するエラーを、ユーザがタブキーで次のフィールドに移動するときに表示できれば便利でしょう。しかしそのためにJavaScriptのコードを書くのがかなりの手間になります。検証したいモデルが複数ある場合や検証が複雑な場合は特にそうです。この手間をなくすために、client_side_validationsというgemを利用して作業を任せることにしましょう。このgemはモデルの検証機能を読み込んで、クライアント側での検証に必要なJavaScriptを生成し、これがユーザのフォーム入力時に起動されます。このgemの唯一標準でないところはjQueryを必要とする点だけです。もしRailsアプリケーションでPrototypeを使用している場合は別の対応策を探す必要があります。
client_side_validationsをインストールする
gemをインストールするために、アプリケーションのGemfileに参照情報を追加しbundleコマンドを実行します。
/Gemfile
source 'http://rubygems.org' gem 'rails', '3.0.7' gem 'sqlite3' gem 'nifty-generators' gem 'bcrypt-ruby', :require => 'bcrypt' gem 'jquery-rails' gem 'client_side_validations'
今回はjquery-rails gemを追加して、アプリケーションにjQueryを設定してあります。client_side_validations gemがインストールされたら、このgemが提供するジェネレータを実行します。これにより、アプリケーションにいくつかのファイルが追加されます。まずconfig/initializersディレクトリに設定ファイルが、そしてJavaScriptファイルが追加されるのでレイアウトファイルに参照情報を追記します。
$ rails g client_side_validations:install
create config/initializers/client_side_validations.rb
create public/javascripts/rails.validations.js
ここでレイアウトファイルを修正します。includeするJavaScriptファイルのリストにrails.validationsを追加します。
/app/views/layouts/application.html.erb
<%= javascript_include_tag :defaults, "rails.validations" %>
client_side_validationsの設定ファイルも修正します。デフォルト設定ではSimpleFormとFormtasticを使用するようになっていますが、今回はどちらも使わないので、ファイルの末尾のセクションを非コメント化してfield_error_procメソッドをオーバーライドします。
/config/initializers/client_side_validations.rb
# ClientSideValidations Initializer
require 'client_side_validations/simple_form' if defined?(::SimpleForm)
require 'client_side_validations/formtastic' if defined?(::Formtastic)
# Uncomment the following block if you want each input field to have the validation messages attached.
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
unless html_tag =~ /^<label/
%{<div class="field_with_errors">#{html_tag}<label for="#{instance.send(:tag_id)}"
class="message">#{instance.error_message.first}</label> </div>}.html_safe
else
%{<div class="field_with_errors">#{html_tag}</div>}.html_safe
end
end
このメソッドは、不正なフィールドに対して、エラーメッセージを含んでmessageクラスを持ったlabel要素を追加します。デフォルト設定のエラーの表示方法がアプリケーションに合わない場合は、上記のファイルをカスタマイズできます。
さらにもう一つのステップが必要です。form_forのフォーム呼び出しのうち、検証を加えたいものに:validate => trueオプションを追加します。これで検証が動作するために必要なものがすべて揃いました。
登録ページを再度読み込み、ユーザ名フィールドに何も値を入力せずにタブキーで抜け出そうとすると、エラーメッセージがページのその場所にすぐに表示されます。他のフィールドでエラーが出る場合も同様です。
より複雑な検証
簡単な検証はデフォルト設定でうまくいきますが、より複雑なものはどうでしょうか? Userモデルにはその他にも検証が必要な部分があります。usernameフィールドの形式をチェックして通常以外の文字が入力されないようにする、パスワードの長さをチェックする、パスワードの確認フィールドの中身が一つ目のフィールドと一致しているかをチェックする、などです。
/app/models/user.rb
validates_presence_of :username, :email validates_presence_of :password, :on => :create validates_presence_of :username, :with => /^[-\w\._@]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or .-_@" validates_length_of :password, :minimum => 4, :allow_blank => true validates_confirmation_of :password
これらの検証はすべてclient_side_validationsによってJavaScriptに翻訳されて動作するので、不正なユーザ名や短すぎるパスワードを入力すると、すぐにそのように警告を受けるようになります。
エラーが表示されているとフォームを送信できませんが、各フィールドに正しい値が入力されると送信が可能になり、ユーザが登録されます。
検証の中には、validates_uniqueness_ofのようにデータベースからデータを読み出さなくてはいけないものもあります。usernameとemailのフィールドは値をユニークにしたいので、この検証機能を追加してどうなるか見てみましょう。
/app/models/user.rb
validates_uniqueness_of :username, :email
"eifion"というユーザアカウントをすでに作成済みで、同じ名前で作成しようとするとusernameフィールドからタブキーで抜けようとするとエラーが表示されます。
これが動作する仕組みとしては、ユーザがusernameフィールドからタブキーで抜けようとしたときにclient_side_validationsがAJAXリクエストを発行し、そのユーザ名がすでに存在するかどうかをサーバでチェックしています。
スタイルを追加する
エラーメッセージの表示をカスタマイズしたいので、対象となるCSSを見てみます。まずfield_with_errorsクラスのスタイルを設定します。このクラスは、要素をインラインで表示するために、Railsが不正なフィールドに追加するものです。フィールドに対するエラーメッセージは、メッセージのclassを持つlabel要素です。これをもう少しエラーメッセージらしく見せるために赤字で表示して、関連するフォーム要素から少し左側にずらします。
/public/stylesheets/application.css
.field_with_errors {
display: inline;
}
.field_with_errors .message {
color: #D00;
padding-left: 5px;
font-size: 12px;
}
登録ページを再度読み込んでエラーを発生させると、新しく設定されたスタイルを見ることができます。
カスタム検証
エピソード211[動画を見る, 読む]で、電子メールアドレスの形式を検証するためにEmailFormatValidatorと呼ばれるカスタム形式の検証クラスを作成しました。このRubyの検証用クラスをJavaScriptに翻訳して、クライアント側で標準のRailsの検証機能のように使えるようにするには、どうすればいいでしょうか?
ありがたいことに、client_side_validations Wikiにまさにこの方法について説明しているページがあるので、作成中のアプリケーションにEmailFormatValidatorを再現し、このページの情報を使ってクライアントでも動作するように拡張します。
まず最初に、アプリケーションの/libディレクトリ内のファイルにvalidatorを追加します。
/lib/email_format_validator.rb
class EmailFormatValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
object.errors[attribute] << (options[:message] || "is not formatted properly")
end
end
end
このクラスは、渡された値が正規表現に一致するかどうかをチェックして、一致しなかった場合にエラーメッセージをモデルのエラーリストに追加します。これを実現するために、簡単なvalidates_format_ofを使用することもできたのですが、例を示す目的でカスタムの検証機能を作成しました。
Rails 3では/libディレクトリのファイルはアプリケーションに自動的にincludeされません。このディレクトリをアプリケーションのapplication.rbファイルの autoload_pathsに追加して、そこのファイルがロードパスに追加されるようにします。
/config/application.rb
require File.expand_path('../boot', __FILE__)
require 'rails/all'
# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)
module Validator
class Application < Rails::Application
config.autoload_paths << "#{config.root}/lib"
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
end
end
カスタムの検証機能は、localesファイル内にデフォルトのエラーメッセージが定義されている必要があります。エラーメッセージは、errors/messages下にネストされ、カスタム検証機能の名前と一致するキー、この場合はemail_formatを持っている必要があります。
/config/locales/en.yml
# Sample localization file for English. Add more files in this directory for other locales.
# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
en:
errors:
messages:
email_format: "is not formatted properly."
これで作成した検証機能をUserモデルに追加できます。validatesを用いて、検証したいファイルの名前と検証機能の名前を渡します。
/app/models/user.rb
validates :email, :email_format => true
このファイルのtrueの部分をオプションのハッシュに置き換えれば、カスタムのエラーメッセージを設定するなどできるのですが、ここではとりあえずデフォルトのままにしておきます。
フォームに不正なメールアドレスを入力して送信すると、カスタム検証機能のエラーメッセージが表示されるはずですが、フィールドからタブキーで抜けてもエラーは現れません。この検証機能はサーバで動作しているのですが、EmailFormatValidatorのコードをJavaScriptに翻訳してclient_side_validationに変換して、メールフィールドがフォーカスを失うときに起動されるようにする必要があります。
この検証機能のコードを/public/javascripts内のrails.validations.custom.jsという新規ファイルに記述します。このファイル名の形式は、プラグインに認識させるためのものです。ファイルにクライアントのフィールドを検証するコードを書きます。
/public/javascripts/rails.validations.custom.js
clientSideValidations.validators.local["email_format"] = function (element, options) {
if(!/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i.test(element.val())) {
return options.message;
}
}
このコードで、clientSideValidations.validatorsオブジェクトにemail_formatという名前の新しいlocalのvalidatorを追加して、新しく検証機能を作成しています。localに設定するということはvalidatorのすべてがクライアント側で実行されるということです。もしremote validationsに追加していたら、先に説明したvalidates_uniqueness_of validatorがおこなうように、サーバに対してAJAX呼び出しをおこなうことになります。
この検証機能のコードでは、Rubyコードがおこなっていたのと同じように、要素の値を正規表現と照合してメールアドレスのフォーマットを検証します。一致しなければエラーメッセージが返されます。アプリケーションに新しくJavaScriptの検証機能を追加するには、アプリケーションのレイアウトファイルで新しいvalidatorファイルへの参照を追加します。
/app/views/layouts/application.html.erb
<%= javascript_include_tag :defaults, "rails.validations", "rails.validations.custom" %>
すべての設定ができたので動作を試してみます。ページを再度読み込んで不正な形式のメールアドレスを入力すると、メールフィールドがフォーカスを失った瞬間にエラーメッセージが表示されます。
これでフォーム上のすべてのフィールドについてクライアント側での検証機能ができました。ブラウザでJavaScriptを有効にしていないユーザのために、検証機能はサーバでも実行されます。
クライアント側での検証機能についての今回のエピソードは以上です。カスタムの検証機能を書くことは大変な作業のように思うかもしれませんが、一つ目を設定できたらそれ以降追加していくのは簡単です。フォーム入力時にすぐにフィードバックを得ることができれば、ずっと優れたユーザ体験を提供できます。この機能をRailsアプリケーションに追加する優れた方法を紹介しました。


