#7 Hotwire Turbo Frames: Search without page refresh. Stimulus. Ransack
Search without page refresh using Hotwire (Turbo Frames & Stimulus):
0. Initial setup #
rails g scaffold inbox name
rails db:migrate
rails c
5.times { Inbox.create(name: SecureRandom.hex) }
rails s
1. Without Ransack #
- case-insensitive search, like in this post
#app/controllers/inboxes_controller.rb
def index
if params[:name].present?
@inboxes = Inbox.where('name ilike ?', "%#{params[:name]}%")
else
@inboxes = Inbox.all
end
end
- stimulus controller to submit form with 500ms delay, inspired by this post
// app/javascript/controllers/debounce_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "form" ]
connect() { console.log("debounce controller connected") }
search() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.formTarget.requestSubmit()
}, 500)
}
}
- initialize stimulus controller on form
- stimulus target - this form
- on input - fire the stimulus controller to submit this form
- form target -
turbo_frame: 'search'
with list of inboxes - wrap list of inboxes into a
turbo_frame_tag 'search'
#app/views/inboxes/index.html.erb
<%= form_with url: inboxes_path,
method: :get,
data: { controller: 'debounce',
debounce_target: 'form',
turbo_frame: 'search' } do |form| %>
<%= form.text_field :name,
placeholder: 'Name',
value: params[:name],
autocomplete: 'off',
autofocus: true,
data: { action: 'input->debounce#search' } %>
<% end %>
<%= turbo_frame_tag 'search' do %>
<%= request.url %>
<%= link_to 'Clear search', request.path if request.query_parameters.any? %>
<div id="inboxes">
<%= render @inboxes %>
</div>
<% end %>
- Next, you will want to use
target: '_top'
on such a “global” turbo_frame, so that when you click a link on content inside the frame, it does not look for aturbo_frame_tag "search"
#app/views/inboxes/index.html.erb
-- <%= turbo_frame_tag 'search' do %>
++ <%= turbo_frame_tag 'search', target: '_top' do %>
-- <%= link_to 'Clear search', request.path if request.query_parameters.any? %>
++ <%= link_to 'Clear search', request.path, data: { turbo_frame: 'search'} if request.query_parameters.any? %>
2. With Ransack #
Basic search without Turbo:
#Gemfile
gem 'ransack', github: 'activerecord-hackery/ransack'
#app/controllers/inboxes_controller.rb
def index
@q = Inbox.ransack(params[:q])
@inboxes = @q.result(distinct: true)
end
#app/views/inboxes/index.html.erb
++<%= search_form_for @q do |f| %>
++ <%= f.label :name_cont %>
++ <%= f.search_field :name_cont %>
++ <%= f.submit %>
++<% end %>
++<%= sort_link @q, :messages_count, 'Popular' %>
++<%= sort_link @q, :created_at, 'Fresh' %>
++<%= link_to 'Clear search', request.path if request.query_parameters.any? %>
<div id="inboxes">
<%= render @inboxes %>
</div>
2.1. ADD TURBO #
- wrap the results into a frame
- search/sort should be in the frame - so that the sort direction images get updated
-
target: "_top"
- explicitly target what you need by the frame - the search links should have a turbo_frame ‘search’ target
- connect the stimulus controller to the FORM
- stimulus - submit
search_field
on INBPUT
#app/views/inboxes/index.html.erb
--<%= search_form_for @q, data: { turbo_frame: 'search'} do |f| %>
++<%= search_form_for @q, data: { controller: 'debounce',
++ debounce_target: 'form',
++ turbo_frame: 'search' } do |f| %>
<%= f.label :name_cont %>
-- <%= f.search_field :name_cont %>
++ <%= f.search_field :name_cont,
++ autocomplete: "off",
++ data: { action: "input->debounce#search" } %>
<%= f.submit %>
<% end %>
++<%= turbo_frame_tag 'search', target: "_top" do %>
--<%= sort_link @q, :messages_count, 'Popular' %>
--<%= sort_link @q, :created_at, 'Fresh' %>
++<%= sort_link @q, :messages_count, 'Popular', {}, { data: { turbo_frame: 'search'} } %>
++<%= sort_link @q, :created_at, 'Fresh', {}, { data: { turbo_frame: 'search'} } %>
<%= link_to 'Clear search', request.path, data: { turbo_frame: 'search'} if request.query_parameters.any? %>
<!-- request.url - to see the URL returned by the turbo_frame -->
<%= request.url %>
<div id="inboxes">
<%= render @inboxes %>
</div>
++<% end %>
2.2. Reset search #
- currently, if search_form is not in the turbo_frame, clear_search does not clear the input field
- add a stimulus controller to “click a button -> reset an input field”
#app/javascript/controllers/reset_controller.js
import { Controller } from "@hotwired/stimulus"
//<div data-controller="reset">
// <input data-reset-target=clearme>
// <button data-action="click->reset#clean">clear</button>
//</div>
export default class extends Controller {
static targets = [ "clearme" ]
connect() { console.log("reset controller connected") }
clean() {
console.log(this.clearmeTarget)
this.clearmeTarget.value=''
}
}
- wrap the content into the new controller:
<div data-controller="reset">
- add a target to the input field that should be reset:
data: { reset_target: 'clearme',
- add an action to the button that should reset the input:
action: "click->reset#clean"
++<div data-controller="reset">
<%= search_form_for @q, data: { controller: 'debounce',
debounce_target: 'form',
turbo_frame: 'search' } do |f| %>
<%= f.label :name_cont %>
<%= f.search_field :name_cont,
autocomplete: "off",
++ data: { reset_target: 'clearme',
action: "input->debounce#search" } %>
<%= f.submit %>
<% end %>
<%= turbo_frame_tag 'search', target: "_top" do %>
<%= sort_link @q, :messages_count, 'Popular', {}, { data: { turbo_frame: 'search'} } %>
<%= sort_link @q, :created_at, 'Fresh', {}, { data: { turbo_frame: 'search'} } %>
--<%= link_to 'Clear search', request.path, data: { turbo_frame: 'search'} if request.query_parameters.any? %>
++<%= link_to 'Clear search', request.path, data: { action: "click->reset#clean", turbo_frame: 'search'} if request.query_parameters.any? %>
<!-- request.url - to see the URL returned by the turbo_frame -->
<%= request.url %>
<div id="inboxes">
<%= render @inboxes %>
</div>
<% end %>
++</div>
PERFECTO!!!
However…
A BIG DRAWBACK of this approach = this way we do not update the URL on search.
How can we do it? Turbo Drive? Some js? Who knows…
Did you like this article? Did it save you some time?