Skip to content

lumoslabs/comply

Repository files navigation

Comply

Server-side ActiveRecord validations, all up in your HTML and client-side javascript.

Intention and Purpose

Rendering the same page over and over due to validations is a slow and unpleasant user experience. The logical alternative is, of course, validating the form on the page, before it is submitted.

Though it's better, doing that still requires duplicating your validations on the client and server. Any time you change a validation in one place, it must be changed in the other as well. In addition, some validations require hitting the server and database, for example validating the uniqueness of an email address.

The solution offered here is combine the server-side validations with inline error messages on the form. This is done by creating an API for validating your objects. When triggered, the form is serialized and sent to the API endpoint, where it is instantiated as your ActiveRecord model, and validations are run. The API returns the instance's error messages, which the form uses to determine if the described object is valid.

Basic Usage

Include Comply in your gemfile:

gem 'comply'

Mount the engine in your routes.rb:

mount Comply::Engine => '/comply'

Require the javascript files & dependencies in the form's corresponding javascript file (requires Asset Pipeline):

//= require jquery
//= require comply

In your form: Add the data-validate-model tag to your form tag with the name of the model you intend to validate:

<%= form_for @movie, html: { data: { validate_model: 'movie' } } do |f| %>

On the input tags you want validated, add the data-validate tag:

<div class="field">
  <%= f.label :title %><br>
  <%= f.text_field :title, data: { validate: true } %>
</div>
<div class="field">
  <%= f.label :description %><br>
  <%= f.text_area :description, data: { validate: true } %>
</div>

Of course, this all assumes you have ActiveRecord validations set up in your model:

class Movie < ActiveRecord::Base
  validates :title, presence: true, uniqueness: true
  validates :desciption, presence: true
end

Customizations

You can control the behavior of your validatable forms and inputs in a variety of ways.

Validation route

You can specify the route to be used for validation on the form by setting the data-validate-route attribute.

<%= form_for @movie, html: { data: { validate_model: 'movie', validate_route: custom_validation_path } } do |f| %>

Success messages

Want success messages to add some personality to your forms? Add the message as the data-validate-success attribute.

<%= f.text_field :title, data: { validate: true, validate_success: "Sweet title yo!" } %>

Validation contexts

Supplementary reading on validation contexts:

By default, all validations run through Comply's Comply::ValidationsController will be in the :comply validation context. This gives you the option to modify how your application runs its validations. The most common way to use validation contexts, is when you want to skip a certain validation. Let's say we didn't want to check the uniqueness of a particular attribute when asking Comply to validate a model. You would do this:

class SomeModel < ActiveRecord::Base
  validates :my_attribute, uniqueness: true, unless: -> { validation_context == :comply }
  # or perhaps you *ONLY* want to run the validation when in the :comply context
  validates :other_attribute, numericality: true, on: :comply
end

You now have the option to specify your own context if you inherit from Comply::ValidationsController if your model has a specific business case for needing to run/not-run a set of validations for a given context.

class SomeController < Comply::ValidationsController
  # ... snip
  protected

  def validation_context
    :my_new_context
  end
end

class SomeModel
  validates :my_attribute, uniqueness: true, unless: -> { validation_context == :my_new_context }
end

That's it!

Event triggers

You can change the jQuery event which triggers the input validation with the data-validate-event attribute.

<%= f.text_field :description, data: { validate: true, validate_event: 'click' } %>

Note: the default event is input keyup.

Timeouts

You can delay validation by setting the data-validate-timeout attribute. This is great for things like checking a string's format so that it won't validate until the user has had a chance to finish typing.

<%= f.text_field :description, data: { validate: true, validate_timeout: 1000 } %>

Note: the amount is in milliseconds, and the default amount is 500 milliseconds.

Validation dependencies

Sometimes you have two fields that you want to validate at the same time. You can ensure they will be serialized and validated together using the data-validate-with attribute, which takes the jQuery selector of the dependent object. This can be useful for things like confirmation fields.

<%= f.label :password %>
<%= f.password_field :password, class: "span6" %>

<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, data: { validate: true, validate_with: '#user_password' } %>

Multiparameter attributes

Want to validate multiple fields as one like a multiparameter attribute? Add the data-multiparam attribute to your inputs. This is great for multivalue fields like dates.

<%= f.date_select :release_date, {}, data: { validate: true, multiparam: 'release_date' } %>

Forcing validations on multiparameters

Normally, a multiparameter input won't validate until all of its fields have been completed. If you want to override that, you can set the data-validate-force attribute. This is good if you don't want to represent your multiparameter fields as one unit.

<%= f.date_select :release_date, { order: [:month, :day] }, data: { validate: true, multiparam: 'release_date' } %>
<%= f.text_field :'release_date(1i)', id: 'release_date_1i', data: { validate: true, multiparam: 'release_date', validate_force: true } %>

Customizing the ValidationMessage class

If you don't want to use the default Comply.ValidationMessage, which is responsible for putting the validation message tag on the page and handling the display of messages, great news: you can overwrite it!

After you've included Comply in the Asset Pipeline, feel free to extend it! For example (in foo.js.coffee):

class Comply.ValidationMessage extends Comply.BaseValidationMessage
  constructor: (@$el) ->
    super
    console.log 'We can build him. We have the technology.'

  successMessage: (message = '') ->
    super
    console.log 'All systems go.'

  errorMessage: (message = '') ->
    super
    console.log 'Houston, we have a problem.'

  resetMessage: (message) ->
    super
    console.log 'Mulligan!'

Mounting the engine elsewhere

If you would like to mount the engine under a different namespace, all you need to do is add the engine path to the javascript object:

# routes.rb
mount Comply::Engine => '/joanie_loves_chachi'
//= require comply

Comply.enginePath = 'joanie_loves_chachi';

Customizing Validation Behavior

You can override the validation behavior by inheriting from Comply::ValidationsController. This can be useful for cases like forms which update specific attributes of an instance.

For example, if you have a "change email" form, and you want to validate the email's uniqueness against the current user, you can override the validation_instance method on Comply::ValidationsController, set corresponding routes, and add the data-validate-route attribute to your form.

# my_validations_controller.rb
class MyValidationsController < Comply::ValidationsController
  def validation_instance
    current_user.attributes = @fields
    current_user
  end
end

# routes.rb
match 'validations' => 'my_validations#show', only: :show, as: :my_validation
<%= form_for @movie, html: { data: { validate_model: 'movie', validate_route: my_validation_path } } do |f| %>