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| %>