Valibot!
Automatic field validation for forms backed by DataMapper models through Sinatra.
What it requires:
What it provides:
- full automatic field validation on default bind events
- field value matching confirmation
- extensible through callbacks used to build, place, remove, or transform error tags
- Valibot#all method to do the whole thing at once.
- automatic 'onsubmit' binding
To use (brief):
- add this gem to your Gemfile, bundle install
- add the javascript to your site
- add the app to your config.ru (defaults to '/_valibot')
- create Valibots for the models on forms where you need them
To use (details):
to add the javascript to your site, there are two methods:
Valibot has a route at '/valibot.js' that will deliver the source. This is the preferred method:
<script src="/_valibot/valibot.js"></script>
Valibot can add a helper to Sinatra::Base called 'valibot_js'. call this in a script template like so:
<%= valibot_js %>
They deliver the exact same content, but the route adds Cache-Control headers so layers like Varnish or Rack::Cache can do their work. In order to use the helper, you need to register it in your app:
class App < Sinatra::Base
register Valibot::Helpers
get('/'){ erb "<script><%= valibot_js %></script>" }
end
the sinatra app needs a place to run at in your Rack config. something like the following should suffice:
map '/_valibot' do
run Valibot::App
end
note that you can change the path it runs at, but you must pass that path as a value to the Valibot javascript constructor using the key pathPrefix
(see below).
to create the Valibots, call the constructor inside $(document).ready():
$(document).ready(function(){ new Valibot({ modelName: 'user' }); });
Constructor options:
modelName : string required if no modelNames
name of the model to validate against, in snake_case. in modern Rack-based webapps, the convention for form field names is to follow a model_name[field_name]
pattern. Rack translates these names into the params hash and it is very common for the controller to pass this hash to a model constructor like ModelName.new params['model_name']
. your form should follow this convention for Valibot to work, as it will use jQuery to select all input
, textarea
, and select
tags from the DOM that have names that start with this value. ex:
<input type="text" name="user[first_name]"/>
<input type="text" name="user[email]"/>
...
new Valibot({ modelName: 'user' });
modelNames : array of strings required if no modelName
names of the models to validate against, in snake_case. this behaves exactly like modelName
but allows multiple models to be validated in the same form. ex:
<input type="text" name="user[first_name]"/>
<input type="text" name="profile[bio]"/>
...
new Valibot({ modelNames: ['user', 'profile'] });
note that the first invalid field of the first model specified will be focus()
ed if the form fails validation on submit.
pathPrefix : string
path you have Valibot racked up at. ex:
new Valibot({ modelName: 'user', pathPrefix: 'dogs_and_cats' });
errorTagBuilder : function(text, self, minion)
a callback for building the tag you want to put in the DOM and show the user with the error message from DataMapper in it. the default builder will create an <em>
element, set its class to 'error', innerHTML text
in, and return it.
parameters:
text
- error message from DataMapperself
- reference to the Valibot objectminion
- reference to the Valitron or Confirm-o-mat object ######returns:- an element with the
text
in it suitable for placing in to the DOM.
errorTagPlacer : function(tag, errorTag, self, minion)
a callback for placing the element returned from the builder into the DOM.
parameters:
tag
- the field element whose value is being validatederrorTag
- the element built and returned by theerrorTagBuilder
function of this Valibotself
- reference to the Valibot objectminion
- reference to the Valitron or Confirm-o-mat object
errorTagRemover : function(errorTag, self, minion)
a callback for removing the element from the DOM in the event of no error found.
parameters:
errorTag
- the element in the DOM that holds the error message for this fieldself
- reference to the Valibot objectminion
- reference to the Valitron or Confirm-o-mat object
errorTagTransformer : function(text, errorTag, self, minion)
a callback for transforming the element in the DOM in the event of a new error message replacing an old one.
parameters:
text
- error message from DataMappererrorTag
- the element in the DOM that holds the error message for this fieldself
- reference to the Valibot objectminion
- reference to the Valitron or Confirm-o-mat object
include : array or map { modelName : array }
by default, Valibot will attach Valitrons to elements whose name attribute matches a specific regular expression, after jQuery selection. you can force fields whose name attributes don't match the regex to be checked by Valibot by putting the field name in this array. i'm not actually sure when this would be needed. :)
exclude : array or map { modelName : array }
you can keep Valibot from attaching Valitrons to certain elements by putting their field names in this array. ex:
new Valibot({ modelName: 'user', exclude: ['middle_name'] });
new Valibot({ modelNames: ['user', 'profile'], exclude: {'user': ['middle_name'], 'profile': ['icon']});
bindEvents : map { fieldName : event }
Valibot has a fairly intuitive default bind event for each input type: change
for checkboxes, radio buttons, and select drop-downs; blur
for everything else. you can override these defaults on a per-field basis by passing them in this hash. ex:
new Valibot({ modelName: 'user', bindEvents: {'middle_name':'keyup'} });
context : string
DataMapper has contextual validations built-in. if you need to validate against a certain context, pass it in here. ex:
new Valibot({ modelName: 'user', context: 'signup' });
confirm : array of maps { id : things }
often on a form, one will have an 'extra' field that is not part of the data model, but needs to match another field in value. usually, these are passwords or email addresses. in this case, if you have given the actual model field element an id of 'password', the confirmation field id should be 'confirm_password'. then you can tell Valibot about this by passing this array of hashes and it will attach a Confirm-o-mat to each. things
is the word that the Confirm-o-mat will use in its error message about the things not matching. ex:
new Valibot({ modelName: 'user', confirm: [{'password', 'Passwords'}] });
this would attach a Confirm-o-mat to $('#confirm_password')
that checks its value against $('#password').val()
and will build an errorTag with the text, "Passwords do not match."
agreements : array of maps { id : things }
often on a form, one will have a checkbox that is not part of the data model, but needs to checked as a legally binding agreement. usually, these are rules or term and conditions, etc. in this case, simply give the input a unique id. then you can tell Valibot about it by passing this array of hashes and it will attach an Agree-Pee-Oh to each. things
is the word that the Agree-Pee-Oh will use in its error message about the things not being agreed to. ex:
new Valibot({ modelName: 'user', agreements: [{'rules', 'Rules'}] });
this would attach an Agree-Pee-Oh to $('#rules')
that verified it's checked and will build an errorTag with the text, "You must agree to the Rules." if it is not.
fancy : boolean
Valibot has two built-in types of errorTag(Placer|Remover|Transformer) functions. the default is very bland and simply inserts, replaces, or removes the errorTag element. the fancy option uses jQuery animation to fadeIn and fadeOut the errorTag element. if this is true
, and you also feed it your own errorTag callbacks, those will be used instead. ex:
new Valibot({ modelName: 'user', fancy: true, errorTagPlacer: myETPlacer });
this would utilize the "fancy" Remover and Transformer built-in functions, but call myETPlacer()
for placement. note that the _fancyErrorTagTransformer
function actually calls whatever Placer function is set in the containing Valibot; in this case, myETPlacer
.
bindOnSubmit : boolean
Valibot automatically binds it's own onSubmit()
function to the first form that it finds model fields in. this function will run through any unchecked or invalid 'trons or 'mats and call their checkValue()
functions. once that is complete, if all of them are valid, it submits the form; otherwise, error messages are built/placed like normal, and the first invalid field is focus()ed. this flag allows you to turn off this functionality, for cases where you need to do your own onsubmit dance. note that the Valibot's onsubmit can be called in this situation anyway; but, be careful because it will submit the form if everything is valid. ex:
var valibot = new Valibot({ modelName: 'user', bindOnSubmit: false });
$(valibot.form).bind('submit', function(event) {
event.preventDefault();
// do something clever
valibot.onSubmit();
});
submitForm : boolean
sometimes, you just want to submit the form yourself. most of the time, you want to do it all AJAX fancypants. fine, just pass this in as false
and Valibot won't try to submit the form. it will still call onSubmitCallback
so you probably want to put your XHRs in there.
onSubmitCallback : function(valibot)
set this to a function you want to run when the form would be submitted. if you set submitForm
to false, this function will be called, but the form WILL NOT be submitted. use these two together to get Valibot validation on forms you want to submit XHR-style. ex:
var valibot = new Valibot({ modelName: 'foo', submitForm: false, onSubmitCallback: function(valibot) {
$.post(valibot.form.action, $(valibot.form).serialize(), function(response){ /* ... /* });
}});
appUrl : string
perhaps you want to run Valibot at a different URL completely from the one that the form your validating loaded from. fine, just tell Valibot about it with this. note that this puts Valibot into JSONP mode and all validation requests turn from POSTs into GETs. ex:
new Valibot({ modelName: 'user', appUrl: 'http://some.other.domain/'});
this would cause all validation requests to go to 'http://some.other.domain/_valibot/:model/...'
dependents : map of arrays { field : [ otherField, ... ] }
sometimes you need to validate a field based off of the value of another field. you can tell Valibot about this with this option, and it will automatically include the value(s) of the other field(s) in the validation. ex:
new Valibot({ modelName: 'user', dependents: {'last_name': ['first_name']} });
this would cause Valibot to POST the value of the field named 'user[first_name]' with the check request for 'last_name'. your model should be configured to validate last_name
based off of the value of first_name
:
validates_with_block :last_name do
if self.first_name == 'a' and self.last_name == 'b'
[false, "can't have b with a."]
else
true
end
end
note that in the above example, first_name
would still be validated on it's own.
Next Steps (TODOS):
- automated testing (!!)
- array parameters
- profit!