ruby erb 70 lines · 4 tabs

Inline edit a table row with Turbo Frames

Shared by codesnips Jan 2026
4 tabs
class ProductsController < ApplicationController
  before_action :set_product, only: %i[edit update]

  def index
    @products = Product.order(:name)
  end

  def edit
  end

  def update
    if @product.update(product_params)
      render partial: "products/product", locals: { product: @product }
    else
      render partial: "products/form",
             locals: { product: @product },
             status: :unprocessable_entity
    end
  end

  private

  def set_product
    @product = Product.find(params[:id])
  end

  def product_params
    params.require(:product).permit(:name, :sku, :price_cents)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows the canonical Hotwire pattern for editing a single table row in place without any custom JavaScript. The key idea is that each row is wrapped in its own <turbo-frame> whose ID is unique to the record, so a link inside the frame navigates only that frame. Turbo intercepts the click, fetches the response, extracts the frame with the matching ID, and swaps just that fragment — leaving the rest of the table untouched.

The ProductsController is deliberately thin. edit renders the frame in its editing state, and update decides what to send back: on success it re-renders the read-only row partial, and on validation failure it re-renders the edit form with status: :unprocessable_entity. Turbo only processes a 422 response into a frame swap because a normal 200 on a failed form would otherwise be discarded, so returning the right status code is what makes inline validation errors appear correctly inside the frame.

The shared frame identity is the load-bearing detail. _product.html.erb and _form.html.erb both call the same dom_id(product) helper via turbo_frame_tag, producing an ID like product_42. Because the show partial and the edit partial declare the same frame, Turbo knows they are two states of one region. The Edit link targets its enclosing frame implicitly, and the form's response (whichever partial it renders) is matched back into that frame by ID.

A subtle point is that the form partial wraps only the row's cells, keeping the <tr>/<td> structure valid — Turbo replaces the frame's inner HTML, so the frame must sit inside the table in a way that still parses. The Cancel link simply re-requests the edit-less show state by pointing back at the product, restoring the read-only view.

The pattern scales well: many rows can be edited independently and concurrently, each swap is small, and it degrades gracefully — without Turbo, the links and form still perform full-page navigations to the same controller actions. It avoids the complexity of client-side state while giving an SPA-like feel, at the cost of a network round trip per interaction.


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.

Inline edit a table row with Turbo Frames — share card
Link copied