Ransack
Ransack is fully supported but entirely optional. The engine works without any search system.
Installation
Add Ransack to your application:
bundle add ransack
Ransack 4+ requires you to allowlist searchable attributes on your models:
class User < ApplicationRecord
def self.ransackable_attributes(_auth_object = nil)
%w[name email created_at]
end
def self.ransackable_associations(_auth_object = nil)
[]
end
end
Search and sort helpers
The engine provides helpers that wrap Ransack with styled controls. If Ransack is not installed, both helpers degrade gracefully:
- In development, a visible warning replaces the form; sort links render as plain text with the warning in a tooltip
- In production and test,
l_ui_search_formreturns nothing andl_ui_sort_linkrenders a plain-text header cell; both log a warning
| Helper | Purpose |
|---|---|
l_ui_search_form |
Styled search form wrapping Ransack's search_form_for |
l_ui_sort_link |
Styled sort link wrapping Ransack's sort_link |
Search and sort example
Two independent collections on one page, each in a Turbo Frame. Searching or sorting one does not affect the other. The URL updates for back/forward navigation.
Users
Posts
| Title | Created▲, sorted ascending |
|---|---|
| Testing best practices | about 2 months ago |
| Background jobs with Solid Queue | about 2 months ago |
| Authentication with Devise | about 2 months ago |
| Search with Ransack | about 2 months ago |
| Pagination with Pagy | about 2 months ago |
Usage
In your controller, use Ransack's search_key to namespace each collection:
def index
@users_q = User.ransack(params[:users_q], search_key: :users_q)
@users = @users_q.result(distinct: true)
@posts_q = Post.ransack(params[:posts_q], search_key: :posts_q)
@posts = @posts_q.result(distinct: true)
end
In your view, wrap each collection in a turbo_frame_tag and pass turbo_frame: to the helpers. Add the l-ui--search-form controller with rewriteLink on the frame to preserve URL params in pagination links:
<%= turbo_frame_tag "users_collection", data: { turbo_action: "advance",
controller: "l-ui--search-form", l_ui__search_form_scope_value: "users_q",
action: "click->l-ui--search-form#rewriteLink" } do %>
<%= l_ui_search_form(@users_q, url: users_path, fields: [:name, :email],
clear: true, turbo_frame: "users_collection") %>
<%= l_ui_table(@users,
columns: [
{ attribute: :name, primary: true, render: ->(u) { u.name } },
{ attribute: :email, render: ->(u) { u.email } }
],
caption: "Users",
query: @users_q,
url: users_path,
turbo_frame: "users_collection") %>
<%= l_ui_pagy(@users_pagy) %>
<% end %>
The search_key flows through automatically - l_ui_search_form reads it from the query object and passes it as as: to search_form_for. The clear button preserves other collections' params. data-turbo-action="advance" pushes state to the URL for back/forward navigation. The rewriteLink action merges current URL params into pagination links so other collections' page positions are preserved.
Sort link options
<!-- Basic sort link -->
<%= l_ui_sort_link(@q, :name) %>
<!-- Custom label -->
<%= l_ui_sort_link(@q, :created_at, "Joined") %>
<!-- Default descending order -->
<%= l_ui_sort_link(@q, :created_at, "Joined", default_order: :desc) %>
<!-- With Turbo Frame -->
<%= l_ui_sort_link(@q, :name, turbo_frame: "my_frame") %>
<!-- With HTML options -->
<%= l_ui_sort_link(@q, :name, html: { data: { turbo_action: "replace" } }) %>
See the search page for the full search helper API, options, and partials reference.