- PVSM.RU - https://www.pvsm.ru -
Когда я только начинал задумываться о том, чтобы приобщиться к миру веб-разработки, и выбирал язык, с которого начну, одна из википедий мне напела, что в основе философии Rails лежат 2 принципа: Convention over configuration (CoC) и Don’t Repeat Yourself (DRY). Что касается первого — я тогда вобще не понял о чём речь, а вот второй понял, принял и ожидал, что в недрах этого замечательного фреймворка, я отыщу нативный инструмент, позволяющий мне один раз написать правила валидации для атрибутов модели, и потом использовать эти правила как для front, так и для back проверок.
Как выяснилось позже — меня ждало разочарование. В рельсах из коробки подобной штуки нет, и всё, что удалось отыскать по теме в ходе обучения — это railscast про client_side_validations gem.
Я тогда о javascript знал только то, что он есть, поэтому пришлось молча прикрутить гем к рождающемуся блогу и отложить тему dry-валидаций до более близкого знакомства с js. И вот это время пришло: мне понадобился гибкий инструмент для проверки форм, и переписывать каждый validates_inclusion_of
на js-манер я был не намерен. Да и гем тот больше не поддерживается.
Найти способ, который позволит:
Решение материализовано в небольшой демке: http://sandbox.alexfedoseev.com/dry-validation/showoff [1]
И пара поясняющих абзацев ниже.
Забыл упомянуть, что я в меру ленив, и писать собственный js-валидатор в мои планы изначально не входило. Из готовых решений мой выбор пал на jQuery Validation Plugin [2].
Его можно просто закинуть в js-ассеты или поставить как гем [3].
Больше ничего стороннего не потребуется.
Нести доброе светлое буду через пример. Допустим у нас есть список рассылки, в котором хранятся электронные адреса и настройка периодичности рассылки для каждого адреса (сколько раз в неделю пуляем письмо).
Соответственно есть модель — Email
И два её атрибута:
email
— электронный адресfrequency
— периодичность, с которой рассылка будет уходить на данный адресКакие будут ограничения:
email
обязательноemail
должен быть уникаленemail
должен быть email (с собакой и прочими рюшечками)frequency
не обязателен, но если есть, то должен быть в диапазоне от 1 до 7Воплощаем:
app/models/email.rb
class Email < ActiveRecord::Base
before_save { self.email = email.downcase }
VALID_EMAIL_REGEX = /A[w+-.]+@[a-zd-.]+.[a-z]+z/i
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: VALID_EMAIL_REGEX }
validates_inclusion_of :frequency, in: 1..7, allow_blank: true
end
class EmailsController < ApplicationController
def new
@email = Email.new
end
def create
@email = Email.new(email_params)
if @email.save
flash[:success] = 'Email добавлен!'
redirect_to emails_url
else
render :new
end
end
private
def email_params
params.require(:email).permit(:email, :frequency)
end
end
app/views/emails/new.html.haml
%h1 Новая почта
= form_for @email do |f|
= render partial: 'shared/error_messages', locals: { object: f.object }
%p= f.text_field :email, placeholder: 'Почта'
%p= f.text_field :frequency, placeholder: 'Периодичность рассылки'
%p= f.submit 'Добавить!'
Следующий шаг — повесить валидатор на форму и посмотреть что к чему.
Делается это просто: $('#form').validate();
Повторю ссылку на документацию к плагину [4], чтобы к ней больше не возвращаться. Там со структурированностью контента небольшая проблема, но информация вся есть.
Итак, вешаем:
app/assets/javascripts/emails.js.coffee
jQuery ->
validate_url = '/emails/validate'
$('#new_email, [id^=edit_email_]').validate(
debug: true
rules:
'email[email]':
required: true
remote:
url: validate_url
type: 'post'
'email[frequency]':
remote:
url: validate_url
type: 'post'
)
Пройдемся по каждой строчке:
validate_url = '/emails/validate'
$('#new_email, [id^=edit_email_]').validate
debug: true
rules:
'email[email]':
На следующих двух методах остановимся подробнее.
remote:
url: validate_url
type: 'post'
Сначала поговорим о главном методе этого поста — remote
. С его помощью мы можем отсылать ajax-запросы к серверу и обрабатывать возвращаемые данные.
Как оно работает: методу нужно скормить url запроса и его тип (в нашем случае отсылаем post-запрос). Этого достаточно, чтобы отправить значение поля на проверку серверу.
В ответ метод ожидает получить json
:
true
— означает, что с полем всё окfalse
, undefined
или null
, а также любая другая string'а
— расцениваются методом как сигнал провальной валидацииrequired: true
Метод «обязательных полей». Единственная проверка, которую нельзя (да и не нужно) выполнять через обращение к серверу, — это validates_presence_of
(то есть валидацию наличия). Это связано с особенностями работы валидатора — он дёргает метод remote
только в том случае, если в поле вводились какие-либо данные. «Запустить руками» данную проверку невозможно, поэтому валидации наличия прописываем непосредственно через данный метод. Кстати, он принимает в качестве аргумента функции, поэтому сложные логические проверки на наличие можно (и нужно) осуществлять через него.
Валидатор повешен, ajax-запрос уходит к серверу, что дальше:
app/controllers/emails_controller.rb
def validate
# пока пустой
end
config/routes.rb
resources :emails
post 'emails/validate', to: 'emails#validate', as: :emails_validation
Отлично, теперь сервер может принимать post-запросы на адрес '/emails/validate'
Давате запустим сервер, откроем форму создания Email
в браузере (lvh.me:3000/emails/new), наберём «что-нибудь» в поле формы и бегом в консоль — смотреть что же передаёт нам валидатор.
В общем-то, этого можно было ожидать:
Started POST "/emails/validate" for 127.0.0.1 at 2014-02-17 22:10:31 +0000
Processing by EmailsController#validate as JSON
Parameters: {"email"=>{"frequency"=>"что-нибудь"}}
Теперь о стратегии: что мы будем делать с этим добром — как обрабатывать и что возвращать:
json
, мы создадим в памяти новый объект Email
ActiveModel
ActiveModel::Errors
(доступный через метод errors
), в котором будет хэш @messages
— либо с ошибками (если атрибуты не прошли валидацию), либо пустой (если с объектом всё хорошо)true
, а если в нём есть ошибки для проверяемого атрибута — ответим текстом этих ошибок, что будет расценено сервером как провальная валидация. И, более того, плагин использует полученную стрингу как текст сообщения об ошибке.Шо-ко-лад! Мало того, что правила валидации прописываются один раз непосредственно в моделе, так ещё и сообщения об ошибках хранятся непосредственно в локале рельс.
Кстати, давайте их напишем.
config/locales/ru.yml
ru:
activerecord:
attributes:
email:
email: "Почта"
frequency: "Периодичность"
errors:
models:
email:
attributes:
email:
blank: "обязательна"
taken: "уже добавлена в список рассылки"
invalid: "имеет странный формат"
frequency:
inclusion: "должна быть в диапазоне от 1 до 7 включительно"
О I18n читаем в гайдах Rails: http://guides.rubyonrails.org/i18n.html [5]
Названия атрибутов и сообщения прописаны.
Теперь самое интересное — формируем ответ браузеру.
Сразу вываливаю работающий код, который будем разбирать по строчкам:
app/controllers/emails_controller.rb
def validate
email = Email.new(email_params)
email.valid?
field = params[:email].first[0]
@errors = email.errors[field]
if @errors.empty?
@errors = true
else
name = t("activerecord.attributes.email.#{field}")
@errors.map! { |e| "#{name} #{e}<br />" }
end
respond_to do |format|
format.json { render json: @errors }
end
end
Поехали.
email = Email.new(email_params)
email.valid?
Создаём в памяти объект из прилетевших от формы параметров и дёргаем проверку на валидность, чтобы в памяти появился объект ActiveModel::Errors
. В хэше @messages
с ошибками, помимо нужных нам для проверяемого атрибута, будут лежать и сообщения для всех остальных атрибутов (т.к. значения всех остальных — nil
, прилетело же только значение проверяемого атрибута).
Давайте посмотрим как выглядит объект, чтобы понять как его разобрать:
(rdb:938) email.errors
#=> #<ActiveModel::Errors:0x007fbbe378dfb0 @base=#<Email id: nil, email: nil, frequency: "что-нибудь", created_at: nil, updated_at: nil>, @messages={:email=>["обязательна", "имеет странный формат"], :frequency=>["должна быть в диапазоне от 1 до 7 включительно"]}>
Мы видим хэш с сообщениями об ошибках, и доки нам подсказывают как их достать [6]:
(rdb:938) email.errors['frequency']
#=> ["должна быть в диапазоне от 1 до 7 включительно"]
То есть для того, чтобы достать ошибки для атрибута, нам прежде всего нужно достать имя этого атрибута.
Это мы вытянем из хэша params
:
# так выглядит хэш
(rdb:938) params
#=> {"email"=>{"frequency"=>"что-нибудь"}, "controller"=>"emails", "action"=>"validate"}
# нам известно название модели, поэтому достаём атрибуты
(rdb:938) params[:email]
#=> {"frequency"=>"что-нибудь"}
# поскольку за запрос улетает всегда один атрибут, то забираем первый
(rdb:938) params[:email].first
#=> ["frequency", "что-нибудь"]
# на первом месте всегда будет ключ, то есть имя атрибута -> забираем
(rdb:938) params[:email].first[0]
#=> "frequency"
Возвращаемся к функции валидации в контроллере:
field = params[:email].first[0]
@errors = email.errors[field]
Сначала мы достали название проверяемого атрибута модели, потом вытащили массив с сообщениями об ошибках.
После этого сформируем ответ браузеру:
if @errors.empty?
@errors = true
else
name = t("activerecord.attributes.email.#{field}")
@errors.map! { |e| "#{name} #{e}<br />" }
end
Если массив с ошибками пуст, то переменная @errors
— это true
(именно этот ответ ожидает плагин, если ошибок нет).
Если же в массиве есть ошибки, то:
@errors
, то получим сообщение «должна быть в диапазоне от 1 до 7 включительно» (а если их будет несколько, то они вобще «слипнутся» при выводе)Поэтому мы:
name = t("activerecord.attributes.email.#{field}")
@errors.map! { |e| "#{name} #{e}<br />" }
br
в конце каждой ошибки прилепить, зависит от вёрстки, короче добавлять по вкусу
Получаем в итоге массив с сообщениями в формате:
«Периодичность должна быть в диапазоне от 1 до 7 включительно».
И последний штрих — пакуем всё это в json
:
respond_to do |format|
format.json { render json: @errors }
end
Rails отдаёт ответ браузеру.
Для одной модели это работает, но у нас в приложении будет много моделей. Для того, чтобы не-повторять-себя, можно переписать роутинг и метод валидации в контроллере.
config/routes.rb
# было
post 'emails/validate', to: 'emails#validate', as: :emails_validation
# чтобы не переписывать маршрут для каждого контроллера
post ':controller/validate', action: 'validate', as: :validate_form
Вынесем логику валидации в application_controller.rb, чтобы его могли использовать любые контроллеры приложения.
app/controllers/application_controller.rb
def validator(object)
object.valid?
model = object.class.name.underscore.to_sym
field = params[model].first[0]
@errors = object.errors[field]
if @errors.empty?
@errors = true
else
name = t("activerecord.attributes.#{model}.#{field}")
@errors.map! { |e| "#{name} #{e}<br />" }
end
end
app/controllers/emails_controller.rb
def validate
email = Email.new(email_params)
validator(email)
respond_to do |format|
format.json { render json: @errors }
end
end
P.S. Чтобы не дёргать сервер при каждом введённом пользователем символе в полях формы, установите значение метода onkeyup: false
jQuery Validation Plugin:
http://jqueryvalidation.org [7]
Демо с бантиками:
http://sandbox.alexfedoseev.com/dry-validation/showoff [1]
Автор: alexfedoseev
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby-on-rails/55330
Ссылки в тексте:
[1] http://sandbox.alexfedoseev.com/dry-validation/showoff: http://sandbox.alexfedoseev.com/dry-validation/showoff
[2] jQuery Validation Plugin: http://jqueryvalidation.org/
[3] поставить как гем: https://github.com/danryan/jquery-validation-rails
[4] ссылку на документацию к плагину: http://jqueryvalidation.org/documentation/
[5] http://guides.rubyonrails.org/i18n.html: http://guides.rubyonrails.org/i18n.html
[6] подсказывают как их достать: http://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-5B-5D
[7] http://jqueryvalidation.org: http://jqueryvalidation.org
[8] Источник: http://habrahabr.ru/post/213077/
Нажмите здесь для печати.