Adding Client-Side Interactivity to Rails Applications with Turbo Frames and Turbo Streams
Unlock the power of Turbo Frames and Turbo Streams to add seamless client-side interactivity to your Rails applications.
Introduction
Welcome to this comprehensive guide on how to level up your Rails applications! Today, we're focusing on something pretty exciting: adding interactive features without writing a single line of custom JavaScript. Yep, you read that right—no JavaScript required.
We're diving into the world of Turbo Frames and Turbo Streams, two incredible features that let you manage dynamic content smoothly and efficiently. The real-world example we're exploring? A journaling app that allows you to add and manage tags for your entries.
What You'll Learn:
How to make use of Turbo Frames
Implementing Turbo Streams for real-time UI updates
How this fits into a real-world Rails app
Before we dive in, don't forget to watch the accompanying YouTube video to see all these concepts in action.
Ready to jump right in? Let's get started!
Setting Up the Basics
Before diving into the Turbo magic, you need to set up some basics. This section will cover how to wrap the multi-select field in a Turbo Frame and add a manage tags link. We'll also touch on setting up a controller and updating the routing file.
Turbo Frame and Tags Frame
Turbo Frames are a nifty feature that lets you refresh parts of a page without reloading the entire thing. To make this happen for our tags section, you'll:
Locate the multi-select field in your journaling application where tags are selected.
Wrap this field in a Turbo Frame and give it an ID, like
tags_frame
.
By doing this, you'll mark this area of the page for special handling. You can change the content inside this frame without affecting the rest of the page.
<%= turbo_frame_tag "tags_frame" do %>
<div class="field">
<%= form.label :tag_ids, "Tags" %>
<%= form.collection_select :tag_ids,
@journal.tags,
:id,
:name,
{},
{multiple: true} %>
</div>
<% end %>
Adding a Link to Manage Tags
After setting up the Turbo Frame, the next step is to add a link for managing tags. This link should:
Be visible and easily accessible.
Take the user to the section where they can manage their tags.
Remember, you don't want to force the user to go to a different page. The idea is to manage tags right there, in the current context.
<%= turbo_frame_tag "tags_frame" do %>
<div class="field">
<%= form.label :tag_ids, "Tags" %>
<%= form.collection_select :tag_ids,
@journal.tags,
:id,
:name,
{},
{multiple: true} %>
</div>
<%= link_to "Manage Tags",
journal_tags_path(@journal),
class: "link font-medium" %>
<% end %>
Controller and Routing
You can't manage tags without a place to handle the logic. That's where a new controller comes in.
Create a controller specifically for managing tags.
Update your
routes.rb
file to make tags a nested resource under journals.
In terms of routes, you'll be using all the standard RESTful routes except for the show
route. This ensures you have routes for creating, reading, updating, and deleting tags.
Rails.application.routes.draw do
resources :journals do
resources :tags, except: [:show]
end
end
And there you have it! With these basic elements in place, you're now ready to start adding more advanced features to manage tags efficiently.
Implementing Tags Controller
In this section, we will delve into the core component of our feature: the Tags Controller. We'll walk through setting it up, authenticating users, initializing variables, and crafting the index route. Plus, we'll discuss how to create the associated views for each route.
Authentication and Initialization
User Authentication: Before allowing any operations on tags, it's crucial to ensure that the user is authenticated. Devise provides a handy helper method for this purpose.
Setting Instance Variables: Initialize the
@journal
instance variable to capture the context of the journal you're working on. Additionally, for the index route, initialize the@tags
instance variable to hold all the tags related to the current journal.
class TagsController < ApplicationController
before_action :authenticate_user!
before_action :set_journal
def index
@tags = @journal.tags
end
private
def set_journal
@journal = Journal.find(params[:journal_id])
end
end
Index Route
The index route serves as the starting point where users can manage their existing tags.
Creating the View: For the index route, create a corresponding view file. This is where you present the list of tags to the user. Since we're using Turbo Frames, make sure to nest the content inside a Turbo Frame with the same ID as you used earlier (tags_frame
).
<!-- app/views/tags/index.html.erb -->
<%= turbo_frame_tag "tags_frame" do %>
<h2>Manage tags</h2>
<ul class="my-2 divide-y dark:divide-gray-800">
<% @tags.each do |tag| %>
<%= render "tag", tag: tag %>
<% end %>
</ul>
<%= link_to "Done",
new_journal_entry_path(@journal),
class: "link font-medium" %>
<% end %>
Partial for Individual Tags: It's good practice to use partials for reusable components. Create a partial for individual tags, which for now will simply display the tag's name.
<!-- app/views/tags/_tag.html.erb -->
<li>
<div class="py-3">
<%= tag.name %>
</div>
</li>
At this point, you can check your application to see if you can toggle between your journal form and your list of tags without a full-page reload. That's Turbo Frames at work!
And there you have it—your Tags Controller is set up, and your index route is operational. Next up, we'll discuss how to add, edit, and delete tags. But that's a topic for another section.
Implementing the Create Action
In this section, we'll focus on how to bring the tag creation functionality to life in your Rails application. Specifically, we'll explore setting up the "new" action in the tags controller, utilizing Turbo Frame for eager loading, saving the new tag to the database, and issuing a Turbo Stream for a dynamic response.
Setting Up the "New" Controller Action
First off, you'll need to define a "new" action in your tags controller. This action serves as the entry point for users to create a new tag.
class TagsController < ApplicationController
before_action :authenticate_user!
before_action :set_journal
def index
@tags = @journal.tags
end
def new
@tag = @journal.tags.build
end
private
def set_journal
@journal = Journal.find(params[:journal_id])
end
end
Eager Loading the "New" View with Turbo Frame
To make the user experience smoother, we'll use Turbo Frame to eager load the "new" view into the existing page.
In your
index.html.erb
where you display the list of tags, include a new Turbo Frame tag with an ID ofnew_tag
.Use the
src
attribute to set the URL where the "new" view content will be loaded from.
Here's how you wrap it:
<!-- app/views/tags/index.html.erb -->
<%= turbo_frame_tag "tags_frame" do %>
<h2>Manage tags</h2>
<%= turbo_frame_tag "new_tag", src: new_journal_tag_path(@journal) %>
<ul id="tags_list" class="my-2 divide-y dark:divide-gray-800">
<% @tags.each do |tag| %>
<%= render "tag", tag: tag %>
<% end %>
</ul>
<%= link_to "Done",
new_journal_entry_path(@journal),
class: "link font-medium" %>
<% end %>
Creating the Tag and Using Turbo Stream
Once the form is submitted, it's time to implement the create
action. This action handles the incoming form data and attempts to save the new tag to the database. If the save is successful, a Turbo Stream is issued to update the UI dynamically.
In your tags controller, add a
create
action that initializes a new tag and tries to save it.Then, issue a Turbo Stream to update the UI.
class TagsController < ApplicationController
before_action :authenticate_user!
before_action :set_journal
def index
@tags = @journal.tags
end
def new
@tag = @journal.tags.build
end
def create
@tag = @journal.tags.build(tag_params)
@tag.save
turbo_stream
end
private
def tag_params
params.require(:tag).permit(:name)
end
def set_journal
@journal = Journal.find(params[:journal_id])
end
end
Defining Turbo Stream Behavior
The Turbo Stream is defined in a separate view file whose name directly corresponds to the controller action, in this case, create.turbo_stream.erb
.
If there are any errors on the tag, replace the form element with a new form that has the errors displayed.
If the tag is successfully created, append it to the existing list of tags.
<!-- app/views/tags/create.turbo_stream.erb -->
<% if @tag.errors.any? %>
<%= turbo_stream.replace "form_tag" do %>
<%= render "form", tag: @tag %>
<% end %>
<% else %>
<%= turbo_stream.append "tags_list" do %>
<%= render "tag", tag: @tag %>
<% end %>
<%= turbo_stream.replace "form_tag" do %>
<%= render "form", tag: @journal.tags.build %>
<% end %>
<% end %>
This allows you to update specific parts of your page without requiring a full page reload, providing a more interactive user experience.
By combining the "new" and "create" actions with Turbo Frame and Turbo Stream, you're able to add new tags dynamically while keeping the UI fresh and responsive. It’s a powerful example of how Rails 7 and Hotwire can be harnessed to build interactive applications with minimal JavaScript.
Editing and Deleting Tags
In this section, we'll focus on how to edit and delete tags directly in the user interface without requiring a full page reload. This is achieved using Turbo Streams, a powerful feature that makes our application interactive while still staying within the Rails ecosystem.
Adding IDs to List Elements
First, you'll want to add an ID attribute to each tag's list element i.e. the tag partial. This ID is what allows Turbo Streams to target the specific tag for editing or deleting.
We’ll also want to update the list element to show a form inline if the user clicks the link to edit the form. In order to do this hot-swapping inline, we’ll need to wrap the inner contents of the list element within a Turbo Frame.
<!-- app/views/tags/_tag.html.erb -->
<li id="<%= dom_id(tag, :li) %>">
<%= turbo_frame_tag tag do %>
<div class="py-3 flex justify-between">
<%= tag.name %>
<div class="flex space-x-3">
<%= link_to "Edit",
edit_journal_tag_path(@journal, tag),
class: "link font-medium" %>
<%= button_to "Delete",
journal_tag_path(@journal, tag),
method: :delete,
class: "link font-medium" %>
</div>
</div>
<% end %>
</li>
Edit and Update Actions
Next, let's set up the controller actions for editing and updating the tags.
Open your
TagsController
.Add
edit
andupdate
actions.Make use of a helper method
set_tag
to set the tag object based on the ID parameter from the URL.
class TagsController < ApplicationController
before_action :authenticate_user!
before_action :set_journal
before_action :set_tag, only: [:edit, :update]
def index
@tags = @journal.tags
end
def new
@tag = @journal.tags.build
end
def create
@tag = @journal.tags.build(tag_params)
@tag.save
turbo_stream
end
def edit
end
def update
@tag.update(tag_params)
turbo_stream
end
private
def set_tag
@tag = @journal.tags.find(params[:id])
end
def tag_params
params.require(:tag).permit(:name)
end
def set_journal
@journal = Journal.find(params[:journal_id])
end
end
Turbo Stream for Edit and Update
After setting up the actions, the next step is to define the behavior using Turbo Streams.
Create a new Turbo Stream view file corresponding to the update
action.
touch app/views/update.turbo_stream.erb
In this view, specify what happens if the update has errors or is successful:
If there are errors, replace the existing form with a new one that displays the errors.
If the update is successful, replace the list element of that specific tag with the updated tag information, and replace the form with a new tag, effectively clearing the form.
<!-- app/views/tags/update.turbo_stream.erb -->
<% if @tag.errors.any? %>
<%= turbo_stream.replace dom_id(@tag, :form) do %>
<%= render "form", tag: @tag %>
<% end %>
<% else %>
<%= turbo_stream.replace dom_id(@tag, :li) do %>
<%= render "tag", tag: @tag %>
<% end %>
<% end %>
Deleting Tags
Finally, let's implement the ability to delete tags.
Add a destroy
action in your TagsController
.
class TagsController < ApplicationController
before_action :authenticate_user!
before_action :set_journal
before_action :set_tag, only: [:edit, :update, :destroy]
def index
@tags = @journal.tags
end
def new
@tag = @journal.tags.build
end
def create
@tag = @journal.tags.build(tag_params)
@tag.save
turbo_stream
end
def edit
end
def update
@tag.update(tag_params)
turbo_stream
end
def destroy
@tag.destroy
turbo_stream
end
private
def set_tag
@tag = @journal.tags.find(params[:id])
end
def tag_params
params.require(:tag).permit(:name)
end
def set_journal
@journal = Journal.find(params[:journal_id])
end
end
Use Turbo Streams to remove the tag's list element upon deletion.
<!-- app/views/tags/destroy.turbo_stream.erb -->
<%= turbo_stream.remove dom_id(@tag, :li) %>
And that's it! Now you have a functional, interactive system for editing and deleting tags without requiring any custom JavaScript or full page reloads. This is the magic of Turbo Streams in action. For further reading, you may want to check out the official Turbo Streams documentation.
Conclusion
You've just learned how to supercharge your Rails application by adding client-side interactivity without writing a single line of custom JavaScript. Let's quickly recap what you've achieved:
Wrapped form fields with Turbo Frames for isolation.
Added the ability to manage tags right within the journaling context.
Created RESTful routes and controllers for tags management.
Used Turbo Streams to dynamically update the UI.
Implemented server-side validations for added security.
Enabled inline editing and deleting of tags.
All these features were accomplished with native Rails tools like Turbo Frames and Turbo Streams, minimizing the need for additional libraries or custom JavaScript.
The takeaway here is that Rails offers robust ways to make your applications interactive and dynamic. You don't have to break your head over JavaScript libraries or complex frontend frameworks to get the job done.
So go ahead and explore these features more, apply them to your projects, and enjoy the simplicity and power that Rails provides.
Additional Resources
If you're looking to dive deeper into Turbo Frames and Turbo Streams in Rails, I recommend reading David Colby’s blog. He has some good articles that helped me understand Turbo when I was learning it.
In addition, there are a number of go-to resources that can provide you with more insights:
Rails Guides: For a comprehensive understanding of Rails, including its conventions and best practices, Rails Guides are indispensable.
Link: Rails Guides
Hotwire Documentation: If you're particularly interested in Turbo Frames and Turbo Streams, Hotwire's official documentation is your best bet.
Link: Hotwire Docs
Devise Wiki: Since this example used Devise for authentication, you may want to explore the Devise Wiki for further details.
Link: Devise Wiki on GitHub
YouTube Tutorials: There are several YouTubers who provide excellent tutorials on Rails and Hotwire. A couple of good channels to start with are GoRails and Drifting Ruby.
Links: GoRails Channel, Drifting Ruby Channel
Stack Overflow: If you encounter any issues or have specific questions, Stack Overflow's Rails community is pretty active and helpful.
Reddit’s r/ruby and r/rails: These subreddits are great places for discussions, questions, or keeping up-to-date with the latest Rails news.
Feel free to explore these resources to enhance your Rails skills and get a deeper understanding of how to build more interactive and efficient web applications.
Hello, in `app/views/tags/index.html.erb` you have a link named Done with `new_journal_entry_path(@journal)`.
If I get this correctly, what should the @journal be?
In cases when we want to create a new journal, and that would result in the @journal to be nil, how would that work?
Am I missing something?
Suppose you would want to implement that in a more simple example, only containing `journals` and `tags` and even in without nested routes.