erb ruby 63 lines · 3 tabs

Frame navigation that targets a specific frame via form_with

Shared by codesnips Jan 2026
3 tabs
<h1>Products</h1>

<%= form_with url: products_path, method: :get,
              data: { turbo_frame: "products_list", turbo_action: "advance" } do |f| %>
  <div class="filters">
    <%= f.text_field :q, value: params[:q], placeholder: "Search products" %>

    <%= f.select :category,
                 options_for_select(Product.categories, params[:category]),
                 include_blank: "All categories" %>

    <%= f.select :sort,
                 options_for_select([["Newest", "recent"], ["Price", "price"]], params[:sort]) %>

    <%= f.submit "Apply" %>
  </div>
<% end %>

<%= render "products/list", products: @products, pagy: @pagy %>
3 files · erb, ruby Explain with highlit

This snippet shows the Rails idiom for driving a Turbo Frame from a form that lives outside that frame, so a submit updates one region of the page while leaving the rest intact. The core mechanic is the data-turbo-frame attribute: it tells Turbo which frame should absorb the response, even though the form itself is not nested inside that frame.

In the products/index view, a filter form is rendered above the results. It is built with form_with url: products_path, method: :get and marked with data: { turbo_frame: "products_list" }. Because Turbo intercepts the submission, the GET request is issued, but instead of a full-page navigation Turbo looks in the response for a <turbo-frame id="products_list"> and swaps only that element. The turbo_frame_tag "products_list" below wraps the collection partial, giving Turbo a stable target. Note that the form and the frame share the same id string — that string is the whole contract, and a typo silently falls back to a full-page load.

The ProductsController is deliberately ordinary. It filters by the incoming params and renders normally; there is no special JSON path or respond_to branch. This is the key trade-off of frames versus Stimulus-driven fetches: the server keeps rendering plain HTML and stays framework-agnostic, and the same action serves both a first full-page visit and a frame-scoped re-render. Turbo decides what to keep based purely on matching frame ids.

The products list partial renders the matching <turbo-frame> and its contents. Because Turbo replaces the frame's inner HTML by matching ids, the response can be the full layout — Turbo extracts just the frame — which is why the controller needs no branching.

A common pitfall is pagination or sort links inside the frame navigating the whole page; wrapping them in the same frame keeps them scoped. Another is losing the query string in the URL bar — adding data: { turbo_action: "advance" } promotes the frame navigation to a history entry so filters survive a refresh. This pattern is ideal for search filters, tabs, and master-detail panes where a small, server-rendered region updates independently.


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 Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
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
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 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

Share this code

Here's the card — post it anywhere.

Frame navigation that targets a specific frame via form_with — share card
Link copied