![](http://habrastorage.org/getpro/habr/avatars/f4d/2fd/eb2/f4d2fdeb2019ae8d4d9883cff41ff0ce.png)
Rails: DRY style ajax validation
- Tutorial
When I was just starting to think about joining the world of web development and choosing the language I would like to start with, one of the wikipedias told me that Rails philosophy is based on 2 principles: Convention over configuration (CoC) and Don’t Repeat Yourself (DRY) . As for the first - I just didn’t understand what it was about, but the second understood, accepted and expected that in the bowels of this wonderful framework, I will find a native tool that allows me to write once the validation rules for model attributes, and then use these rules for both front and back checks.
As it turned out later, I was disappointed. There is no such thing in the rails from the box, and all that was possible to find on the topic during the training is railscast about client_side_validations gem.
At that time, I only knew about javascript that it is, so I had to silently fasten the gem to the emerging blog and postpone the topic of dry validations to a closer acquaintance with js. And now this time has come: I needed a flexible tool for checking forms, and
Find a way that will allow:
The solution is materialized in a small demo: http://sandbox.alexfedoseev.com/dry-validation/showoff
And a couple of explanatory paragraphs below.
I forgot to mention that I'm moderately lazy, and to write my own js-validator was not originally included in my plans. From ready-made solutions, my choice fell on jQuery Validation Plugin .
It can simply be thrown into js-assets or put as a gem .
Nothing else is required.
I will carry the good light through an example. Suppose we have a mailing list that stores email addresses and sets the frequency of mailing for each address (how many times a week we write a letter).
Accordingly, there is a model -
And its two attributes:
What will be the restrictions:
We implement:
app / models / email.rb
The next step is to hang the validator on the form and see what's what.
This is done simply:
So, hang up:
app / assets / javascripts / emails.js.coffee
Let's go through each line:
The following two methods will dwell in more detail.
First, let's talk about the main method of this post -
How it works: the method needs to feed the request url and its type (in our case, send the post-request). This is enough to send the field value for verification to the server.
In response, the method expects to receive
The method of "required fields". The only check that you can’t (and don’t) need to be done through a server call is
The validator is hung up, the ajax request goes to the server, what's next:
app / controllers / emails_controller.rb
config / routes.rb
Great, now the server can accept post-requests to the address.
Let's start the server, open the creation form
In general, this could be expected:
Now about the strategy: what will we do with this good - how to process and what to return:
Chocolate! Not only are validation rules written once directly in the model, but also error messages are stored directly in the rail locale.
By the way, let's write them.
config / locales / ru.yml
Attribute names and messages are spelled out.
Now the most interesting part is creating a response to the browser.
I immediately dump the working code, which we will parse into the lines:
app / controllers / emails_controller.rb
Go.
We create an object in memory from the parameters that arrived from the form and pull the validation check so that the object appears in memory
Let's see how an object looks to understand how to parse it:
We see a hash with error messages, and the docks tell us how to get them :
That is, in order to get errors for an attribute, we first need to get the name of this attribute.
This we will pull from the hash
We return to the validation function in the controller:
First, we got the name of the checked attribute of the model, then we pulled out an array with error messages.
After that, we will formulate a response to the browser:
If the array with errors is empty, then the variable
If there are errors in the array, then:
That's why we:
As a result, we
get an array with messages in the format: "The frequency should be in the range from 1 to 7 inclusive . "
And the last touch - we pack it all in
Rails responds to the browser.
For one model, this works, but we will have many models in the application. In order to not-repeat-yourself, you can rewrite the routing and validation method in the controller.
config / routes.rb
We will take out the validation logic in application_controller.rb so that it can be used by any application controllers.
app / controllers / application_controller.rb
app / controllers / emails_controller.rb
PS In order not to jerk the server with each character entered by the user in the form fields, set the value of the
jQuery Validation Plugin method :
http://jqueryvalidation.org
Demo with bows:
http://sandbox.alexfedoseev.com/dry-validation/showoff
UPDATE: Edited heading
As it turned out later, I was disappointed. There is no such thing in the rails from the box, and all that was possible to find on the topic during the training is railscast about client_side_validations gem.
At that time, I only knew about javascript that it is, so I had to silently fasten the gem to the emerging blog and postpone the topic of dry validations to a closer acquaintance with js. And now this time has come: I needed a flexible tool for checking forms, and
validates_inclusion_of
I did not intend to rewrite each one in js-style. And the gem is no longer supported.Formulation of the problem
Find a way that will allow:
- when validating attributes use one logic: both for the back and for the front
- quickly "hang up" checks on different forms and flexibly configure them (both logic and visual)
The solution is materialized in a small demo: http://sandbox.alexfedoseev.com/dry-validation/showoff
And a couple of explanatory paragraphs below.
Instruments
I forgot to mention that I'm moderately lazy, and to write my own js-validator was not originally included in my plans. From ready-made solutions, my choice fell on jQuery Validation Plugin .
It can simply be thrown into js-assets or put as a gem .
Nothing else is required.
I will carry the good light through an example. Suppose we have a mailing list that stores email addresses and sets the frequency of mailing for each address (how many times a week we write a letter).
Let's get to the point
Accordingly, there is a model -
Email
And its two attributes:
email
- email addressfrequency
- the frequency with which the newsletter will go to this address
What will be the restrictions:
- availability
email
required email
must be uniqueemail
must be email (with a dog and other things)frequency
optional, but if any, it should be in the range of 1 to 7
We implement:
app / models / email.rb
class Email < ActiveRecord::Base
before_save { self.email = email.downcase }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[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
In the controller and view, everything is absolutely standard
app / controllers / emails_controller.rb
app / views / emails / new.html.haml
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 'Добавить!'
The next step is to hang the validator on the form and see what's what.
This is done simply:
$('#form').validate();
I’ll repeat the link to the documentation for the plugin so that it no longer returns. There is a small problem with the structured content, but there is all the information.
So, hang up:
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'
)
Let's go through each line:
validate_url = '/emails/validate'
the address to which we will send the ajax request to check the field values$('#new_email, [id^=edit_email_]').validate
we hang the validator both on the form of a new email and on the form of editing existing addressesdebug: true
the method disables the form submission so that you can have fun with the settingsrules:
the verification rules for fields are written in the method, the plugin has a lot of them out of the box (see docks), but so far we are only interested in 2'email[email]':
name – attribute of the form field (simple names are indicated without quotes, with special characters - we take in quotes)
The following two methods will dwell in more detail.
remote
remote:
url: validate_url
type: 'post'
First, let's talk about the main method of this post -
remote
. With it, we can send ajax requests to the server and process the returned data. How it works: the method needs to feed the request url and its type (in our case, send the post-request). This is enough to send the field value for verification to the server.
In response, the method expects to receive
json
:- the answer
true
means that everything is ok with the field - answers
false
,undefined
ornull
, as well as any other,string'а
are regarded by the method as a signal of failed validation
required
required: true
The method of "required fields". The only check that you can’t (and don’t) need to be done through a server call is
validates_presence_of
(that is, availability validation). This is due to the features of the validator - it pulls the method remote
only if any data was entered in the field. "Run with your hands" this check is impossible, therefore, the validation of the presence is prescribed directly through this method. By the way, it takes functions as an argument, so complex logical checks for availability can (and should) be done through it.Continue
The validator is hung up, the ajax request goes to the server, what's next:
- you need to create a method in the controller that will handle the request
- register a route to this method
app / controllers / emails_controller.rb
def validate
# пока пустой
end
config / routes.rb
resources :emails
post 'emails/validate', to: 'emails#validate', as: :emails_validation
Great, now the server can accept post-requests to the address.
'/emails/validate'
Let's start the server, open the creation form
Email
in the browser (lvh.mehaps000/emails/new), type “something” in the form field and run to the console - see what it sends us a validator. In general, this could be expected:
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"=>"что-нибудь"}}
Now about the strategy: what will we do with this good - how to process and what to return:
- from arriving at the controller
json
, we will create a new object in memoryEmail
- and pull its validation through
ActiveModel
- an object of the class
ActiveModel::Errors
(accessible via the methoderrors
) will be drawn in memory , in which there will be a hash@messages
either with errors (if the attributes did not pass validation) or empty (if everything is fine with the object) - we will analyze this hash and, if it is empty, we will respond to the browser
true
, and if it has errors for the attribute being checked, we will respond with the text of these errors, which will be regarded by the receiving method of the plug-in as a failed validation. And, moreover, the plugin uses the string received as the text of the error message.
Chocolate! Not only are validation rules written once directly in the model, but also error messages are stored directly in the rail locale.
By the way, let's write them.
config / locales / ru.yml
ru:
activerecord:
attributes:
email:
email: "Почта"
frequency: "Периодичность"
errors:
models:
email:
attributes:
email:
blank: "обязательна"
taken: "уже добавлена в список рассылки"
invalid: "имеет странный формат"
frequency:
inclusion: "должна быть в диапазоне от 1 до 7 включительно"
We read about I18n in the Rails guides: http://guides.rubyonrails.org/i18n.html
Attribute names and messages are spelled out.
Now the most interesting part is creating a response to the browser.
I immediately dump the working code, which we will parse into the lines:
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}
" }
end
respond_to do |format|
format.json { render json: @errors }
end
end
Go.
email = Email.new(email_params)
email.valid?
We create an object in memory from the parameters that arrived from the form and pull the validation check so that the object appears in memory
ActiveModel::Errors
. In the @messages
error hash , in addition to the ones we need for the checked attribute, there will be messages for all other attributes (since the values of all the others are nil
, only the value of the checked attribute arrived). Let's see how an object looks to understand how to parse it:
(rdb:938) email.errors
#=> #, @messages={:email=>["обязательна", "имеет странный формат"], :frequency=>["должна быть в диапазоне от 1 до 7 включительно"]}>
We see a hash with error messages, and the docks tell us how to get them :
(rdb:938) email.errors['frequency']
#=> ["должна быть в диапазоне от 1 до 7 включительно"]
That is, in order to get errors for an attribute, we first need to get the name of this attribute.
This we will pull from the hash
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"
We return to the validation function in the controller:
field = params[:email].first[0]
@errors = email.errors[field]
First, we got the name of the checked attribute of the model, then we pulled out an array with error messages.
After that, we will formulate a response to the browser:
if @errors.empty?
@errors = true
else
name = t("activerecord.attributes.email.#{field}")
@errors.map! { |e| "#{name} #{e}
" }
end
If the array with errors is empty, then the variable
@errors
is this true
(it is this answer that the plugin expects if there are no errors). If there are errors in the array, then:
- if we give it away just like that
@errors
, we get a message “should be in the range from 1 to 7 inclusive” (and if there are several, they just “stick together” at the output)
That's why we:
- pull the model attribute name from the rail localization file:
name = t("activerecord.attributes.email.#{field}")
- and in each element of the array with errors, add this name to the beginning with a space:
@errors.map! { |e| "#{name} #{e}
" } - you can also
br
stick at the end of each error, it depends on the layout, in short add to taste
As a result, we
get an array with messages in the format: "The frequency should be in the range from 1 to 7 inclusive . "
And the last touch - we pack it all in
json
:respond_to do |format|
format.json { render json: @errors }
end
Rails responds to the browser.
Refactor
For one model, this works, but we will have many models in the application. In order to not-repeat-yourself, you can rewrite the routing and validation method in the controller.
Routing
config / routes.rb
# было
post 'emails/validate', to: 'emails#validate', as: :emails_validation
# чтобы не переписывать маршрут для каждого контроллера
post ':controller/validate', action: 'validate', as: :validate_form
Validation method
We will take out the validation logic in application_controller.rb so that it can be used by any application controllers.
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}
" }
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
PS In order not to jerk the server with each character entered by the user in the form fields, set the value of the
onkeyup: false
jQuery Validation Plugin method :
http://jqueryvalidation.org
Demo with bows:
http://sandbox.alexfedoseev.com/dry-validation/showoff
UPDATE: Edited heading