I had been using Turbo Frames wrong
"It’s good practice to start your interaction design without Turbo Streams."
That is a verbatim quote from the Turbo Handbook. The full quote reads:
"It’s good practice to start your interaction design without Turbo Streams. Make the entire application work as it would if Turbo Streams were not available, then layer them on as a level-up."
I had been doing the exact opposite. Rather than starting my interaction design without Turbo Streams, I had been starting it with and basically only using Turbo Streams.
And from what I've seen in a lot of other articles or video tutorials, many other people are doing the same and teaching it as the way to use Turbo Frames when in fact they're barely using Turbo Frames at all.
Turbo Frames and Turbo Streams are not the same thing.
The power of Turbo Frames
I had been missing out on the magic of Turbo Frames and writing unnecessary code.
For example, let's say I have a Rails app with posts and on a post can be many comments.
The wrong way
In order to edit one of these posts in my Rails app, I would do the following:
# THE WRONG WAY
# # # # # # # # # # # # # # # # # #
# views/posts/show.html.erb
# #
<div>
<%= @post.content %>
</div>
<% @comments.each do |comment| %>
<div id="<%= dom_id comment %>">
<p><%= comment.content %></p>
<= link_to "Edit", edit_comment(comment) %>
</div>
<% end %>
# # # # # # # # # # # # # # # # # #
# controllers/posts_controller.rb
# #
class CommentsController < ApplicationController
def edit
@comment = Comment.find(params[:id])
end
end
# # # # # # # # # # # # # # # # # #
# views/comments/edit.turbo_stream.erb
# #
<%= turbo_stream.update, template: "comments/edit" %>
# # # # # # # # # # # # # # # # # #
# views/comment#s/edit.turbo_stream.erb
# #
<div id="<%= dom_id comment %>">
<%= form_with model: @comment do |form| %>
<%= form.text_area :content %>
<%= form.submit %>
<% end %>
In this example, I reach for Turbo Streams right away, and that's the problem. There's no issue using Turbo Streams when its needed, but when its not, you end up writing unnecessary code that your Turbo Frames could be handling.
The better way using Turbo Frames
Here's how I would implement the above example using Turbo Frames:
# THE BETTER WAY
# # # # # # # # # # # # # # # # # #
# views/posts/show.html.erb
<div>
<%= @post.content %>
</div>
<% @comments.each do |comment| %>
<%= render template: "comments/show", locals: { comment: comment } %>
<% end %>
# # # # # # # # # # # # # # # # # #
# controllers/comments_controller.rb
class CommentsController < ApplicationController
def edit
@comment = Comment.find(params[:id])
end
end
# # # # # # # # # # # # # # # # # #
# views/comments/show.html.erb
<%= turbo_frame_tag dom_id(comment) do %>
<p><%= comment.content %></p>
<= link_to "Edit", edit_comment(comment) %>
<% end %>
# # # # # # # # # # # # # # # # # #
# views/comments/edit.html.erb
<%= turbo_frame_tag dom_id(@comment) do %>
<%= form_with model: @comment do |form| %>
<%= form.text_area :content %>
<%= form.submit %>
<% end %>
<% end %>
In this example, <%= turbo_frame_tag dom_id(@comment) do %>
renders <turbo-frame id="comment_1">
. And by doing so, when we click on a link within this Turbo Frame, Turbo will look for a matching Turbo Frame with the same ID in the response.
This allows us to omit any turbo_stream.erb
files. Instead, Turbo will see that there is a matching <turbo-frame id="comment_1">
in the response and will replace the frame accordingly.
This also means we're essentially all set up for our edit form as well. All we need is the controller action for comments#update
:
# # # # # # # # # # # # # # # # # #
# controllers/comments_controller.rb
class CommentsController < ApplicationController
def edit
@comment = Comment.find(params[:id])
end
def update
@comment = Comment.find(params[:id])
@comment.update(comment_params)
render template: "comments/show", locals: { comment = @comment }
end
end
Since the form submission occurred within a Turbo Frame, Rails will respond and render comments/show.html.erb
, match the Turbo Frames to the corresponding turbo frame ID, and update the frame.
This allows us to update only the frames being changed without having to reload the entire page.
If we decide later that we want to be able to update multiple parts of the page rather than just the one frame, we can simply update our code to use Turbo Streams and target changes more specifically.
This makes setting up basic CRUD operations with Turbo just as simple as if you aren't using Turbo.