A Rails engine that provides flexible, column-based filtering for Bullet Train applications. This gem enables you to easily add dynamic search and filtering capabilities to your data tables with a clean, responsive UI and real-time search functionality.
PS: This is not an official Bullet Train gem.
- Dynamic Column Filtering: Filter table data by any model attribute
- Real-time Search: Debounced search input with configurable delay (default 300ms)
- Toggleable Filter UI: Collapsible filter form with smooth toggle animation
- Turbo Frame Support: Seamless integration with Hotwire Turbo for fast, Ajax-like updates
- Internationalization: Built-in I18n support with customizable labels and placeholders
- Flexible Integration: Easy to integrate with existing Bullet Train controllers
- Responsive Design: Mobile-friendly filter interface with Tailwind CSS styling
- Safe SQL: Automatic SQL sanitization to prevent injection attacks
Add this line to your application's Gemfile:
gem 'bullet_train-table_filters',
git: 'git@github.com:natannobre/bullet_train-table_filters.git',
branch: 'main'And then execute:
$ bundle installThen run the installer:
$ rails generate bullet_train:table_filters:installIn your controller, include the #available_filter_attributes private method to define which attributes can be filtered:
class Account::ProjectsController < ApplicationController
account_load_and_authorize_resource :project, through: :team, through_association: :projects
# GET /account/teams/:team_id/projects
# GET /account/teams/:team_id/projects.json
def index
delegate_json_to_api
end
private
def available_filter_attributes
%w[name description]
end
endIn your index (in our example app/views/account/projects/_index.html.erb) view:
- Move the pagy controls inside the turbo frame
- Remove the pagy arg from the box rendering (
<%= render 'account/shared/box', pagy: pagy do |box| %>) - Inside the
box.descriptionrender the search form partial - Wrap your table in a turbo frame with the same ID as the one passed to the search form
<% projects = projects.accessible_by(current_ability) %>
<% team = @team %>
<% context ||= team %>
<% collection ||= :projects %>
<% hide_actions ||= false %>
<% hide_back ||= false %>
<%= action_model_select_controller do %>
<% cable_ready_updates_for context, collection do %>
<%= render 'account/shared/box' do |box| %>
<% box.title t(".contexts.#{context.class.name.underscore}.header") %>
<% box.description do %>
<%= t(".contexts.#{context.class.name.underscore}.description#{"_empty" unless projects.any?}") %>
<%= render "shared/limits/index", model: projects.model %>
<%= render "account/table_filters/search_form",
collection: projects,
url: account_team_projects_path(@team),
turbo_frame: "projects",
attributes: { name: :text, description: :text } %>
<% end %>
<% box.table do %>
<%= turbo_frame_tag "projects" do %>
<% pagy ||= nil %>
<% pagy, projects = pagy(projects, page_param: :projects_page) unless pagy %>
<% if projects.any? %>
<table class="table">
<thead>
<tr>
<%= render "shared/tables/select_all" %>
<th><%= t('.fields.name.heading') %></th>
<th><%= t('.fields.description.heading') %></th>
<%# π
super scaffolding will insert new field headers above this line. %>
<th><%= t('.fields.created_at.heading') %></th>
<th class="text-right"></th>
</tr>
</thead>
<tbody>
<%= render partial: 'account/projects/project', collection: projects %>
</tbody>
</table>
<% end %>
<% if defined?(pagy) && pagy %>
<div class="m-4 flex justify-end">
<%== pagy_nav(pagy) if pagy.pages > 1 %>
</div>
<% end %>
<% end %>
<% end %>
<% box.actions do %>
<% unless hide_actions %>
<% if context == team %>
<% if can? :create, Project.new(team: team) %>
<%= link_to t('.buttons.new'), [:new, :account, team, :project], class: "#{first_button_primary(:project)} new" %>
<% end %>
<% end %>
<%# π
super scaffolding will insert new targets one parent action model buttons above this line. %>
<%# π
super scaffolding will insert new bulk action model buttons above this line. %>
<%= render "shared/bulk_action_select" if projects.any? %>
<% unless hide_back %>
<%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary(:project)} back" %>
<% end %>
<% end %>
<% end %>
<% box.raw_footer do %>
<%# π
super scaffolding will insert new action model index views above this line. %>
<% end %>
<% end %>
<% end %>
<% end %>Important: You'll have to change every link in you table to use
data: { turbo_frame: "_top" }to avoid loading the link inside the turbo frame.
Customize the filter labels and placeholders in your locale files
- Controller Integration: The
Account::TableFilters::Baseconcern automatically applies filters to your model scope based on URL parameters - Safe Filtering: All filter values are sanitized using
ActiveRecord::Base.sanitize_sql_liketo prevent SQL injection - Real-time Updates: The JavaScript controllers handle form submission with debouncing and Turbo Frame updates
- Pattern Matching: Filters use SQL
LIKEqueries with wildcards for flexible text matching
- Rails >= 8.0.0
- Hotwire Turbo
- Stimulus (for JavaScript functionality)
- Bullet Train framework
Bug reports and pull requests are welcome on GitHub at https://github.com/natannobre/bullet_train-table_filters.
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
The gem is available as open source under the terms of the MIT License.