Skip to content

Blogging On Rails

Everything on Rails!

Stimulus.js Tutorial – Using multi select to pare down a large set of data

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.

Make Interactivity Default 

Make your web app interactive now with easy to implement and simple to add HOTWire integrations. 

Enter your email and get a free sample of my HOTWire Tutorials ebook.

We won’t send you spam. Unsubscribe at any time.

One comment on “Stimulus.js Tutorial – Using multi select to pare down a large set of data”

Leave a Reply

Your email address will not be published. Required fields are marked *