Hello, fellow Rails enthusiasts! Today, we're diving into an essential feature for many web applications: implementing filters for a list or table. We'll walk through a practical example of how to add dynamic filtering to a Rails 7 application. Imagine we're building a library management system where we need to filter books by author, publication date, genre, and availability. I'll guide you through each step, explaining the setup, the code, and its purpose.
Introduction
Before we get started, let's talk about the tools and gems we'll be using:
- Rails 7: The latest version of Rails, which includes Turbo, Stimulus, and Importmap, making it easier to build modern, interactive web applications.
- Simple Form: A gem that simplifies form building in Rails. It provides a cleaner syntax and more powerful form components. You can find more about it here.
- Stimulus: A JavaScript framework that works perfectly with Rails to create interactive applications. This is now the convention with Rails 7+. Learn more about Stimulus here.
- Turbo: Part of the Hotwire stack, Turbo makes building reactive applications easy by handling partial page updates. More on Turbo here.
With these tools, we'll create a seamless and efficient filtering system.
Step 1: Setting Up the Rails Project
First things first, make sure you have Rails 7 installed. If not, you can install it with:
gem install rails
Let's create a new Rails project:
rails new LibraryManager --skip-javascript --skip-webpack-install cd LibraryManager
Add Simple Form to your Gemfile:
# Gemfile gem 'simple_form'
bundle install rails generate simple_form:install
Step 2: Adding the Filterable Concern
We'll create a `Filterable` module to manage our filtering logic. This module can be included in any model that needs filtering capabilities.
# app/models/concerns/filterable.rb module Filterable extend ActiveSupport::Concern module ClassMethods def filter(filtering_params) results = self.where(nil) # creates an anonymous scope filtering_params.each do |key, value| results = results.public_send("filter_by_#{key}", value) if value.present? end results end end end
This `Filterable` module defines a `filter` method that iterates over the provided parameters and dynamically applies the corresponding scope if the parameter is present.
Step 3: Updating the Book Model
Next, we'll include the `Filterable` module in our `Book` model and define the necessary filter scopes.
# app/models/book.rb class Book < ApplicationRecord include Filterable scope :filter_by_author, -> (author) { where(author: author) } scope :filter_by_start_date, -> (start_date) { where("publication_date >= ?", start_date) } scope :filter_by_end_date, -> (end_date) { where("publication_date <= ?", end_date) } scope :filter_by_genre, -> (genre) { where(genre: genre) } scope :filter_by_availability, -> (availability) { where(available: availability) } end
Step 4: Creating the Controller Actions
We'll update our controller to handle the filtering parameters and pass them to the model.
# app/controllers/books_controller.rb class BooksController < ApplicationController def index @filter_params = filter_params @books = Book.filter(@filter_params) end private def filter_params params.permit(:author, :start_date, :end_date, :genre, :availability) end end
In this code, we define a private `filter_params` method to permit the filtering parameters and use them to filter the books in the `index` action.
Step 5: Setting Up the Views
Let's create a form in the view to capture the filtering inputs and display the filtered list of books. We'll use Simple Form to make our form building easier.
<!-- app/views/books/index.html.erb --> <h1>Library Collection</h1> <%= simple_form_for :filter, url: books_path, method: :get, html: { class: 'form-inline' } do |f| %> <%= f.input :author, label: "Author", input_html: { class: 'form-control' } %> <%= f.input :start_date, as: :date, label: "Published After", input_html: { class: 'form-control' } %> <%= f.input :end_date, as: :date, label: "Published Before", input_html: { class: 'form-control' } %> <%= f.input :genre, label: "Genre", input_html: { class: 'form-control' } %> <%= f.input :availability, collection: [["Available", true], ["Checked Out", false]], include_blank: true, label: "Availability", input_html: { class: 'form-control' } %> <%= f.button :submit, "Filter", class: 'btn btn-primary' %> <% end %> <table class="table"> <thead> <tr> <th>Author</th> <th>Title</th> <th>Publication Date</th> <th>Genre</th> <th>Availability</th> </tr> </thead> <tbody> <% @books.each do |book| %> <tr> <td><%= book.author %></td> <td><%= book.title %></td> <td><%= book.publication_date %></td> <td><%= book.genre %></td> <td><%= book.available ? 'Available' : 'Checked Out' %></td> </tr> <% end %> </tbody> </table>
Step 6: Adding Stimulus for Enhanced UX
To enhance the user experience, we'll use Stimulus to handle form submission without page reloads.
First, install Stimulus:
rails turbo:install stimulus:install
Create a Stimulus controller to handle form submissions:
// app/javascript/controllers/filter_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = ["form"] submit() { this.formTarget.requestSubmit() } }
Connect the Stimulus controller to the form:
<!-- app/views/books/index.html.erb --> <%= simple_form_for :filter, url: books_path, method: :get, html: { class: 'form-inline', data: { controller: "filter", action: "change->filter#submit" } } do |f| %> ... <% end %>
Step 7: Final Touches
Make sure you have run all necessary migrations and seeded the database with some data for testing. Then, start the Rails server and navigate to the books page to see the filtering in action.
rails db:migrate rails db:seed rails server
Conclusion
In this guide, we've implemented a dynamic filtering system in a Rails 7 application using a library management system as our example. We covered creating a `Filterable` concern, updating the model with scopes, handling filtering in the controller, setting up the views, and enhancing the user experience with Stimulus. This pattern can be easily adapted to various models and use cases, providing a flexible and powerful way to manage filtered data in your Rails applications. Happy coding!