LoginSignup
3
4

More than 3 years have passed since last update.

[Rails]改行が2文字でカウントされてバリデーションが正しく動作しない件(カスタムバリデータ(validate_eachメソッド)で解決)

Last updated at Posted at 2019-12-25

タイトルの通りです。

結論

  • ActiveModel::EachValidatorの派生クラスとして○○Validatorを作る
  • その中で改行の文字数カウントを修正してからバリデーション判定

早速ですが具体例を見てみましょう。

現象

とても簡略化したお問い合わせフォームを用意しました。
文字数制限は50文字とします。
(ちなみにその投稿画面には、javascriptで文字数をカウントして表示しているとします。)

view/contacts/new.html.erb
<%= form_with url: contacts_path, method: :post, local: true do |f| %>
  <%= f.text_area :message %>
  <%= f.submit "送信する" %>
<% end %>

必須かつ50文字を超えたらエラーメッセージが表示されます。

model/contact.rb
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メソッドで作成します。

実装

models/contact.rb
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というカスタムバリデータを付与しています。
 

models/validators/correct_line_break_validator.rb
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 の違い

使い方

  • 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 CorrectLineBreakValidator)

は同じである必要があります。
上2つはスネークケース、クラス名はパスカルケースですね。

これで意図したバリデーション検証が可能となりました。

発展

50文字という静的な値の設定は、他の箇所でも使いたい場合に適していません。
動的に文字数を変更するため、パラメータを渡す方法もあります。

models/contact.rb
class Contact < ApplicationRecord
  # 変更前
  validate :message, presence: true, correct_line_break: true
  # 変更後
  validate :message, presence: true, correct_line_break: { maximum: 100 }
end
correct_line_break_validator.rb
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文字と換算されてしまう場合の対処方法

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4