Member preview

Refactoring views with Ruby on Rails’ ActiveSupport helpers

You have some great support when running on Rails. Original photo by Rucksack Magazine on Unsplash

Ruby on Rails (strictly speaking, the ActiveSupport gem which is installed as a prerequisite for all Rails projects, but which can be installed and used independently of Rails) has some pretty useful features built in.

One such feature is something that this week helped me rip out some tortuous code that was introducing a subtle HTML rendering bug. It’s something that only surfaced when we sent affected emails to specific mail clients, but it’s always nice to stop bugs occurring, however few people may have been affected.

The scenario

Each of our events can have many sponsors. There are multiple levels of sponsorship available, each of which is represented in the database as a numeric value where 1 is the highest level of sponsorship, followed by level 2, level 3, and so on. For simplicity’s sake, we’ll focus on levels up to 4 in this worked example.

At the end of emails concerning a sponsored event, a series of sponsor logos is included. The rules for implementing this are:

  • Each level of sponsorship is separated from the level below by a separating rule
  • The maximum number of sponsor logos per line is the same as the level number — 1 per row for level 1, 2 per row for level 2, etc. up to level 4. From level 5 upwards (if we ever get that many), all sponsors should be rendered 6 to a row.
  • If the final row of a sponsorship level isn’t “full”, then the remaining items should be spaced out in a balanced manner.
  • Everything should be implemented using tables for layout because HTML email clients don’t reliably implement CSS.

Assuming that the logo image size for each sponsor has been prepared separately, an example layout meeting these rules would look something like this (with actual logos replaced by cute dogs, because why not):

The old, buggy code

Before it gets to the view, the event’s sponsors are massaged into a structure which is given the name sponsors_by_level. This contains an array of SponsorLevel objects, each of which has a #level attribute (the level number, starting at 1) and a #sponsors collection, comprising the sponsor images for each level.

In HAML, the code originally looked like this:

While the amount of calculation being done within the view itself is obviously a code smell, it does at least allow us to follow the logic here. Some key points:

  • no_of_sponsors_per_line (line 3) says that any sponsorship level 5 or before should have 6 items per row.
  • no_of_full_line_sponsors (line 5) takes advantage of Ruby’s integer arithmetic: dividing by the number of items per row returns the number of whole rows, and multiplying by the same amount then returns the number of items in those rows. (There’s a side bug here, in that we should be using no_of_sponsors_per_line and notlevel, but that only crops up if we hit level 5 or above so let’s ignore that for now.)

We then loop through all the sponsors. The table is built on a 12-column grid, so the colspan for each <td> element is determined easily enough for full rows; for the last row of each group, we do some further calculation to split the 12 columns evenly.

The intention is that whenever we hit the first element in a row, we output a <tr> element to start a new row.

Unfortunately, because of the way HAML works, our output isn’t quite the structure we probably want. The intention is to create something like:

<tr>
<td><img src=“…”></td>
<td><img src=“…”></td>
</tr>
<tr>
<!-- and so on -->

But the HAML %tr on its own (on line 9) produces an empty element:

<tr></tr>
<td><img src=“…”></td>
<td><img src=“…”></td>
<tr></tr>
<!-- and so on -->

What made this easy to miss for so long is that web browsers are terribly permissive when it comes to HTML content. Somehow this structure renders just fine — until it hits some email clients.

On those clients the invalid table structure, with <td> elements placed as direct children of a <table> element with no <tr> in between, causes all sorts of weirdness.

Not in all web clients, though — it seems that many of them parse the table in the same way as browsers do. That hopefully means that the number of people affected by the original bug would have been small.

But from a developer point of view, the code is still tricky. Getting your head round what is going on takes longer than it should, and keeping the code in its current form would make fixing the hierarchy bug much harder.

The replacement: in_groups_of

The main problem the above code tries to fix is: split a list of arbitrary size into a collection of lists of the same size, and deal with any remainder nicely.

Thankfully, in_groups_of is designed to scratch that itch. From the official documentation:

Splits or iterates over the array in groups of size number, padding any remaining slots with fill_with unless it is false.

In our case, we don’t want to pad out any remaining slots: if there are 3 items left over when dividing an array into groups of 4, we just want a collection of 3. As that short description above states, the false argument value gives us what we need. Again from the documentation:

%w(1 2 3 4 5).in_groups_of(2, false) { |group| p group }
[“1”, “2”],
[“3”, “4”],
[“5”]

With that in mind, we can offload most of the calculation work to in_groups_of. We can also move the number of items per table row calculation to our level object (not shown) as SponsorLevel#sponsor_per_line which results in a much more straightforward snippet of HAML:

As you can see, each table row can now be implemented as a simple loop through the group subset of items. Within each row, all we need to worry about is the number of items in group (group.size) because the false value passed to in_groups_of ensured that we won’t have any empty values.

Conclusion

Because Rails was extracted out of a working web application, it already deals with a lot of existing requirements for manipulating a data set into an appropriate shape for display on the web.

But you do have to know that the methods are there. Looking through the documentation is essential, although I’d recommend an app like Dash to best navigate through the official docs.

If you’re using Rails, everything in ActiveSupport is available to you already. If you’re writing lighter weight Ruby code and don’t need the full heft of the Rails Framework, you can leverage some, or all, of ActiveSupport’s extra bells and whistles as required.

Better still, look in the source code — ActiveSupport is entirely built in Ruby, after all — and work out how the helper method you’ve just used works.

In the case of in_groups_of, it relies on the built-in Ruby method each_slice, with a little bit of extra logic to work out what to do with the remainders.

In our case, we could have easily switched to use each_slice because that would treat our remainders in exactly the way we want right now. Sticking with the ActiveSupport method, though, makes it easier to change our views should our design need to change in future.

And if there’s anything that working with four-year-old code teaches you, it’s that making it easier to change in future will earn you the gratitude of whoever has to implement that change.

Like what you read? Give Scott Matthewman a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.