Change - Know that nothing stays the same

AngularJS validations with Rails model validations

January 25, 2014

angularjs

After my previous post about AngularJS and Rails, I think the best topic to continue talking about is the form validations.

First of all we need to understand what a form validation does in AngularJS. 

In all fields in our form we need to define the attribute ng-model to tell to AngularJS our field’s identification, is a good practice to use the model’s name to handle all of yours :

<input type=“text” name=“name" ng-model="model.name” />

AngularJS validates each input by many possible rules, for example, if you want to set an input as required you only need to set the attribute required:

<input type=“text” name=“name” required=“required” />

Finally our field is going to look more or less like this:

<input type=“text” name=“name” ng-model=“model.name” required=“required” />

if a field has many  validations, we are going to have:

<input id=“contact_email” name=“contact[email]” ng-model=“contact.email” ng-pattern=“/[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}/i” required=“required” type=“text”>

Each ng- is an AngularJS directive, ng-model is to name the model and the other in this cases are to handle validations.

With all this we can set out submit button to only be enabled when all fields are valid:

<input name=“commit” ng-disabled=“form.$invalid” type=“submit” value=“Create”>

Now, with this info, we can work with our Rails integration. We’ll have a config file to set up the model’s we want to integrate.

  1. First we define a file named afv.yml where we add the models to handle through AngularJS validation with this format:
development:
  models:
   - Contact
   - Post

Here we define the env, and the models names. Each model needs a slash before the name and a space to get all as an Array.

  1. This initializer load’s our config file, adds the necessary methods to our models:
module AngularValidation
  class << self
    attr_accessor :configuration
  end

  def self.configure
    self.configuration ||= Configuration.new
    yield(configuration)
  end

  class Configuration
    attr_accessor :models, :ng_application_name, :ng_controller_name

    def initialize
      @models               = []
      @ng_application_name  = 'angularFormValidator'
      @ng_controller_name   = 'angularFormValidatorCntl'
    end
  end
end

afv_config_file = File.join(Rails.root,'config','angular_validation_models.yml')
afv_config = YAML.load_file(afv_config_file)[Rails.env].symbolize_keys

AngularValidation.configure do |config|
  config.models               = afv_config[:models]
  config.ng_application_name  = afv_config[:ng_application] if afv_config[:ng_application]
  config.ng_controller_name   = afv_config[:ng_controller]  if afv_config[:ng_controller]
end


module ModelValidators
  module ClassMethods
    def validations
      validators.map{|v|
        {
          class: v.class.to_s.split('::').last,
          field_name: v.attributes.first,
          options: v.options
        }
      }
    end
  end
  def self.included(base)
    base.extend(ClassMethods)
  end
end

afv_config[:models].each do |model|
  model.constantize.send :include, ModelValidators
end
  1. We have some methods in our helper to use in our forms
def afv_javascript
    javascript_tag('angular.module("angularFormValidator", ["angularFormValidator.controllers"]);angular.module("angularFormValidator.controllers", []).controller("angularFormValidatorCntl", ["$scope", function($scope) {}]);')
  end
  def afv_form_submit(model)
    {'ng-disabled' => "#{afv_form_name(model)}.$invalid"}
  end
  def afv_form_name (model)
    "#{model.to_s.downcase}_form"
  end
  def afv_field_validators(model, field_name)
    options = {'ng-model' => "#{model.to_s.downcase}.#{field_name.to_s}"}
    field_validations = model.validations.select{|i| i[:field_name]==field_name}
    field_validations.each do |validation|
      case validation[:class]
        when 'PresenceValidator' then
          options['required'] = 'required'
        when 'FormatValidator' then
          options['ng-pattern'] = "/#{validation[:options][:with].source}/i"
      end
    end
    options
  end
  def afv_validate_form?(model)
    AngularValidation.configuration.models.include?(model.to_s)
  end
  def afv_ng_attributes(model)
    if afv_validate_form? model
      {
        'ng-app' => AngularValidation.configuration.ng_application_name,
        'ng-controller' => AngularValidation.configuration.ng_controller_name
      }
    else
      {}
    end
  end
  • afv_javascript => this method puts a small javascript to define our application and a controller.

  • afvformsubmit     | add the attribute to submit button to enable/disable it.

  • afvformname       | builds the form name to connect with the validations

  • afvfieldvalidators  | converts rails validators info in fields attributes

  • afvvalidateform?  | checks if the model is configured to velidates with AngualrJS

  • afvngattributes     | returns the ng-app and ng-controller for a form.

  • in our views we are going to call our helpers like this:

4.1) if we have a view for new, other for edit, etc. We are going to call our render line inside a content_tag to add the necessary attributes.

<%= content_tag :div, afv_ng_attributes(@contact.class) do %> <%= render ‘form’ %> <% end %>

Notice the @contact.class, this is to get the correct class for @contact, in this case our var it has an easy name to see what’s about, but if our var is @resource and the class is Contact, it’s easy to use @resource.class.

4.2) inside our form we need to do:

4.2.1) Adds the javascript for the app:

<%= afv_javascript %>

4.2.2) in our form definition we need to add the form name:

<%= form_for @contact, :html => {:name => afv_form_name(@contact.class)} do |f| %>

4.2.3) for each field we need to add the attributes to add angularjs verifications

<%= f.text_field :name, afv_field_validators(@contact.class, :name) %>

4.2.4) finally we need to add the form submit attributes:

<%= f.submit nil, afv_form_submit(@contact.class) %>

This is going to change our form to a code like this:

doss<div ng-app=“angularFormValidator” ng-controller=“angularFormValidatorCntl”>   <script> //<![CDATA[ angular.module(“angularFormValidator”, [“angularFormValidator.controllers”]);angular.module(“angularFormValidator.controllers”, []).controller(“angularFormValidatorCntl”, [“$scope”, function($scope) {}]); //]]> </script> <form accept-charset=“UTF-8” action=“/contacts” class=“new_contact” id=“new_contact” method=“post” name=“contact_form”><div style=“margin:0;padding:0;display:inline”><input name=“utf8” type=“hidden” value=“✓”><input name=“authenticity_token” type=“hidden” value=“3vjRTB1zLkRarvsfnD+zr78BHf2CAj+rfiTHGOhwUHQ=”></div>   <div class=“field”>     <label for=“contact_name”>Name</label><br>     <input id=“contact_name” name=“contact[name]” ng-model=“contact.name” required=“required” type=“text”>   </div>   <div class=“field”>     <label for=“contact_email”>Email</label><br>     <input id=“contact_email” name=“contact[email]” ng-model=“contact.email” ng-pattern=“/[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}/i” required=“required” type=“text”>   </div>   <div class=“field”>     <label for=“contact_message”>Message</label><br>     <textarea id=“contact_message” name=“contact[message]” ng-model=“contact.message”></textarea>   </div>   <div class=“actions”>     <input name=“commit” ng-disabled=“contact_form.$invalid” type=“submit” value=“Create Contact”>   </div> </form> </div>

I’m going to do a Gem with this , I’m finishing to put all kind of validators and searching for a way we shouldn’t need to add thing to the form syntax, etc For now, I think is a good point to understand how AngularJS works and use it with rails.

Example code: https://github.com/agustinvinao/blog-examples/tree/master/form-validation


Agustin Vinao
Agustin Vinao.

Paradox: Life is a mystery. Don't waste time trying to figure it out.
Humor: Keep a sense of humor, especially about yourself. It is a strength beyond all measure.
Change: Know that nothing stays the same.