erb javascript ruby 114 lines · 3 tabs

Live search with Turbo Frames and debouncing

Jordan Lee Jan 2026
3 tabs
<div class="search-page">
  <div class="search-header mb-6">
    <h1 class="text-2xl font-bold mb-4">Search Posts</h1>

    <%= form_with url: posts_path,
        method: :get,
        data: {
          controller: "search",
          search_target: "form",
          turbo_frame: "search_results"
        } do |f| %>

      <div class="relative">
        <i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
        <%= f.search_field :q,
            value: params[:q],
            placeholder: "Search posts...",
            class: "form-input pl-10",
            autocomplete: "off",
            data: {
              search_target: "input",
              action: "input->search#search"
            } %>
      </div>
    <% end %>
  </div>

  <%= turbo_frame_tag "search_results" do %>
    <% if params[:q].present? %>
      <div class="search-results">
        <% if @posts.any? %>
          <p class="text-gray-600 mb-4">
            Found <%= pluralize @posts.total_count, 'post' %> for "<%= params[:q] %>"
          </p>
          <div class="space-y-4">
            <%= render @posts %>
          </div>
        <% else %>
          <div class="text-center py-12">
            <i class="fas fa-search text-4xl text-gray-300 mb-4"></i>
            <p class="text-gray-600">No posts found for "<%= params[:q] %>"</p>
            <p class="text-sm text-gray-500">Try different keywords</p>
          </div>
        <% end %>
      </div>
    <% end %>
  <% end %>
</div>
3 files · erb, javascript, ruby Explain with highlit

Real-time search enhances discoverability but naive implementations hammer the server with requests. I use Turbo Frames to scope search results and a Stimulus controller to debounce input events, only sending requests after typing pauses. The search frame loads results from a dedicated endpoint that returns just the results partial. Empty searches clear results gracefully. I also show loading states and handle empty results with helpful messages. For large datasets, I implement minimum query length requirements and abort in-flight requests when new queries arrive. This pattern works for any filtered list—products, users, documents—and degrades gracefully to traditional form submission without JavaScript.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
ruby
class TasksController < ApplicationController
  def destroy
    @task = Task.find(params[:id])
    @task.destroy!

    respond_to do |format|

Remove deleted items instantly with turbo_stream.remove

rails hotwire turbo
by Henry Kim 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Live search with Turbo Frames and debouncing — share card
Link copied