<%= turbo_frame_tag dom_id(user) do %>
<tr>
<td><%= user.name %></td>
<td><%= user.role %></td>
<td>
<%= link_to 'Edit', edit_user_path(user), data: { turbo_frame: dom_id(user) } %>
</td>
</tr>
<% end %>
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to users_path, notice: 'Updated.' }
end
else
respond_to do |format|
format.turbo_stream { render :update, status: :unprocessable_entity }
format.html { render :edit, status: :unprocessable_entity }
end
end
end
private
def user_params
params.require(:user).permit(:name, :role)
end
end
For admin-ish UIs, inline editing is the best balance of speed and clarity. I wrap each row in a turbo_frame_tag keyed by dom_id(record) and render either a read-only row partial or an edit form partial inside the same frame. Clicking “Edit” targets the frame, so only that row swaps in-place. The update action responds with either the display partial (success) or the form with errors (failure). The important detail is keeping both states compatible with the same frame ID, so Turbo can seamlessly replace content without reloading the rest of the page. This pattern scales well to lists and feels ‘SPA-like’ with mostly Rails views.