How to update different parts of the page using Turbo frame

Turbo Frames is a new (not so new in 2023 actually) concept introduced as the part of Turbo and is designed to improve the perceived performance of web applications built with Ruby on Rails.

With this new feature, it is possible to create complex web applications that have multiple dynamic UI elements that can be updated independently, without reloading the entire page. For example, an e-commerce website can have a shopping cart that is updated dynamically as the user adds items to it, without the need for a full page refresh.

The most straightforward and simple part of the feature are Eager-Loading Frames. Just add a tag with id and src attributes:

<turbo-frame id="tag_1" src="/tags/1"></turbo-frame>

Turbo will call the page /tags/1 , find the turbo-frame tag with id="tag_1" there, and insert its content into the turbo-frame on your page.

But what if I tell you that you can update any part of the page, not just turbo-frame’s content?

To illustrate what I mean let’s imagine a little example: a list of tags with post counters and an indicator of tag trendiness. Everything seems fine, but we don't have a posts counter cache for each tag, and we have to count posts on the fly each time. This calculated value will also be used to show the trendiness indicator.

And this is the part we can make asynchronous using Turbo Frames.

List of Tags

Note: I assume that you are familiar with Ruby on Rails so I’m skipping steps with creating new project, scaffolding views, etc.

The data model for this example is dead simple.

class Tag < ApplicationRecord
  has_many :post_tags
  has_many :posts, through: :post_tags
end
class PostTag < ApplicationRecord
  belongs_to :tag
  belongs_to :post
end
class Post < ApplicationRecord
  has_many :post_tags
  has_many :tags, through: :post_tags
end

Each tag has a /stats endpoint, which we will call to get the posts count and trendiness:

resources :tags do
  member do
    get :stats
  end
end

A template for each tag in the list looks like this. Pay attention to the three turbo-frame tags in the example:

  • The first one has an src attribute and will trigger stats loading once it's rendered on the page.
  • The rest of the tags will be updated by the response from stats_tag_path(tag).
<div id="<%= dom_id tag %>" class="tag">
  <turbo-frame id="tag_stats" src="<%= stats_tag_path(tag) %>"></turbo-frame>

  <h3 class="tag-name">
    #<%= tag.name %>

    <turbo-frame id="<%= dom_id tag, 'trendiness' %>"></turbo-frame>
  </h3>
  <div class="tag-count">
    <turbo-frame id="<%= dom_id tag, 'posts_count' %>"></turbo-frame>
  </div>
</div>

And here is out controller

class TagsController < ApplicationController
  include ActionView::RecordIdentifier
  include ActionView::Helpers::TextHelper

  before_action :set_tag, only: %i[stats show edit update destroy]

  def stats
    posts_count_percentage = @tag.posts.count * 100 / Post.count

    trendiness = if posts_count_percentage > 25
       '<span class="trend-up">⬆</span>'
    elsif posts_count_percentage < 5
       '<span class="trend-down">⬇</span>'
    else
      ''
    end

    render turbo_stream: [
      turbo_stream.replace(dom_id(@tag, 'trendiness'), trendiness),
      turbo_stream.replace(dom_id(@tag, 'posts_count'), pluralize(@tag.posts.count, 'post')),
    ]
  end

  # and other standard CRUD action...
end

The most interesting thing here is

render turbo_stream: [
  turbo_stream.replace(dom_id(@tag, 'trendiness'), trendiness),
  turbo_stream.replace(dom_id(@tag, 'posts_count'), pluralize(@tag.posts.count, 'post')),
]

Instead of the regular HTML render we use turbo_stream format to specify that the response will be a Turbo Stream.

The Turbo Stream response contains an array of operations that will be performed on the client-side view. In this case, the array contains two operations.

First turbo_stream.replace(dom_id(@tag, 'trendiness'), trendiness) replaces the contents of an HTML element with the id dom_id(@tag, 'trendiness') with the value of trendiness. The dom_id method generates a unique id for the element based on the @tag object and the trendiness string.

And the second turbo_stream.replace does pretty much the same.

Overall, this code is sending a Turbo Stream response that updates two elements in the client-side view with new values based on the @tag object and its associated data.

And the result page will look like this. You can see that the posts count and green/red arrows load asynchronously now.

Untitled