📜 ⬆️ ⬇️

Rails: DRY style ajax validation

When I first started to think about joining the world of web development, and chose a language to begin with, one of the Wikipedia sang to me that there are 2 principles at the heart of Rails philosophy: Convention over configuration (CoC) and Don't Repeat Yourself (DRY) . As for the first one, I didn’t understand what I was talking about, but the second one understood, accepted and expected that in the depths of this wonderful framework I would find a native tool that allows me to write validation rules for attributes of a model once 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 out of the box, and all that was found on the topic during training is a railscast about client_side_validations gem .

I then knew about javascript only what it is, so I had to silently screw the gems to the emerging blog and put 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 I was not going to rewrite every validates_inclusion_of in js-style. And that heme is no longer supported.

Formulation of the problem


Find a way to:
  1. in the validation of attributes use the same logic: both for the back and for the front
  2. quickly “hang up” checks for different forms and flexibly adjust 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 writing my own js-validator was not originally my plan. From ready-made solutions, my choice fell on the jQuery Validation Plugin .

It can simply be thrown into js-assets or set as heme .
Nothing more is needed.

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 a letter is sent).

Getting to the point


Accordingly, there is a model - Email
And its two attributes:

What are the limitations:


We embody:

app / models / email.rb

 class Email < ActiveRecord::Base before_save { self.email = email.downcase } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[az\d\-.]+\.[az]+\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

 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 will repeat the link to the documentation for the plugin in order not to return to it anymore. There is a small problem with the structured content, but all the information is there.

So, we hang:

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:

We will dwell on the following two methods.

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 a post-request). This is enough to send the field value to the server for verification.

In response, the method expects to get json :


required

 required: true 

The method of "required fields." The only check that cannot (and should not) be performed through a call to the server is validates_presence_of (that is, presence validation). This is due to the peculiarities of the validator - it pulls the remote method only if any data is entered in the field. "Run by hand" this check is impossible, therefore, validation of availability is prescribed directly through this method. By the way, it takes a function as an argument, so complex logical checks for presence can (and should) be performed through it.

We continue


The validator is hung, the ajax request goes to the server, which is further:


app / controllers / emails_controller.rb

  def validate #   end 


config / routes.rb

 resources :emails post 'emails/validate', to: 'emails#validate', as: :emails_validation 


Ok, now the server can accept post requests to the address '/emails/validate'
Let's start the server, open the Email creation form in the browser (lvh.me Tre000/emails/new), type “something” in the form field and run to the console - see what the validator reports to us.

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 we will do with this good - how to process and what to return:

Chocolate! Not only that the validation rules are 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 " 

Read about I18n in Rails guides: http://guides.rubyonrails.org/i18n.html

The names of the attributes and messages are spelled out.
Now the most interesting - we form the answer to the browser.

Immediately throwing out the working code, which we will sort through 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}<br />" } end respond_to do |format| format.json { render json: @errors } end end 


Go.

 email = Email.new(email_params) email.valid? 

Create an object in memory from the parameters arriving from the form and pull the validation check so that the ActiveModel::Errors object ActiveModel::Errors in the memory. The @messages hash with errors, in addition to the ones we need for the attribute being checked, will contain messages for all other attributes (since the values ​​of all the others are nil , only the value of the attribute being checked arrives).

Let's see what an object looks like to see how to disassemble it:

 (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 "]}> 

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 is what we extract from the params hash:

 #    (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" 

Go back 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 pulled out an array with error messages.

After that we will form the answer to the browser:

 if @errors.empty? @errors = true else name = t("activerecord.attributes.email.#{field}") @errors.map! { |e| "#{name} #{e}<br />" } end 

If the array with errors is empty, then the @errors variable is true (this is the answer the plugin expects if there are no errors).

If there are errors in the array, then:

That's why we:

We end up with an array of messages in the format:
"The periodicity should be in the range of 1 to 7 inclusive . "

And the final touch - we pack all this in json :

 respond_to do |format| format.json { render json: @errors } end 

Rails responds to the browser.

Refractory


For one model it works, but we will have many models in the application. In order to not repeat itself, 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 display the validation logic in application_controller.rb so that any application controllers can use it.

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 

PS In order not to pull the server with each onkeyup: false entered by the user in the form fields, set the value of the onkeyup: false method onkeyup: false

jQuery Validation Plugin:
http://jqueryvalidation.org

Demo with bows:
http://sandbox.alexfedoseev.com/dry-validation/showoff

UPDATE: Edited header

Source: https://habr.com/ru/post/213077/


All Articles