Now that we have a lot of books in our library system, it would be great if we could quickly filter the books based on their category. HTML supplies a nifty builtin tag, called multi select, that will let us display a couple options. Our librarians can then select one category, or a couple of categories, and we’ll filter the books displayed on the page.
This is going to require a number of additions to our existing app. We’ll create a new controller that returns a list of books in HTML that will be sent over the wire. We’ll have to setup a route to handle the search request. Since our stimulus controller will use the fetch api, and we’re sending filter parameters, it seemed like this action would work best as a POST end point.
I’m going to work from my previous tutorial, which you can find on Github.
Here’s our setup.
Refactor the Books index action
Our books_controller.rb
now gets an extra list of data. This is leveraging the pluck
operation, which only returns the values in a single column as an array. Then we use the ruby uniq
method to give us all the unique values. These will appear in the select field as selectable values.
class BooksController < ApplicationController
def index
@categories = Book.all.order(:category).pluck(:category).uniq
@books = Book.all
end
end
We will refactor our index.html.erb
to include the book table as a reusable fragment, and the select tag.
<h1>All Books</h1>
<div data-controller="select-filter" data-select-filter-url="<%= books_filter_path %>">
<div class="left-col">
<h2>Filter categories:</h2>
<select name="categories" multiple size="<%= @categories.length %>" data-action="select-filter#change">
<%= @categories.each do |category| %>
<option value="<%= category %>"><%= category.humanize %></option>
<% end %>
</select>
</div>
<div class="right-col" data-target="select-filter.books">
<%= render partial: 'book_list', locals: { books: @books} %>
</div>
</div>
And our fragment, _book_list.html.erb
:
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Publisher</th>
<th>Category</th>
</tr>
</thead>
<tbody>
<% books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.author %></td>
<td><%= book.publisher %></td>
<td><%= book.category %></td>
</tr>
<% end %>
</tbody>
</table>
Add our new Books Filter Controller
We’ll need to add the route for our new controller:
Rails.application.routes.draw do
resources :books
post 'books_filter', action: :index, controller: 'books_filter'
end
And our new controller, books_filter_controller.rb
:
class BooksFilterController < ApplicationController
def index
@books = Book.where(category: params[:categories]).order(:category)
end
end
And finally, the view that will be sent back to our Stimulus filter controller, books_filter/index.html.erb
. It’s going to reuse the previous books_list.html.erb
fragment, so that any changes we make to that one file will be propagated throughout our app.
<%= render partial: 'books/book_list', locals: { books: @books} %>
Using Stimulus to Wrap It All Together
Let’s create our stimulus controller that will handle the selection changes, load the new filtered list of books, and change the pages html.
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "books" ]
change(event) {
fetch(this.data.get("url"), {
method: 'POST',
body: JSON.stringify( { categories: [...event.target.selectedOptions].map(option => option.value)}),
credentials: "include",
dataType: 'script',
headers: {
"X-CSRF-Token": getMetaValue("csrf-token"),
"Content-Type": "application/json"
},
})
.then(response => response.text())
.then(html => {
this.booksTarget.innerHTML = html
})
}
}
function getMetaValue(name) {
const element = document.head.querySelector(`meta[name="${name}"]`)
return element.getAttribute("content")
}
When the select element changes, our controller fetches data from a url we’ve set as a data attribute of the controller. We post the categories to our book_filter
index action, which takes the tags, and returns our filtered table. The html table is replaced with the new data.
Now you have a controller that retrieves data from an API and replaces html on your page. And it let’s you select multiple categories, so you can easily find your favorite scientific treatise and horror book.
Feel free to leave a comment or question below.
Want To Learn More?
Try out some more of my Stimulus.js Tutorials.
One comment on “Stimulus.js Tutorial – Using multi select to pare down a large set of data”
[…] Stimulus.js Tutorial – Using multi select to pare down a large set of data […]