RubyMine

How to use Turbo in your Rails apps with RubyMine

Hello everyone!

The RubyMine team is constantly striving to provide support for new technologies for Ruby and Rails. One of the most exciting recent additions to Rails is undoubtedly Hotwire, so we’ve prepared an overview of this suite of frameworks and a tutorial on how to use the most important Turbo and Stimulus features in your Rails app with RubyMine. This post covers Turbo; to learn more about Stimulus support, stay tuned for our next blog post.

Hotwire and Turbo

What is Hotwire?

Hotwire simplifies web development by sending HTML instead of JSON over the wire (hence the name: it stands for “HTML over the wire”). This reduces the amount of JavaScript written and sent to the browser and keeps template rendering on the server. Hotwire is made up of several frameworks: Turbo, Stimulus, and Strada. In this post, we’ll take a look at Turbo.

What is Turbo?

Turbo provides simple ways of delivering live partial updates to the page without having to write any JavaScript at all. It also speeds up all link clicks and form submissions in your app thanks to Turbo Drive. In this tutorial, we’ll focus on splitting the pages into separate contexts that can be updated in real time without reloading the full page. For this, we’ll use the other two core concepts of Turbo: Turbo Streams and Turbo Frames.

What are Turbo Frames?

Turbo Frames let you decompose the page into independent contexts. If you wrap a part of your Document Object Module (DOM) inside a Turbo Frame, you can update just that frame without having to reload the rest of the page.

What are Turbo Streams?

Turbo Streams can be used to update parts of the DOM without reloading the entire page. A server can send a Turbo Stream message instead of HTML of the entire page; the message will contain HTML fragments, the target element ID, and the action that will be applied to them (add to the target, remove from the target, update the target, etc.: for a full list, please consult the documentation). Much like Turbo Frames, Turbo Streams let you deliver real-time partial page changes. In some ways, the functionality of Turbo Streams is more robust: they let you change multiple parts of DOM and not just the corresponding frame, as well as perform a variety of updates, such as appending or removing elements.

The turbo-rails gem is shipped by default with Rails 7, so you can start using it in your applications right away!

RubyMine, an IDE for Ruby and Rails developers, provides code insight support for Turbo, such as code completion and navigation, which we encourage you to use as you follow this tutorial.

Tutorial: how to use Turbo in Rails apps with RubyMine

In this tutorial, we’ll show you how to use the basic building blocks of Turbo. We’ll use a sample Rails application that allows users to make accounts, create microposts, follow each other, and read the microposts in a feed.

Clone the sample Rails app

Follow these steps to clone our sample app and get it running:

Step 1. Check out the sample application as described in Check out a project from a remote host (git clone). In the URL field, insert the following address: https://github.com/JetBrains/sample_rails_app_7th_ed.git

Step 2. Specify the Ruby interpreter and install gems.

Alice’s feed in the sample app

How to use Turbo Frames

We can currently delete posts in this sample app. Let’s add editing, too.

  1. Open the file _micropost.html.erb and add an “Edit” link next to the “Delete” link in the timestamp.
<!--_micropost.html.erb-->

<span class="timestamp">
  Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                            turbo_confirm: "You sure?" } %>
    <%= link_to "edit", edit_micropost_path(micropost) %>
  <% end %>
</span>

2. Add edit and update methods to the micropost controller (don’t worry – the routes have already been added to routes.rb).

// micropost_controller.rb

def edit
  @micropost = Micropost.find(params[:id])
end

def update
  @micropost = Micropost.find(params[:id])
  if @micropost.update(micropost_params)
    redirect_to root_url
  else
    render :edit, status: :unprocessable_entity
  end
end

The edit button will redirect us to the micropost/[id]/edit route, which will want to load the view template. Click on the gutter icon next to def edit to create a template file edit.html.erb.

Click the gutter icon to create a view template next to a controller action

3. Add the following code to your microposts/edit.html.erb file:

<!--microposts/edit.html.erb-->

<%= render 'shared/micropost_form'%>

In order to edit a post, we simply need to render the existing partial shared/micropost_form which we already use for creating microposts. If we click on edit now, we’ll be redirected to a new page.

The Edit button takes us to a new page

But what we’d like to do here is to have in-place editing: instead of loading a new page, let’s replace the existing micropost body with a form, update the text, save it – and stay on the same page throughout.

To do that, we can utilize Turbo Frames.

With Turbo Frames, we’ll be able to load the form in place of the micropost, update its content, and then render the updated micropost without having to reload the rest of the page.

4. Add a turbo_frame_tag view helper to _micropost.html.erb:

<!--_micropost.html.erb-->

<%= turbo_frame_tag micropost do %>
  <%= link_to avatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %>
</span>
  <span class="content">
    <%= micropost.content %>
    <% if micropost.image.attached? %>
      <%= image_tag micropost.image.variant(:display) %>
    <% end %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                               turbo_confirm: "You sure?" } %>
      <%= link_to "edit", edit_micropost_path(micropost) %>
    <% end %>
  </span>
<% end %>

We’ll be able to use this frame to specify which parts of HTML should be replaced when the server processes a request. A frame should have a unique ID which is passed as an argument to turbo_frame_tag. It can be a string or a symbol, but in our case, we need it to be unique to make sure we’re working with the right frame, so it needs to depend on the specific record. Luckily, Turbo Frames are very convenient to use: we can just pass the micropost object, and turbo_frame_tag will automatically apply the dom_id helper to it and use that as the frame ID.

Since we’d like to replace our micropost with a form to edit it, we’ll need edit.html.erb to contain a matching Turbo Frame.

5. In microposts/edit.html.erb, add the turbo_frame_tag helper:

<!--microposts/edit.html.erb-->

<%= link_to "Back", root_path %>
<%= turbo_frame_tag @micropost do %>
  <%= render "shared/micropost_form", micropost: @micropost %>
<% end %>

Please note that the frame IDs passed to the turbo_frame_tag helper should match on both pages.

Now, when we click on the Edit link, the controller responds to the microposts/[id]/edit request with a Turbo Frame. The content of this new Turbo Frame replaces the old content, and we now see a micropost form instead of the micropost content and can edit its text. When we click on Post and send a request to the microposts/[id]/update route, the controller once again sends us a response containing Turbo Frames; Turbo finds the matching frame and uses its contents to replace the existing ones: in this case, the updated micropost. You can check the console to see how Rails processes the requests.

Edit with Turbo

Any link or form inside a Turbo Frame will be intercepted and will try to find a Turbo Frame of the same ID as its parent frame or its target (links can target different frames; to do that, add a data: { turbo_frame: [frame id] } as an argument for link_to).

But what if you want to update multiple elements on the page with a single click of a link? Or append or remove elements instead of replacing them? This is where Turbo Streams come in handy.

How to use Turbo Streams

To demonstrate the power of Turbo Streams, we’ll add an unfollow link to each micropost and update the user.following count in real time when the current user unfollows someone from their feed. We’ll also remove all the posts by the unfollowed user.

  1. Open _micropost.html.erb and add an unfollow link to each micropost not made by the current user:
<!--microposts/edit.html.erb-->

<% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                           turbo_confirm: "You sure?" } %>
  <%= link_to "edit", edit_micropost_path(micropost) %>
<% elsif !current_user.nil? && current_user.following?(micropost.user) %>
  <%= link_to "unfollow",
  relationship_path(current_user.active_relationships.find_by(followed: micropost.user), user_to_update: current_user),
  data: {turbo_method: :delete, turbo_confirm: 'Are you sure?'} %>
<% end %>

Where does the path come from? The current app already implements functionality that allows users to follow and unfollow each other; moreover, this functionality even uses Turbo Streams. To achieve a better understanding of Turbo, we’ll take that implementation up a step and allow users to unfollow other users not just from the user page, but also from their feed.

The User model maintains the list of all the users a user follows (active_relationships) and all the users that follow that user (passive_relationship). To unfollow a user, we need to destroy this active_relationships association between the two users; therefore, we need to obtain the correct path and set the link to use the DELETE method (which, thanks to Turbo, we can do out of the box).

Let’s see what happens when we click on unfollow:

Unfollow: initial implementation

Not only are the posts of the user we just unfollowed still present on our feed, but the counter update, although live, is also wrong.

To fix this, let’s first take a look at the controller that manages relationships.

2. Open the file relationships_controller.rb.

The current code is as follows:

# relationships_controller.rb

@user = Relationship.find(params[:id]).followed
current_user.unfollow(@user)
respond_to do |format|
  format.html { redirect_to @user, status: :see_other }
  format.turbo_stream
end

It is designed for unfollowing users from the Users page of the sample app: as the current user, you unfollow another user and then see their follower count decrease. However, in our case, we’d like to unfollow a user from our feed page and then see the current user’s following count decrease. To do that, we just need to know which user (current or unfollowed user) we’re about to update the counters for.

3. Open the file routes.rb and add a route with an extra parameter that will let us pass the ID of the user for whom we want to update the counter:

# routes.rb

resources :relationships, only: [:create, :destroy]
delete '/relationships/:id/:user_to_update', to: 'relationships#destroy_with_counter_update

Now that we’re passing a parameter and using a custom route, we’ll need to update the path we’ll be using for the unfollow link:

<!--microposts/edit.html.erb-->

<%= link_to "unfollow", "/relationships/#{current_user.active_relationships.find_by(followed: micropost.user).id}/#{current_user.id}/",
                data: {turbo_method: :delete, turbo_confirm: 'Are you sure?'} %>

We can now retrieve the user from the parameters when updating relationships_controller.rb.

4. Add the following code to relationships_controller.rb:

# relationships_controller.rb

def destroy_with_counter_update
  relationship = Relationship.find(params[:id])
  @unfollowed_user = relationship.followed
  current_user.unfollow(@unfollowed_user)

  @user = User.find(params[:user_to_update])
  @microposts = Micropost.where(:user_id => @unfollowed_user)

  respond_to do |format|
    format.html { redirect_to @unfollowed_user, status: :see_other }
    format.turbo_stream
  end
end

This method handles unfollowing users from the feed and immediately updating the following counters.

By this point, you may be wondering what format.turbo_stream is and what this method does.

Turbo interjects its special turbo_stream format into the Accept header of the request (this is done by default if the form is submitted with a POST, PUT, PATCH, or DELETE method, but you can manually add this format to your GET requests as well; see the documentation). The controller can then send a response in this format or ignore it and fall back on HTML. This is exactly what’s happening in the code for RelationshipsController#destroy: the controller can send responses in both HTML and Turbo Stream formats.

format.turbo_stream can render the response inline in the controller:

format.turbo_stream { render turbo_stream: turbo_stream.update "following", @user.following.count }

If you’d like to update multiple targets, it’s preferable to use the corresponding view template with the extension .turbo_stream.erb.

5. Create a file named relationships/destroy_with_counter_update.turbo_stream.erb and paste the following code there:

<%= turbo_stream.update "followers" do %>
  <%= @user.followers.count %>
<% end %>
<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>
<%= microposts = Micropost.where(:user_id => @unfollowed_user) %>
<% microposts.each do |post| %>
  <%= turbo_stream.remove post %>
<% end %>

Let’s take a closer look at the code that updates the Following count:

<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>

What do these lines do?

The controller needs to send some HTML in response to a request. Turbo lets us send fragments of HTML without sending the entire page. In this case, only those fragments are updated, without a full page reload. These lines specify the Turbo Streams message that the controller will send.

The HTML for those messages takes on the following form:

<turbo-stream action="[action name]" target="[target element or Turbo Frame ID]">
  <template>
    The contents of this tag will be applied to the target element as per the specified action, e.g. deleted, appended, replaced, etc.
  </template>
</turbo-stream>

Turbo Streams let the users execute eight actions in total: append, prepend, replace, update, remove, before, after, and morph. For more information, please consult the documentation.

Let’s take a look at the code we added to destroy.turbo_stream.erb again:

<%= turbo_stream.update "following" do %>
  <%= @user.following.count %>
<% end %>

We want to perform the update action on the following target (you may find the DOM element with this ID in the shared/_stats.html.erb partial), and we want the target to be updated with the @user.following.count HTML.

Now let’s take a look at the code that removes all the posts by the unfollowed user from the feed:

<%= microposts = Micropost.where(:user_id => @unfollowed_user) %>
<% microposts.each do |post| %>
  <%= turbo_stream.remove post %>
<% end %>

Unfollow with Turbo Streams

As you can see, now we can unfollow users right from the feed. The following count will update correctly and in real time, and the posts will disappear from the feed – all without a full-page reload!

How to use broadcasting

Another way to achieve this is broadcasting.

Turbo Streams can be broadcast from models upon updates to the record. There are several basic actions that can be broadcast: remove, replace, append, prepend, before, after, and update. For more information, please consult the documentation.

In our case, we’d like to broadcast updates when one user unfollows another – that is, when an instance of Relationship is destroyed.

Firstly, we’ll need to destroy the record in RelationshipsController#destroy.

  1. Open relationships_controller.rb, navigate to the destroy_with_counter_update method, and add the following line:
#relationships_controller.rb

relationship.destroy!

Since broadcasting is done in a model callback, we’ll need to explicitly destroy the record first.

2. Open the file relationship.rb and add the following line:

# relationship.rb

after_destroy { broadcast_update_to "following_stream_#{follower.id}", target: "following", html: "#{follower.following.count}" }

after_destroy is the callback that executes after a Relationship record has been destroyed. broadcast_update_to is the method that allows us to broadcast the update action to Turbo Streams. You can broadcast other actions, too:

Completion for broadcast

Let’s take a look at the arguments to broadcast_update_later_to. First, we have to specify the stream we’ll be broadcasting to. In our case, we’d like the stream name to depend on the user ID to ensure we’re broadcasting the right information to the right users; otherwise, we might, for example, decrease the counter for all the users and not just for the current one. Therefore, let’s name our stream following_stream_#{follower.id}. We also have to specify the target: it’s still the same DOM element with the ID following. Finally, we need to provide the HTML fragment to be rendered. Frequently, it is a partial view, in which case the arguments would look like this:

after_destroy { broadcast_update_later_to "[stream name]", target: "[target ID]", partial: "[partial name]", locals: { ... } }

In our case, it is enough to pass the HTML.

In order to receive broadcasts, we have to subscribe to streams of the corresponding name in the view files.

3. Open the file shared/_stats.html.erb. After the first line, add the following:

<!--shared/_stats.html.erb-->

<% @user ||= current_user %>

<%= turbo_stream_from "following_stream_#{@user.id}" %>

And don’t forget to remove the code we added to destroy.turbo_stream.erb that updates the following counter, since we’re now updating it via broadcasting instead.

Now, if we load the page and unfollow a user, we’ll see the counter being updated in real time.

When should we use broadcasting instead of sending Turbo Streams from the controller?

You should use broadcasting if: 

  • The update should be visible not just to the current user, but to everyone viewing the same page 
  • The update is asynchronous

If neither of those is required, you can use Turbo Streams from the controller.

Conclusion

In this tutorial, we’ve explored the Turbo framework and its core concepts: Turbo Streams and Turbo Frames. We’ve learned to use Turbo Frames and Turbo Streams in our Rails applications to deliver real-time partial updates to the pages.

Turbo Frames let us deliver partial live updates to certain DOM elements, while Turbo Streams let us target multiple elements and perform a variety of actions. However, sometimes we might want more interactivity than Turbo Streams allow, and thus, we must turn to JavaScript.

Luckily, Hotwire provides another component to do just that: Stimulus.

For more information about Stimulus, please stay tuned for our next blog post.

Take advantage of Hotwire support in your favorite JetBrains IDE for Ruby and Rails. Download the latest RubyMine version from our website or via the freeToolbox App.

To learn about the newest features as they come out, please follow RubyMine on X.

We invite you to share your thoughts in the comments below and to suggest and vote for new features in the issue tracker.

Happy developing!

The RubyMine team

image description