How to add an accessible opening times feature to a Rails app

Jaye Hackett
FutureGov
Published in
5 min readJun 8, 2020

--

A common problem in directory-style apps is representing the opening hours of a business or service. It’s easy to provide a simple text input and move on. But, if we want to power a Google Maps-style “open now” feature, we need to use a more structured format.

In this example, we‘re representing the time people can access local community support in one of our service directory products. To boost usability, we wanted the editor to resemble the opening hours sign you might see in a shop:

A tabular interface with days of the week along the left hand column, opening and closing times in each row
The finished editing interface we’ll create.

We’ll cover:

  • the models and methods to define and make use of the opening hours, using nested attributes
  • the views and controllers to edit opening hours through an accessible, semantic form, using fields_for
  • how to progressively enhance the form with JavaScript

Models

There are two relevant models: Service and Schedule. They have a one-to-many relationship. A schedule represents the opening and closing times on a single day of the week. And services can have between zero (always closed) and seven (open every day) of them.

You can generate Schedules using a terminal command like:

rails g model Schedule opens_at:time closes_at:time weekday:integer service:references

The models/schedule.rb file should be correct out of the box:

class Schedule < ApplicationRecord
belongs_to :service
end

And our models/service.rb file looks like this:

class Service < ApplicationRecord    has_many :schedules
accepts_nested_attributes_for :schedules, allow_destroy: true
def open_weekends?
schedules.exists?(weekday: [6,7])
end
def open_after_six?
schedules.where("closes_at > ?", Time.parse("18:00")).exists?
end
end

We use accepts_nested_attributes_for so we can modify opening times from the same form we edit the service with. Providing the allow_destroy option means we can remove schedules using that same form.

We’ve also defined some methods, open_weekends? and open_after_six?, which you could use on the front-end to display services scheduled to be open at different times in the evenings or weekends.You could do something similar to show services open right now.

We could make these even more useful by turning them into scopes.

Controllers and views

We’ll be editing opening times from the same form we edit the service in.

This means controllers/services_controller.rb is the file we’ll need to modify.

Assuming we’re using strong params, edit your controller:

class ServicesController < ApplicationController    # your controller actions here....    private    def service_params
params.require(:service).permit(
name,
description,
# other service parameters here.... schedules_attributes: [
:id,
:opens_at,
:closes_at,
:weekday,
:_destroy,
]

)
end
end

The nested schedules_attributes array is the important part. We’re permitting the fields we’ve spoken about, plus _destroy, a special rails-defined attribute that will delete our object if it’s set to something truthy.

We chose to store an array of days of the week and their associated values in a helper. This will be useful in making our form.

You could do the same in helpers/schedule_helper.rb:

module ScheduleHelper
def weekdays
[
{label: "Monday", value: 1},
{label: "Tuesday", value: 2},
{label: "Wednesday", value: 3},
{label: "Thursday", value: 4},
{label: "Friday", value: 5},
{label: "Saturday", value: 6},
{label: "Sunday", value: 7},
]
end
end

In your views, you can now create a form like this:

<%= form_for @service do |s| %><table class="schedule-editor">
<thead>
<tr>
<th>Day</th>
<th>Opens at</th>
<th>Closes at</th>
</tr>
</thead>
<tbody>
<% weekdays.each do |day| %> <%= s.fields_for :schedules, s.object.schedules.find_or_initialize_by(weekday: day[:value]) do |sched| %> <tr> <td>
<%= sched.hidden_field :weekday %>
<div class="checkbox">
<%= sched.check_box :_destroy, {checked: sched.object.persisted?}, "0", "1" %>
<%= sched.label :_destroy, day[:label] %>
</div>
</td>
<td>
<%= sched.label :opens_at, class: "visually-hidden" %>
<%= sched.time_field :opens_at %>
</td>
<td>
<%= sched.label :closes_at, class: "visually-hidden" %>
<%= sched.time_field :closes_at %>
</td>
</tr> <% end %> <% end %> </tbody>
</table>
<% end %>

Let’s walk through it:

  1. First, we make a table. HTML tables are out of fashion, but when you’re displaying tabular data, they’re still the correct semantic choice. Don’t be afraid of them.
  2. Inside the <tbody/>, we loop through the days in our weekday helper from earlier.
  3. Inside each iteration, we use fields_for to allow us to edit nested objects.
  4. The .find_or_initialize_by() on this line is important, because it checks whether a schedule already exists for the given day, and builds a fresh one for us to edit if not.
  5. Inside the fields_for block, we provide the current weekday as a hidden field.
  6. We use a checkbox to set the _destroy attribute we spoke about earlier. We will check the box initially if the schedule object has been saved to the database.
  7. Crucially, the values are flipped. It sends a falsy “0” value when it’s checked and a truthy “1” value when it’s not. Don’t forget this part!
  8. The opens_at and closes_at form inputs use the HTML input type of “time”, which has pretty good support in everything but Internet Explorer. If you still need to support IE, you could use a polyfill or ask the user to enter the time in a text field and validate it on the server.
  9. We’ll also use CSS to visually hide their labels (the column header is adequate labelling for a sighted user), but keep them readable for screen readers.

Progressively enhancing with JavaScript

Now we have a form that lets us create and edit opening times and the ability to query them in the front-end of our app.

We can improve the user experience a little with vanilla JavaScript. Something like this will disable the time inputs if the associated checkbox isn’t turned “on”:

const editor = document.querySelector(".schedule-editor")const update = checkbox => {
const inputs = checkbox.parentNode.parentNode.parentNode.querySelectorAll("input[type='time']")
if(checkbox.checked){
inputs.forEach(input => input.removeAttribute("disabled"))
} else {
inputs.forEach(input => input.setAttribute("disabled", "true"))
}
}
if(editor){ let checkboxes = editor.querySelectorAll("input[type='checkbox']")

checkboxes.forEach(checkbox => {
update(checkbox)
checkbox.addEventListener("click", () => {
update(checkbox)
})
})
}

With a little CSS to grey out the disabled inputs, this could prevent users being frustrated by accidentally typing times into an “off” day.

And that’s it! There are bits that could be refactored, but this is a quick, usable solution that should be production-ready.

Further reading

  • OpenReferral UK has lots of good guidance for data describing local community services.

Read more about our tech work. If you’d like to chat about how we might support your organisation, please get in touch.

--

--

Jaye Hackett
FutureGov

Strategic designer & technologist. Why use three short words when one long weird one will do? jayehackett.com