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:

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 <strong>opens_at:</strong>time <strong>closes_at:</strong>time <strong>weekday:</strong>integer <strong>service:</strong>references

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

  • <em>class</em> Schedule < ApplicationRecord
    belongs_to :service
    <em>end</em>

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

  • <em>class</em> Service < ApplicationRecord has_many :schedules
    <strong> accepts_nested_attributes_for :schedules, allow_destroy: true</strong><em> def</em> open_weekends?
    regular_schedules.exists?(weekday: [6,7])
    <em>end</em><em> def</em> open_after_six?
    regular_schedules.where("closes_at > ?", Time.parse("18:00")).exists<em>?
    end</em>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<em> # your controller actions here....</em> private def service_params
    params.require(:service).permit(
    name,
    description,<em> # other service parameters here....</em><strong> schedules_attributes: [
    :id,
    :opens_at,
    :closes_at,
    :weekday,
    :_destroy,
    ]</strong>
    )
    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:

  • <em>module</em> ScheduleHelper
    <em>def</em> 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},
    ]
    <em>end
    end</em>

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

  • <%= form_for @service <em>do</em> |s| %><table <em>class</em>="schedule-editor">
    <thead>
    <tr>
    <th>Day</th>
    <th>Opens at</th>
    <th>Closes at</th>
    </tr>
    </thead>
    <tbody> <% weekdays.each <em>do</em> |day| %> <%= s.fields_for :regular_schedules, s.object.regular_schedules.find_or_initialize_by(weekday: day[:value]) <em>do</em> |sched| %> <tr> <td>
    <%= sched.hidden_field :weekday %>
    <div <em>class</em>="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> <% <em>end</em> %> <% <em>end</em> %> </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”:

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

    checkboxes.forEach(checkbox <em>=></em> {
    update(checkbox)
    checkbox.addEventListener("click", () <em>=></em> {
    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.

Get in touch

We’re always happy to answer any questions you have about FutureGov and discuss how we can work together.

Contact us