DEV Community

Cover image for Using each_slice in Rails
Chuck
Chuck

Posted on

Using each_slice in Rails

I wanted to take a moment to share about an approach to a problem I encounter with our application at work. I was working on a multi-step controller pattern which returned data from an external API to move through a registration process. The data did not persist into a database, but instead, was presented from JSON.

The design called for a grid of 12 cards that could be paginated, searched, and selected. For instance if we have 32 cards, on multiple pages, we needed to be able to select all the cards, and on submit, persist 32 cards to the next controller action with an array of data. After reviewing the specification, we decided to use DataTables, which we have already used throughout this Rails 5 application.

The problem: How do you populate a card view into a HTML table? DataTables will read a table, add pagination and search automatically but does not work with a CSS Grid Card view.

TLTR: Just go grab the source code if you prefer.

Base application

So, I have created a base Rails 6 application to start with set up with Webpacker, Bootstrap5, and I have added jQuery for DataTables. I have created a single resource called Player, with the attribute of name, used faker to populate the database with thirty instances, and displayed in a card view on the index action.

Players card view

Set up Datatables

We are going to set up DataTables just so we can see the results. You can go to the DataTables Download page to confirm which package you will need. In my case, I am using the package which uses Bootstrap5.

yarn add datatables.net-bs5

Next set up a file to configure. I just placed in my packs directory: app/javascript/packs/player-datatables.js, and do not forget to import from application.js: import "./player-datatables.js".

To set up, call like so in player-datatables.js:

require('datatables.net-bs5');

$(document).ready( function () {
    $('#players').DataTable(); // players ID for our table
} );
Enter fullscreen mode Exit fullscreen mode

However, I want to make a few configuration changes.

  • Set the pagination parameter of 4 rows
  • Do not show the rows filter select and label
  • Hide the search box label
  • Add a placeholder into the search box
  • Inject some CSS classes for the search box styling
require('datatables.net-bs5');

$(document).ready( function () {
    $('#players').DataTable({ 
        "pageLength": 4,        // set rows for pagination
        "bInfo": false,         // Hide show columns select
        "bLengthChange": false,  // Hide bInfo 1 of n shown
        "oLanguage": {
            "sSearch": "",
            "sSearchPlaceholder": "Search players..."
        }
    });
    // Add classes to search box
    $('#players_filter').addClass('d-flex justify-content-end me-3') 
} );
Enter fullscreen mode Exit fullscreen mode

DataTables will look for a table with the ID of players, use the table rows to populate the data, and do its magic. The table must have column headings for each column, but we can kind of fake it like I have done here:

<table id="players">
  <thead class="d-none">
    <tr>
      <td>Player</td>
      <td>Player</td>
      <td>Player</td>
    </tr>
  </thead>
<tbody></tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

Now we have an empty table populating on the index view.
Empty Data Table

Logic of iteration

Let's look at the logic of iterating over a collection to populate a view. Iterating normally would look something like this using each:

  <tbody>
    <% @players.each do |player| %>
      <tr>
        <td><%= player.title %></td>
      </tr>
    <% end %>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

However, we are trying to replicate a card grid into a table, which means that we want to force only three columns, then move to the next row, using the same data in each table cell. The above loop will place our entire collection into one column.

There might be other solutions, maybe using each_with_index and evaluating the index with modulus, although this did not work for me.

First let me say, I love Ruby, which had the perfect method for this use case: each_slice, which will iterate the given block for each slice of a number of specified elements. If no block is given, it will return an enumerator.

Notice this example:

irb(main):009:0> (1..10).each_slice(3) { |a| p a }
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
=> nil
Enter fullscreen mode Exit fullscreen mode

each_slice return arrays with the number of elements specified (3), and then an array of the remainder elements.

Solution

So, to start building our view with each_slice, the <tbody> section will start as so:

<tbody>
    <% @players.each_slice(3) do |player| %>
      <tr role="row">
        ...
      </tr>
    <% end %>
  </tbody>
Enter fullscreen mode Exit fullscreen mode

Now, remember, each_slice returns an array, so player is an array, not an instance, in which will will need to iterate:

<tbody>
    <% @players.each_slice(3) do |player| %>
      <tr role="row">
        <% player.each do |p| %>
          <%= render partial: "players/player", locals: { p: p } %>
        <% end %>
      </tr>
    <% end %>
  </tbody>
Enter fullscreen mode Exit fullscreen mode

This seems like we are finished, but remember if we do have a remainder array returned from each_slice, we have not addressed these. In fact, DataTable will crash. See console output:
Data Tables console errors
The reason: DataTables expect correctly formatted table markup. The browser is more forgiving, and will display the table. However, all the DataTables features will not be present (i.e. search, pagination). If there are remainders, there is no <td></td> tags for those cells. Luckily, Ruby can help us real easily to check if there are any remainder in player:

<tbody>
    <% @players.each_slice(3) do |player| %>
      <tr role="row">
        <% player.each do |p| %>
          <%= render partial: "players/player", locals: { p: p } %>
        <% end %>
        <% (3 - player.length).times do %> // calc remainders
          <td></td>
        <% end %>
      </tr>
    <% end %>
  </tbody>
Enter fullscreen mode Exit fullscreen mode

Conclusion

So, what have we learned? First, Ruby is beautiful, but beyond the obvious, we have learned about each_slice. This is a method you may not use everyday, but with this example you have seen a least one use case. Be sure to leave a comment or hit me up on Twitter, and I hope you have enjoyed.

Discussion (0)