Let's revisit our view template for the habit counter again. I've also removed the turbo_frame_tag
, we won't need it anymore.
<div class="text-center font-bold text-gray-700" id="habit-name">
<%= @habit.name %>
</div>
<div class="mt-3 flex justify-center items-center space-x-5">
<%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
<div class="text-4xl font-bold"><%= @habit.count %></div>
<%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>
<div class="mt-3 p-2 flex justify-center space-x-1">
<% @habit.count.times do %>
<div class="inline-block border p-1 bg-green-400"></div>
<% end %>
</div>
The component is made up of four elements:
The name of the habit
The buttons to update the count
The habit count
The streak markers
As things stand now, upon clicking the buttons, we're replacing the whole component, including all of the above four elements. However, the only elements that are changing are the habit count (#3) and the streak markers (#4).
It would be nice if we could specifically target those two elements while leaving the other two as they were.
In our small example, this is not a big deal at all, but you could imagine having a large component with a few heavy parts that are updated less frequently and a few parts that update quite often.
If we're replacing the whole component each time an element changes, we have to rebuild all the heavy components that didn't change, which is not very efficient. This problem is similar to our discussion on Turbo Frames, just scoped to the context inside the frame.
Consider another scenario. What if one of the elements that need to be updated after changing the count lies outside the scope of the frame? e.g. ringing a notification at the top-right corner? Turbo Frame can't modify it, as it can only target one frame element at a time.
Turbo Frames won't let us target and update multiple elements on the page.
Turbo Streams solve this problem. Once again, I encourage you to read the Turbo Streams documentation and then return to this section.
In a nutshell,
Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing
<turbo-stream>
elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it.
Turbo Streams let us target and update multiple elements on the page in a single request, which is pretty cool, if you think about it.
We are going to use Turbo Streams to update only the elements that change, namely the habit count and the streak markers.
Step 1: Add IDs to the Elements of Interest
To modify the elements on the page, we need to specify their IDs in the turbo stream response. In our case, we want to update the habit count and the green streak markers. So let's wrap those elements inside separate <div>
tags with IDs.
<div class="text-center font-bold text-gray-700" id="habit-name">
<%= @habit.name %>
</div>
<div class="mt-3 flex justify-center items-center space-x-5">
<%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
<div class="text-4xl font-bold">
<div id="habit-count"><%= @habit.count %></div>
</div>
<%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>
<div class="mt-3 p-2 flex justify-center space-x-1">
<div id="habit-markers">
<% @habit.count.times do %>
<div class="inline-block border p-1 bg-green-400"></div>
<% end %>
</div>
</div>
Step 2: Extract Markers to a Partial
Reusing server-side templates is a major goal of Hotwire.
This step is optional, but it will let us reuse the partial to avoid duplication. You'll soon learn how.
Let's extract the streak markers to a partial named _habit_markers.html.erb
in the app/views/habits
directory.
<div id="habit-markers">
<% habit.count.times do %>
<div class="inline-block border p-1 bg-green-400"></div>
<% end %>
</div>
This is the resulting show.html.erb
view after extracting the partial:
<div class="text-center font-bold text-gray-700" id="habit-name">
<%= @habit.name %>
</div>
<div class="mt-3 flex justify-center items-center space-x-5">
<%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
<div class="text-4xl font-bold">
<div id="habit-count"><%= @habit.count %></div>
</div>
<%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>
<div class="mt-3 p-2 flex justify-center space-x-1">
<%= render 'habit_markers', habit: @habit %>
</div>
The reason we extracted the partial is we want to reuse it inside the turbo stream response.
Step 3: Render Template Containing Turbo Streams
The last step is to create and render a new template called result.turbo_stream.erb
after updating the count.
When submitting a
<form>
element, Turbo injectstext/vnd.turbo-stream.html
into the set of response formats in the request’sAccept
header. It also knows to automatically attach<turbo-stream>
elements when they arrive in response to<form>
submissions that declare a MIME type oftext/vnd.turbo-stream.html
.
class HabitsController < ApplicationController
# ...
def plus
@habit.update(count: @habit.count + 1)
render :result
end
def minus
@habit.update(count: @habit.count - 1)
render :result
end
# ...
end
The result
template contains two <turbo-stream>
elements that replace the habit count and the streak markers, respectively.
<%# app/views/habits/result.turbo_stream.erb %>
<%= turbo_stream.replace 'habit-count' do %>
<div id="habit-count"><%= @habit.count %></div>
<% end %>
<%= turbo_stream.replace 'habit-markers' do %>
<%= render 'habit_markers', habit: @habit %>
<% end %>
Note: I'm using the turbo_stream.replace
helper function provided by the turbo-rails
gem. Check out the turbo-rails
documentation for more details.
Reload the browser, and click the buttons a few times. The counter should be working as expected.
If you open the network tab, you'll notice that the response HTML is even smaller, only containing two <turbo-stream>
tags.
It is pure magic.
That's a wrap. As you saw, it's very easy to progressively enhance your web application as your needs grow.
Turbo Drive gives you the majority of the benefits out-of-box, without having to do anything. When you need more interactivity on your components, you introduce Turbo Frames, and finally, when you need to update multiple elements on your page in a single response, you introduce Turbo Streams.