タイトルの通りです。
結論
-
ActiveModel::EachValidator
の派生クラスとして○○Validator
を作る - その中で改行の文字数カウントを修正してからバリデーション判定
早速ですが具体例を見てみましょう。
現象
とても簡略化したお問い合わせフォームを用意しました。
文字数制限は50文字とします。
(ちなみにその投稿画面には、javascriptで文字数をカウントして表示しているとします。)
<%= form_with url: contacts_path, method: :post, local: true do |f| %>
<%= f.text_area :message %>
<%= f.submit "送信する" %>
<% end %>
必須かつ50文字を超えたらエラーメッセージが表示されます。
class Contact < ApplicationRecord
validate :message, presence: true, length: { maximum: 50, message: '%{count}文字以内で入力してください' }
# 〜(中略)〜
end
この状態でユーザーが以下のように送信したらどうなるでしょうか。
1234567890↵ (↵ = 改行)
1234567890↵
1234567890↵
1234567890↵
123456
46文字 + 改行4つ = HTMLの50文字
要件を満たしていますが…
エラーメッセージ
「50文字以内で入力してください」
ユーザー「なんでや!」
追求
paramsで中を見てみる
[1] pry(#<ContactsController>)> params
=> <ActionController::Parameters {"utf8"=>"✓",
"message"=>"1234567890\r\n1234567890\r\n1234567890\r\n1234567890\r\n123456",
〜(中略)〜 }>
改行が\r\n
となっている!?
原因
改行が\r\n
と2文字で表現されていることによるものでした。
なので今回は改行を1文字としてカウントさせて改めて文字数制限に引っかかるかどうかを、
カスタムバリデータのvalidate_eachメソッドで作成します。
#実装
class Contact < ApplicationRecord
# 変更前
validate :message, presence: true, length: { maximum: 50, message: '%{count}文字以内で入力してください' }
# 変更後
validate :message, presence: true, correct_line_break: true
end
length
パラメーターの代わりにcorrent_line_break
というカスタムバリデータを付与しています。
class CorrectLineBreakValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# 改行以外の文字数 (「\r\n」以外の文字数をcountメソッドで数える)
text_length = value&.count("^\r\n") || 0
# 改行の回数 (「\r\n」という塊の数だけscanメソッドで配列にし、その長さをlengthメソッドで求めている)
break_length = value&.scan("\r\n")&.length || 0
# 文字数 + 改行の回数
correct_text_length = text_length + break_length
# 50超ならエラーメッセージ追加
record.errors.add(attribute, "50文字以内で入力してください") if correct_text_length > 50
end
end
ロジックはコメントを見ていただければわかるかと思います。
undefined method 'count' for nil:NilClass
対策でつけている
&.
についてはこちらを参考にしてください。
[Ruby の &. と #try の違い]
(http://secret-garden.hatenablog.com/entry/2016/09/02/000000)
使い方
-
app/models
配下にファイルを作成する。 -
ActiveModel::EachValidator
を継承する。 -
def validate_each(record, attribute, value)
を定義する - 引数の意味は
-
record
は検証対象のオブジェクト名(Contact) -
attribute
検証対象のフィールド名(:message) -
value
検証対象の値("1234567890\r\n......123456")
-
注意点
命名ルールとして、
-
モデルで指定するカスタムバリデータ名(
correct_line_break
: true) - 「models/validators/」配下に作成する○○_validator.rbのファイル名(models/validators/
correct_line_break
_validator.rb) -
クラス名(class
CorrectLineBreak
Validator)
は同じである必要があります。
上2つはスネークケース、クラス名はパスカルケースですね。
これで意図したバリデーション検証が可能となりました。
発展
50文字という静的な値の設定は、他の箇所でも使いたい場合に適していません。
動的に文字数を変更するため、パラメータを渡す方法もあります。
class Contact < ApplicationRecord
# 変更前
validate :message, presence: true, correct_line_break: true
# 変更後
validate :message, presence: true, correct_line_break: { maximum: 100 }
end
class CorrectLineBreakValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
text_length = value&.count("^\r\n") || 0
break_length = value&.scan("\r\n")&.length || 0
correct_text_length = text_length + break_length
record.errors.add(attribute, "#{options[:maximum]}文字以内で入力してください") if correct_text_length > options[:maximum]
end
end
パラメータはoptions[:maximum]
で取得が可能です。
参考資料
Ruby on Rails 5アプリケーションプログラミング
【Rails】カスタムバリデータの使い方
Railsの文字数のバリデーション問題を解決する 改行文字が2文字と換算されてしまう場合の対処方法