Filterable
Decouple your filtering and faceted search using concerns
How
- Use a
Filterable
concern to mediate between your controllers/concerns and the filtering logic - In separate
Filter
classes, define how and which rules apply, and how to update filter params
Caveats
The Book
and Restaurant
classes in this example mimick an ActiveRecord
model. The array refinements simply serve as stand-ins for model scopes or AR queries.
app/reflexes/filter_reflex.rb
class FilterReflex < ApplicationReflex
include Filterable
def filter
resource, param = element.dataset.to_h.fetch_values(:resource, :param)
value = if element["type"] == "checkbox"
element.checked
else
element.dataset.value || element.value
end
set_filter_for!(resource, param, value)
end
end
app/controllers/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
included do
if respond_to?(:helper_method)
helper_method :filter_active_for?
helper_method :filter_for
end
end
def filter_active_for?(resource, attribute, value=true)
filter = filter_for(resource)
filter.active_for?(attribute, value)
end
private
def filter_for(resource)
"#{resource}Filter".constantize.new(session)
end
def set_filter_for!(resource, param, value)
filter_for(resource).merge!(param, value)
end
end
app/controllers/books_controller.rb
class BooksController < ApplicationController
include Filterable
def index
@books = Book.all
@books = filter_for("Book").apply!(@books)
end
end
app/filters/base_filter.rb
class BaseFilter
include ActiveModel::Model
include ActiveModel::Attributes
def initialize(session)
@_session = session
super(@_session.fetch(:filters, {})[filter_resource_class])
end
def apply!(_chain)
raise NotImplementedError
end
def merge!(_attribute, _value)
@_session[:filters] ||= {}
@_session[:filters][filter_resource_class] ||= {}
end
def active_for?(attribute, value=true)
filter_attribute = send(attribute)
return filter_attribute.include?(value) if filter_attribute.is_a?(Enumerable)
filter_attribute == value
end
def filter_resource_class
@filter_resource_class || self.class.name.match(/\A(?<resource>.*)Filter\Z/)[:resource]
end
end
app/filters/book_filter.rb
class BookFilter < BaseFilter
attribute :query, :string, default: ""
def apply!(chain)
chain = chain.search(query) if query.present?
chain
end
def merge!(attribute, value)
super
send(:"#{attribute}=", value)
@_session[:filters]["Book"].merge!(attribute => send(attribute))
end
end
app/models/book.rb
class Book < ApplicationRecord
scope :search, -> { where ... }
end
app/views/books/index.html.erb
<h2 class="mt-4">Books</h2>
<input type="text" class="form-control" id="book_query" placeholder="Search for author or title" data-reflex="input->Filter#filter" data-resource="Book" data-param="query" data-reflex-root="#books-table"/>
<table class="table" id="books-table">
<thead>
<tr>
<th scope="col">Author</th>
<th scope="col">Title</th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.author %></td>
<td><%= book.title %></td>
</tr>
<% end %>
</tbody>
</table>