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.
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
} );
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')
} );
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>
Now we have an empty table populating on the index view.
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>
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
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>
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>
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:
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>
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.
Top comments (0)