Updating table rows in Rails using Turbo Streams
With previous versions of Ruby on Rails, there was no built-in way to update parts of a page without writing any Javascript code. With Hotwire or Turbo-Rails integration, updating parts of the page gets really easy.
I assume a lot of Ruby on Rails applications have a table similar to this example. Data is displayed together with action buttons for each row.
In this tutorial, I’m using a simple Invoices table where I’d like to mark the Invoice as paid and update only the relevant row of the table. With minimal data transfer, no custom JavaScript code and no page redirect.
What you’d usually have is a button like this
<%= link_to "Mark as paid", mark_as_paid_invoice_path(invoice), class: "button", method: :patch %>
Code language: ERB (Embedded Ruby) (erb)
And controller action would look like this, ending with redirect to update the whole page.
# app/controllers/invoices_controller.rb
def mark_as_paid
invoice = Invoice.find(params[:id])
invoice.mark_as_paid!
respond_to do |format|
format.html do
redirect_back fallback_location: invoices_path, notice: "Invoice marked as paid"
end
end
end
Code language: Ruby (ruby)
Turbo streams
What if we can just update a particular row in the table without the need to render the whole page and redirect? No Javascript code included!
The first step is to add and install turbo-rails
in your application. Just follow the official documentation. The process differs depending on your application setup.
Now you need to separate table row HTML into a partial template. This partial will be rendered on the server replacing old data in the table.
# app/views/invoices/_invoice.html.erb
<tr id="<%= dom_id invoice %>">
<td><%= invoice.description %></td>
<td><%= invoice.customer %></td>
...
<td><%= link_to "Mark as paid", mark_as_paid_invoice_path(invoice), class: "button", method: :patch %></td>
</tr>
# app/views/invoices/index.html.erb
<table>
<thead>
...
</thead>
<tbody>
<%# automatically renders Invoices collection using _invoice.html.erb partial %>
<%= render @invoices %>
</tbody>
</table>
Code language: ERB (Embedded Ruby) (erb)
Notice the ID
of the TR
element. dom_id
helper automatically generates a unique identifier for the Invoice
object. In this case, it will be invoice_1
for the Invoice with ID 1. This is super important as we will use this identificator for updating the correct part of the DOM.
Now in the controller, we update the response part of the mark_as_paid
action to respond with the Turbo Stream.
# app/controllers/invoices_controller.rb
def mark_as_paid
invoice = Invoice.find(params[:id])
invoice.mark_as_paid!
respond_to do |format|
format.html do
redirect_back fallback_location: invoices_path, notice: "Invoice marked as paid"
end
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
invoice, # translates to same identifier as dom_id(invoice)
partial: 'invoices/invoice',
locals: { invoice: invoice },
)
end
end
end
Code language: Ruby (ruby)
We keep the redirect just in case the request isn’t a Turbo Stream. But if it is, we send replace
TurboStream response to the invoice
identifier using rendered partial with updated Invoice object.
Instead of rendering the whole page together with the layout, the complete response now looks like this.
<turbo-stream action="replace" target="invoice_1"><template><tr id="invoice_1">
<td>
Ruby Turbo Example
</td>
<td>
My dear customer
</td>
<td>
2022-05-11 13:03:36 UTC
</td>
<td>
<a class="button" rel="nofollow" data-method="patch" href="/invoices/1/mark_as_paid">Mark as paid</a>
</td>
</tr>
</template></turbo-stream>
Code language: HTML, XML (xml)
From this response, Turbo knows that it should replace the DOM element with ID invoice_1
with template
contents.
Conclusion
And that’s it! We’ve learned how to use Turbo Streams to replace a row of the table after the data change. The page is also more responsive to the user because no redirect is needed. All this with little extra code on the backend and no custom Javascript.