Inline Editing

Inline Edits using Turbo Frames

This post explains how to use Turbo Frames to make your web application behave like a Single-Page Application without using any JavaScript.

5 min read
If you are interested in learning about Hotwire, check out my crash-course on Hotwire's Turbo framework.

In one of my previous posts that explores Turbo Drive, we learned that Turbo Drive makes your web application feel like a modern SPA by intercepting the link clicks and form submissions, making a fetch request, and dynamically replacing the whole body instead of reloading the browser.

This is a big improvement over the traditional web applications not using a JavaScript framework like React. However, the server still has to build and send the entire HTML in the response. If the only thing that you wanted to change was one single element on your page, all that effort in building and transporting the HTML over the wire is wasteful.

What if you could just send the specific HTML that changed, without touching the rest of the page? The response would be much smaller, and the rest of the HTML could be easily cached, making the application even more responsive.

This is where Turbo Frames comes into play.

Turbo Frames allow you to dynamically update sections on the page in response to some action, such as clicking a link or submitting a form. In contrast to Turbo Drive, Turbo Frames let you contain the scope of the change, reducing the size of the HTML your server has to send.

Usually, to build the final response HTML, you compose various views or partials together. With Turbo Frames, you can place those independent segments inside <turbo-frame> elements. Turbo replaces the existing frame that matches the response and updates it dynamically. This also allows you to lazily load frames in parallel, improving the perceived performance of your web application.

A key concept to understand with Turbo Frames is that any user interaction inside a specific <turbo-frame> element is scoped within that frame. The rest of the page doesn’t change or reload, unless you specifically target it.

You can make any section on your page a Turbo Frame by wrapping it inside a <turbo-frame> tag. For example:

<turbo-frame id="turbo-messages">

The server can provide a full document or just a part of it. The only condition is that the response contains the updated <turbo frame id="turbo-messages"> element. Upon receiving the response, Turbo extracts the specific frame from it and replaces the existing frame with the matching ID.

Points to note:

  • Each <turbo-frame> element must have a unique ID. Turbo uses this ID to match the content it will replace when a response arrives from the server.
  • A single page can contain multiple turbo frames, allowing different contexts. They can be lazy-loaded or replaced independently.

That’s enough theory. Let’s use a concrete example that demonstrates how useful Turbo Frames can be when used properly.

Consider the to-do list that we built in the previous tutorial using Turbo Drive.

Here’s how it’s implemented right now.

  • The _task.html.erb partial displays the task.
<!--display the checkbox and task-->

<%= link_to "Edit", edit_task_path(task), class: "btn bg-gray-100" %>
  • The Edit button points to tasks/2/edit
  • The TasksController renders the edit.html.erb template with the following content.
  <h1 class="font-bold text-2xl mb-3">Editing Task</h1>

  <div id="<%= dom_id @task %>">
    <%= render "form", task: @task %>
    <%= link_to "Never Mind", tasks_path, class: "btn mb-3 bg-gray-100" %>

Clicking the Edit button takes you to the edit page. If you inspect the Network tab in DevTools window, you can see that it’s rendering the whole HTML response, which is about 5.2 kB. The Turbo Drive only replaces the body upon receiving the response.

Let’s improve this functionality by rendering the edit form in-place on the index page, instead of taking the user to a separate page. We will achieve this in three simple steps using Turbo Frames.

Step 1: Highlight Turbo Frames

Turbo Frames are invisible elements. However, when learning, it’s useful to give them some style to understand what’s going on. So add some CSS to make them visible.

turbo-frame {
  display: block;
  border: 1px solid lightblue;
  border-radius: 5px;
  padding: 0.1em 1em;
  margin: 1em 0;

Step 2: Wrap the task template in a turbo frame

We will also use the dom_id helper to assign a unique ID to each <turbo-frame> element.

<turbo-frame id="<%= dom_id task %>">
  <!--display the checkbox and task-->

  <%= link_to "Edit", edit_task_path(task), class: "btn bg-gray-100"%>

Step 3: Wrap the edit response in a turbo frame

Notice that we haven’t wrapped the <h1> element inside the <turbo-frame> tag. You will see why in a minute.

  <h1 class="font-bold text-2xl mb-3">Editing Task</h1>

  <turbo-frame id="<%= dom_id @task %>">
    <%= render "form", task: @task %>
    <%= link_to "Never Mind", tasks_path, class: "btn mb-3 bg-gray-100" %>

That’s it. Reload the page and be prepared to be amazed.

First, each <turbo-frame> tag containing the task will be rendered on the page.

highlighted turbo frames

Upon clicking the Edit button, the edit form is rendered inline, without having to redirect to a separate page.

Notice that the <h1> tag containing the text Editing Task is not showing up. This is because Turbo replaces the existing <turbo-frame> tag with the matching <turbo-frame> tag with the same ID. Everything else is ignored.

What’s more, if you open the Network tab and click Edit, you will see the response is only 2.1 kB, and only contains the template, without any layout. This is because Rails is being smart about it, setting the layout to false if it’s a <turbo-frame> request.

For more details, checkout the frame_request.rb file in the turbo-rails repository.

Upon editing and hitting Save or Never Mind buttons, Turbo conveniently gets the response from the server and replaces the edit form with the now updated task.

Now comment out the CSS that makes the <turbo-frame> tags visible, and our application feels even more impressive.