written by primehammer

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
endCode 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.