Err Supply is a Rails view helper that produces simple, beautiful error messages.
The helper unpacks and rekeys the standard Rails error hash to make applying error messages to your views dead simple. Even better, because the extension cures Rails' brain-damaged way of recording errors from nested resources/attributes, it works with both simple and complex forms.
Err Supply is designed to make the default Rails error reporting structure more useful for complex interfaces.
We started thinking about Err Supply when we worked on a project that required an AJAX form submission.
For normal HTML submissions, we coded up a custom error handler in Rails to display the messages below the form input. To replicate this for the AJAX submission, we realized we would have to convert the error hash to JSON and then wire up a jQuery event handler to perform the same DOM manipulations Rails was performing internally. Obviously, we weren't super excited about having code in two places and in two languages to provide the same business value.
The problem was compounded a few months later when we worked on a different project that required an AJAX form submission for a nested form.
Here, even the workaround is problematic. Because Rails reports errors on nested attributes ambiguously, there really wasn't any way to use a javascript workaround without first reconstituting the error hash itself.
If you don't know what I mean by saying the error messages are ambiguous, here's an example. Say you have these models defined:
class Father < ActiveRecord::Base
attr_accessible :name
attr_accessible :age
has_many :sons
accepts_nested_attributes_for :sons, :allow_destroy => true
validates_presence_of :name, :age
end
class Son < ActiveRecord::Base
attr_accessible :name
attr_accessible :age
belongs_to :father
validates_presence_of :name, :age
end
If you pull up the nested edit form for a father with two sons and delete one son's name and the other son's age, Rails will return the following error hash:
{
"sons.name": ["can't be blank"],
"sons.age": ["can't be blank"]
}
Umm, thanks, but which son is missing a name and which one is missing an age? Or is it the same son missing both values? Or, do they both have problems?
Err Supply converts the Rails error hash from a slightly ambiguous object graph to a flat, unambiguous hash of DOM element ids. It does this by traversing the object graph for you and determining exactly which child resources have errors. It then adds those errors to a new hash object where the key is the DOM id of the corresponding form input.
Err Supply publishes this newly constituted error hash via a custom jQuery event, allowing the view to handle errors through a single javascript interface. This strategy allows errors from HTML and AJAX form submissions to be run through a single piece of code.
Install me from RubyGems.org by adding a gem dependency to your Gemfile. Bundler does the rest.
gem 'err_supply'
Add the appropriate javascript file to the asset pipeline. Err Supply comes with an adapter for Twitter Bootstrap enabled sites. Those using other frameworks will need to write a separate adapter based on their framework's interface.
#= require err_supply-bootstrap
Add the appropriate stylesheet file to the asset pipeline. Err Supply comes with style classes for Twitter Bootstrap enabled sites. Those using other frameworks will need to write a separate set of style classes based on their framework's interface.
@import 'err_supply-bootstrap';
The main err_supply
helper returns an escaped javascript invocation that triggers a custom
event named err_supply:loaded
and supplies the edited error hash as data.
<script type="text/javascript">
<%= err_supply @father %>
</script>
This will evaluate @father.errors and apply an errors to the form. It assumes all form inputs are named in the standard rails way and that all error attribute keys match the form input keys exactly.
Attributes can be whitelisted or blacklisted using the standard :only
and :except
notation.
Because err_supply
will ignore any unmatched attributes, such declarations are not strictly
required. They typically only make sense for minor actions against models with many,
many attributes.
Say a User class has an attribute :born_on
to store the user's date of birth. In your form
builder you declare the textbox normally like so:
<%= f.text_field :born_on %>
A presence_of error will be formatted as:
Born on can't be blank.
To make this nicer, we can change the label for the attribute like this:
<script type="text/javascript">
<%= err_supply @user, :born_on => { :label => "Date of birth" } %>
</script>
This will attach the following presence of error to the :born_on field:
Date of birth can't be blank.
Say a User class belongs to an Organization class. In your form, you declare a selector
for assigning the organization. The selector is named :ogranization_id
.
Depending on how your validations are written, you might very well get an error message for
this form keyed to :organization
. Because your selector is keyed to :organization_id
,
the default javascript handler will consider this an unmatched attribute.
You can solve this problem by changing the key for the attribute like so:
<script type="text/javascript">
<%= err_supply @user, :organization => { :key => :organization_id } %>
</script>
You can apply the same principles to nested attributes by nesting the instructions. To return to our father/son example, you can change the name labels for both entities using the following notation:
<script type="text/javascript">
<%= err_supply @father,
:name => { :label => "Father's name" },
:sons => {
:name => { :label => "Son's name" }
}
%>
</script>
Attribute instructions are provided as hashes so that both key
and label
changes can be
declared on the same attribute.
Honestly, such instructions are rare in the real world, but error handling can get weird fast, so the library supports it.
There are a handful of scenarios that fall outside the area of basic usage worth discussing.
When Rails submits a nested form via a full page refresh, Rails automatically re-indexes any DOM elements in a nested collection starting at 0.
If you're submitting a form remotely, it's on you to do this by re-rendering the nested fields with the current collection.
As long as you do this before the err_supply
call is made, everything will work normally.
If you don't use jQuery, you may want to override the err_supply
helper to format the
javascript invocation differently.
The library is constructed so the main public method named err_supply
is pretty dumb. Most
of the real hash-altering magic occurs in a protected method called err_supply_hash
. You can
override the much simpler method without worrying about damaging the core hash interpretation
functionality.
See the lib
directory for details.
The default javascript handler bundles up unmatched error keys into a new hash and publishes them
using the custom jQuery event err_supply:unmatched
.
- Ruby on Rails: http://rubyonrails.org
- jQuery: http://jquery.com
- Repository: http://github.com/coroutine/err_supply
- Gem: http://rubygems.org/gems/err_supply
- Authors: http://coroutine.com
Other gems by Coroutine include:
- acts_as_current
- acts_as_label
- acts_as_list_with_sti_support
- delayed_form_observer
- kenny_dialoggins
- michael_hintbuble
- tiny_navigation
Copyright (c) 2011 Coroutine LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.