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.