Skip to content

Blogging On Rails

Everything on Rails!

HOTWire: Considering Morphing or Turbo Frames

With the new morphing features in Turbo 8, you now need to decide on when to use Turbo streams or Turbo frames instead of full page refreshing. Thankfully, all three techniques work together. Let’s take the Todo app that we’ve been working on, and see where using Streams or Frames makes sense.

Turbo Streams for a new Todo form

The current new Todo interaction is a new page when clicking the New Todo button on the top of the page. If we add the data-turbo-stream attribute, the GET request now appears to the server as a TURBO_STREAM request. The response, new.turbo_stream.erb can append the form to the bottom of the list of Todos, and then when the form is submitted, the full page refresh will display the new Todo in the same place.

First, add the data-turbo-stream to the New Todo link on todos\index.html.erb:

<%= link_to "New todo", new_todo_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium", data: {turbo_stream: true} %>

Then add the file todos\new.turbo_stream.erb:

<%= turbo_stream.append "todos" do %>
  <%= turbo_frame_tag 'new_todo', target: "_top", class: "flex items-center py-3 pl-3 bg-white border-b pr-11 gap-x-4 todo" do %>
    <%= render partial: 'form', locals: { todo: @todo } %>
  <% end %>
<% end %>

This will append the new form to the bottom of the Todos list. The target is set to _top so that the successful creation and redirection bring the page back to the index action. Otherwise, since there wouldn’t be a Turbo Frame named new_todo, Turbo wouldn’t have anything to replace the interior content.

Now we are using Turbo Streams to load a new form. The form, todos\_form.html.erb, should be changed to fit inline:

<%= form_with(model: todo, class: "flex-1 flex flex-row gap-x-4 items-center") do |form| %>
  <%= form.check_box :completed, class: "ml-8" %>
  <div class="flex-1">
    <%= form.text_field :name, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 w-full" %>
  </div>
  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>

And you get a nice new Todo form. Clicking the Create Todo button saves the Todo, performs a redirect to the Todos#index, which Turbo morphs and shows the new Todo at the bottom of the list.

Editing in place

Using Turbo Frames, we don’t have to make many other changes to get in place editing of each Todo. First, change the div of each Todo in todos\_todo.html.erb to a turbo_frame_tag. The original:

<div id="<%= dom_id todo %>" 
  class="flex items-center py-3 bg-white border-b gap-x-4 todo" 
  data-reorderable-id="<%= todo.id %>" 
  data-reorderable-path="<%= todo_priority_path(todo) %>"
  draggable="true">
  	<!-- rest of partial -->
</div>

becomes:

<%= turbo_frame_tag todo, class: "flex items-center py-3 bg-white border-b gap-x-4 todo", data: { reorderable_path: todo_priority_path(todo), reorderable_id: todo.id }, draggable: true do %>
	<!-- rest of partial -->
<% end %>

The next change is to update the todos\edit.html.erb partial to wrap the form in a turbo_frame_tag. Change the following lines:

    <%= render "form", todo: @todo %>
    <%= link_to "Show this todo", @todo, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>

And wrap the form like this:

    <%= turbo_frame_tag @todo, class: "flex items-center py-3 bg-white border-b gap-x-4 todo"  do %>
      <%= turbo_stream_from @todo %>
      <%= render "form", todo: @todo %>
      <%= link_to "Back", @todo, class: "mr-10 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
    <% end %>

Clicking the “Edit” button will send a request for the edit.html.erb. Turbo will just get the contents of the frame, and replace the inside with the form. Clicking “Back” will make a request to the show.html.erb, which has the same frame id since it renders the _todo.html.erb partial.

Deleting Todos

Lastly, we can use Turbo actions to remove a Todo from the list. Add a delete button to the _todo.html.erb partial:

<%= link_to todo_path(todo), data: {turbo_method: "delete", turbo_confirm: "Are you sure?" }, class: "rounded-full p-2 ml-2 bg-red-500 inline-block font-medium mr-1 text-red-50" do %>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
      <path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd" />
    </svg>
  <% end %>

This uses Heroicons trash icon and you get a nice delete button.

Clicking the trashcan will send the request to the destroy method on the TodosController, so add a new response in the respond_to block:

format.turbo_stream { render turbo_stream: turbo_stream.remove(@todo) }

This will send a response back that removes the Todo from the list.

Putting it all together

Turbo Morphs fits right on top of how an existing app works. It layers on a new performance feel without needing to rewrite the who app to work with a new feature. That’s the best kind of upgrade.

You can find the source code on Github here.

Animating the Insertions and Deletions

You can animate the additions and removals from the page easily. Check out the tutorial here.

Looking to make your Rails app more interactive?

Subscribe below, and you won’t miss the next Rails tutorial, that will teach you how to make really interactive web apps that just work.

We won’t send you spam. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *