erb ruby javascript 84 lines · 4 tabs

Turbo Frames: inline “details drawer” without a SPA router

Shared by codesnips Jan 2026
4 tabs
<div class="orders-layout" data-controller="drawer">
  <table class="orders">
    <tbody>
      <% @orders.each do |order| %>
        <tr>
          <td><%= order.reference %></td>
          <td><%= number_to_currency(order.total) %></td>
          <td>
            <%= link_to "View details",
                  order_path(order),
                  data: { turbo_frame: "drawer", action: "drawer#open" } %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>

  <%# The drawer starts empty and is filled lazily by the clicked link. %>
  <%= turbo_frame_tag :drawer, class: "drawer", data: { drawer_target: "panel" } %>
</div>
4 files · erb, ruby, javascript Explain with highlit

This snippet shows how a lazy-loading "details drawer" is built with Turbo Frames so a click on a row swaps in richly-rendered detail content without any client-side router, JSON API, or SPA framework. The core idea is that a Turbo Frame is a scoped region of the DOM that intercepts links and forms targeting it, fetches the response over HTTP, and replaces only the matching <turbo-frame> element by id — leaving the rest of the page, including scroll position and unrelated state, untouched.

In orders/index.html.erb, the list is ordinary server-rendered markup, but each row link carries data-turbo-frame: "drawer". That attribute tells Turbo to route the response of that GET into the frame named drawer rather than doing a full-page visit. The empty turbo_frame_tag :drawer at the bottom is the target: it starts collapsed and only fills in once a row is clicked, which keeps the initial index cheap.

The OrdersController is deliberately dull, and that is the point. show just loads the record and renders normally — there is no branching on request format, no respond_to, no separate drawer endpoint. Turbo sends a standard Accept: text/html request; Rails returns the whole page, and Turbo extracts only the <turbo-frame id="drawer"> fragment from that response, discarding everything else. This means the same URL works as a full page on hard navigation and as a frame swap inside the list, which is why deep links and back-button behavior keep working for free.

In orders/show.html.erb, the detail content is wrapped in a matching turbo_frame_tag :drawer. The frame ids on both sides must agree or Turbo cannot find the fragment to graft in. A small Stimulus controller in drawer_controller.js handles pure presentation — toggling an open class and closing on Escape — without owning any data-fetching logic.

The trade-off is that frame content is a subset of a real page, so shared layout must live outside the frame, and error responses need a matching frame to render into. Reach for this pattern when a modest amount of contextual detail should load on demand while the surrounding page stays put, and a full SPA would be overkill.


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.

Turbo Frames: inline “details drawer” without a SPA router — share card
Link copied