Many agile teams treat 508 compliance as an afterthought. Teams typically build the product then retrofit it. If we want to build a modern website with accessibility in mind from day one, we need a better strategy. In general, we’d like to provide developers with tools that are 508 compliant from the beginning. But each form has its own set of unique accessibility constraints. By leveraging Rails’ unique templating library, we can build reusable components that have accessibility baked-in. In this post, we’ll show you how to use custom FormBuilders to generate reusable 508 compliant form elements.
Technical
To start with, we’ll create a very basic address form for our example. It consists of a handful of inputs and a set of radio buttons to denote the type of address. We’ll be showing both the Rails Form Builder ERB and the HTML that gets produced once that code is executed.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" } do |f| %>
<%= f.collection_radio_buttons :type, [['billing', 'Billing Address'] ,['mailing', 'Mailing Address']], :first, :last %>
<%= f.text_field :street %>
<%= f.text_field :city %>
<%= f.text_field :state %>
<%= f.text_field :zip %>
<% end %>
[/pcsh]
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<form class="new_address" id="new_address" action="/create" accept-charset="UTF-8" method="post" _lpchecked="1">
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="VVMX2CXRHSrKSO0Tu9Wpxn/OyAtxHPy7kb5pmfqzjUpxSkNHtxcMuSilrXsbK7NoLjf7nkUGr75qbOTambPJ6w==">
<input type="radio" value="billing" name="address[type]" id="address_type_billing"><label for="address_type_billing">Billing Address</label>
<input type="radio" value="mailing" name="address[type]" id="address_type_mailing"><label for="address_type_mailing">Mailing Address</label>
<input type="text" name="address[street]" id="address_street">
<input type="text" name="address[city]" id="address_city">
<input type="text" name="address[state]" id="address_state">
<input type="text" name="address[zip]" id="address_zip">
</form>
[/pcsh]
This code creates a functional bare bones form. However, from a 508 perspective, there are a number of things wrong here.
- All the inputs need labels
- Associated radio buttons require fieldsets
- Corresponding name/description for the related radio buttons should go in the legend
To address those shortcomings we could do something like the following:
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" } do |f| %>
<%= content_tag(:fieldset) do %>
<%= content_tag(:legend, 'Type') %>
<%= f.collection_radio_buttons :type, [['billing', 'Billing Address'] ,['mailing', 'Mailing Address']], :first, :last %>
<%= end %>
<%= f.label :street %>
<%= f.text_field :street %>
<%= f.label :city %>
<%= f.text_field :city %>
<%= f.label :state %>
<%= f.text_field :state %>
<%= f.label :zip %>
<%= f.text_field :zip %>
<% end %>
[/pcsh]
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<form class="new_address" id="new_address" action="/create" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="xShi7mTqSj7hrJzlOQnrY2a+V4UW25gautZ7KFrU2hvhMTZx9ixbrQNB3I2Z9/HNN0dkECLByx9BBPZrOdSeug==">
<fieldset>
<legend>Type</legend>
<input type="radio" value="billing" name="address[type]" id="address_type_billing"><label for="address_type_billing">Billing Address</label>
<input type="radio" value="mailing" name="address[type]" id="address_type_mailing"><label for="address_type_mailing">Mailing Address</label>
</fieldset>
<label for="address_street">Street</label>
<input type="text" name="address[street]" id="address_street">
<label for="address_city">City</label>
<input type="text" name="address[city]" id="address_city">
<label for="address_state">State</label>
<input type="text" name="address[state]" id="address_state">
<label for="address_zip">Zip</label>
<input type="text" name="address[zip]" id="address_zip">
</form>
[/pcsh]
We’ve made progress, but these modifications have introduced redundancy. Ideally, we want to use the simple form syntax from before and have Rails handle accessibility.
Luckily, we can extend the Rails form builder to do just that. Now developers won’t need to keep track of all the different nuances; it’s all built in.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# lib/forms/custom_builder.rb
class CustomFormBuilder < ActionView::Helpers::FormBuilder
end
[/pcsh]
Now we’re ready to include it in our form.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" }, builder: CustomFormBuilder do |f| %>
...
<% end %>
[/pcsh]
This allows us to make customizations in two places: the view and the form builder itself.
The separation provides us with a few benefits:
- The builder will take care of the HTML consistency and all the underlying 508 requirements
- The view will be used to tell the builder what to build
Take these two lines from views/address/new.html.erb.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<%= f.label :street %>
<%= f.text_field :street %>
[/pcsh]
508 requires all inputs to have labels describing them. Now, the FormBuilder can take care of this for us.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# lib/forms/my_form_builder.rb
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def text_field(method, options={})
label(:method)
super(method, options)
end
end
[/pcsh]
This allows us to make the input and label:
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<%= f.text_field :street %>
[/pcsh]
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<label for="address_street">Street</label>
<input type="text" name="address[street]" id="address_street">
[/pcsh]
We can also consolidate the code needed to give a collection_radio_button a fieldset by adding another method to our form builder:
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# lib/forms/custom_form_builder.rb
class CustomFormBuilder < ActionView::Helpers::FormBuilder
# ...
def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {})
@template.content_tag(:fieldset) do
input = @template.content_tag(:legend, method.to_s.titleize)
input << super(method, collection, value_method, text_method, options, html_options)
input
end
end
end
[/pcsh]
After both changes, our new form looks like this:
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" }, builder: CustomFormBuilder do |f| %>
<%= f.collection_radio_buttons :type, [['billing', 'Billing Address'] ,['mailing', 'Mailing Address']], :first, :last %>
<%= f.text_field :street %>
<%= f.text_field :city %>
<%= f.text_field :state %>
<%= f.text_field :zip %>
<% end %>
[/pcsh]
Because we have a single place we’re making changes to the inputs we can easily add more customizations. Suppose we wanted to add inline input errors for all fields. With our original form, we would have to do something like this:
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" } do |f| %>
<%= content_tag(:fieldset) do %>
<%= content_tag(:legend, 'Type') %>
<%= f.collection_radio_buttons :type, [['billing', 'Billing Address'] ,['mailing', 'Mailing Address']], :first, :last %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<%= end %>
<%= f.label :street %>
<%= f.text_field :street %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<%= f.label :city %>
<%= f.text_field :city %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<%= f.label :state %>
<%= f.text_field :state %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<%= f.label :zip %>
<%= f.text_field :zip %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<% end %>
[/pcsh]
<%= f.label :street do %>
<%= f.text_field :street %>
<%= content_tag(:span, f.errors[:method].first) if f.errors[:method].present? %>
<% end %>
# With errors
<label for="address_street">
<span class="label-text">Street</span>
<input type="text" name="address[street]" id="address_street">
<span>Street is required</span>
</label>
# lib/forms/custom_form_builder.rb
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def text_field(method, options={})
label(method) do
input = @template.content_tag(:span, method.to_s.titleize, class: 'label-text')
input << super(method, options)
input << @template.content_tag(:span, object.errors[:method].first, class: 'error-text') if object.errors[:method].present?
input
end
end
def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {})
@template.content_tag(:fieldset) do
input = @template.content_tag(:legend, method.to_s.titleize, class: 'legend-text')
input << super(method, collection, value_method, text_method, options, html_options)
input << @template.content_tag(:span, object.errors[:method].first, class: 'error-text') if object.errors[:method].present?
input
end
end
end
By making a couple of changes in one place, we can go back to the clean form and it will produce the same markup as above.
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
# views/address/new.html.erb
<%= form_for Address.new, url: { action: "create" }, builder: CustomFormBuilder do |f| %>
<%= f.collection_radio_buttons :type, [['billing', 'Billing Address'] ,['mailing', 'Mailing Address']], :first, :last %>
<%= f.text_field :street %>
<%= f.text_field :city %>
<%= f.text_field :state %>
<%= f.text_field :zip %>
<% end %>
[/pcsh]
[pcsh lang=”ruby” tab_size=”4″ message=”” hl_lines=”” provider=”manual”]
<form class="new_address" id="new_address" action="/create" accept-charset="UTF-8" method="post" _lpchecked="1">
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="tRSBbSviPWZf8aoDGDSgvgmdOtQgJ2XgvyqMzPTOyEkvsbUgseGMXrV2km1T8e/9voK3r0r2CcInpbWFhPbKoA==">
<fieldset>
<legend class="legend-text">Type</legend>
<input type="radio" value="billing" name="address[type]" id="address_type_billing"><label for="address_type_billing">Billing Address</label>
<input type="radio" value="mailing" name="address[type]" id="address_type_mailing"><label for="address_type_mailing">Mailing Address</label>
</fieldset>
# This input has an error
<label for="address_street">
<span class="label-text">Street</span>
<input type="text" name="address[street]" id="address_street">
<span>Street is required</span>
</label>
<label for="address_city">
<span class="label-text">City</span>
<input type="text" name="address[city]" id="address_city">
</label>
<label for="address_state">
<span class="label-text">State</span>
<input type="text" name="address[state]" id="address_state">
</label>
<label for="address_zip">
<span class="label-text">Zip</span>
<input type="text" name="address[zip]" id="address_zip">
</label>
</form>
[/pcsh]
Conclusion
The purpose of this article was to start Agile teams thinking about 508 compliance from the beginning and to outline some helpful practices for making compliance easy and natural. Once again, by acknowledging 508 early, teams should be able to avoid any long overdue facelifts in order to make a site 508 compliant. Furthermore, outside of 508 compliance, well-maintained code libraries and central components can decrease the potential for developer errors, maintain a consistent look and feel, and DRY up the code base.
Additional Resources
- Testing Custom Form Builders
- Ideas on setting default builder to your custom FormBuilder
- Parsley – Excellent JS form validation