Hotwire Turbo: Drive, Frames and Streams for Ruby on Rail projects
Rails Creator, DHH (David Heinemeier Hansson) has shown some adversity towards Javascript and TypeScript in particular over the recent years. He suggests that the struggle to learn another language, which ultimately distracts you from writing functional code, might be a disadvantage in the long run. Therefore, in his company Basecamp, he created a framework that leverages JavaScript internally, so you don’t have to. The goal is to focus solely on your Ruby backend code, your business logic, rather than complex configurations with JavaScript, especially from the SPA (Single Page Application) perspective.
I presented this at the May 2024 session of the Montreal.rb Meetup. Since some people don’t watch videos, or view slides, or they might prefer a written version, this is a summarized version of it. You can bookmark it and return if you forget any concept. However, this is connected to the demo RoR application that I prepared for the talk, download it and try it. I bet the concepts will be very clear once you try and execute the application.
I also created a small demo application connected to SQLite, so you can test it at your own pace to delve deeper into Turbo concepts. I will leave all the relevant links in the next section.
Interesting links
Why avoid using Javascript, my opinion as a current NodeJS developer
It’s true, as a frequent TypeScript developer, you’ll find that every day you have to learn how to organize or type different functions to write some business logic. And I would say that TypeScript is not an easy learning curve for a Junior Developer, especially starting at the build step.
Since TypeScript is not supported in the browser or the Node runtime, you have to learn how to configure your development environment, focusing on the build and watch steps to support TypeScript. There are many preconfigured projects out there, but still, it leaves no option for the beginning developer who may want to start trying TypeScript. And let’s not even start on teaching them about tuples, generics, etc.
So, we sacrifice development speed to add strong typing to Javascript. Don’t misunderstand me, I agree that in the long term it’s better to have strong typing, especially for large and nested JSON objects. But as a starting point, it can discourage beginners from continuing to learn about programming, writing functional code that addresses some real-world problems, and then in the future, they can become experts in their craft. They should avoid creating unnecessary problems for themselves.
Why Turbo?
Turbo is the solution for creating SPA experiences using mostly your Ruby knowledge. You can write applications that can replace many of the features that a SPA would have, such as fast page loading and changing specific parts of the DOM.
One advantage of modern versions of Ruby on Rails, the web framework for creating Ruby applications, is that it comes with Turbo installed by default.
Turbo Drive: Preloading the pages
One of the key factors in choosing a Single Page Application (SPA) is the speed at which each page loads. After the first request is sent to the server, it returns all the JavaScript needed to run in your browser. This is where Turbo Drive comes into play. It helps preload any page from a link when you want to visit it and also keeps a historical track of it.
You can, of course, disable this behavior for specific links, which is useful if one of those links is really expensive to load. Otherwise, since it’s loaded asynchronously, it will give your customers the experience that your app has a fast navigation.
Reload styles or JavaScript assets when they have changed
Now, since you are frequently making changes to your JavaScript code, you can instruct Turbo Drive to reload any JavaScript or CSS Style code when a change is made in any of them. Just be sure to attach the required HTML tags to it.
Progress loading bar in Turbo
Also, you can enable the loading bar again, like in previous versions of Rails. You only need to make some modifications to your code.
Since the progress bar will appear on pages that take more than 500 ms to load, we can set this minimum to 0
. It will then always be displayed.
Let’s use our main JS file to show this feature.
You can also modify the color of the progress bar. Simply update the .turbo-progress-bar
class.
Use from JavaScript
You can also use Turbo Drive functions and properties from JavaScript. Here is an example.
Use from HTML
And here are a couple of examples as HTML code. ERB compatible.
Morphing
We can specify how the page refreshes and updates the content using the following code.
Scroll preservation
Also, within your navigation, you can preserve the scroll position. One example would be going back to a long article. As a user, I would like to return to the same position where I was before visiting a link in the post.
Exclude content from morphing
This is a really interesting feature if you want to keep sections excluded from the morphing behavior. Let’s say, for example, to keep alert messages or information that persists between pages.
Ruby gem’s helper methods
Also, when we use the Turbo Drive gem, we can take advantage of the helpers and functions that the gem provides. Here is a sample list of some of the most commonly used methods.
Testing in Turbo Drive
For testing your code, you can use the same approach as you do for testing your regular Ruby on Rails applications.
For most of the CRUD apps out there, you need to be sure that your application is executing the 4 main actions from it (Create, Read, Update, and Delete).
Check the following code, there is nothing new about Turbo Drive.
Turbo Frames: load other pages within the same page, component’s behavior
Working with Turbo Frames is similar to dealing with components in a Frontend framework.
You can define your partials in another view and then use a parent view to load them.
Once a change is made, you can trigger a refresh or a reload of a target frame.
Example of a Turbo Frame
A Turbo Frame only declared with HTML will look like the next code.
Turbo Frames properties
Turbo Frames allow us to modify their behavior with the following properties:
-
src
: A URL or path that controls the navigation of the element. -
loading
: This has two values,eager
andlazy
.loading="eager"
will immediately load the frame, whileloading="lazy"
will load the frame when it becomes visible. -
busy
: A boolean attribute that indicates if the frame is currently loading. This is managed by Turbo. -
disabled
: This is used for disabling frame navigation. -
complete
: A boolean attribute that indicates if the frame has finished loading. This is managed by Turbo. -
autoscroll
: A boolean attribute that indicates if the frame should scroll to the top after loading. This is managed by Turbo.
Also, since Turbo can be used from JavaScript, remember that you have access to all these properties there as well.
-
FrameElement.src
-
FrameElement.disabled
-
FrameElement.loading
-
FrameElement.loaded
-
FrameElement.complete
-
FrameElement.autoscroll
-
FrameElement.isActive
-
FrameElement.isPreview
Gem usage
If you are using the Gem, you can also use some Turbo Frame helpers as well.
Two Turbo Frames in the same page
We can use multiple Turbo Frames within the same page. I will use the HTML version tag to generate two frames on the same view. The goals are:
- To display a
<turbo-frame/>
form to store tags, with the view pulled from a partial. - To display the list of tags beside it, which will be refreshed when the form is submitted.
- To avoid reloading the page.
The first <turbo-frame>
indicates a src
property, this is the view/partial from which we are pulling the content. In this case, it’s the route that renders the new
view. Here is the content.
As you might see, we have a data: { turbo_frame: TagFrameController::TAG_FRAME_ID }
property in the form. This means, once we process the form, it will render the content in that frame as a target. So, our backend will look like this.
More interesting things about Turbo Frames
I didn’t cover this in my presentation, but you can also do additional things with Turbo Frames, including interesting features like:
- Lazy-loading frames
- Caching
- Cross-Site Request Forgery (CSRF)
- Navigation from a frame
Testing in Turbo Frames
Tests work the same as regular tests for a Rails application. But for this case, check that we are aiming the create and list actions towards the same URL, since both actions will be executed from that route.
Turbo Streams: load only the modified data
In Turbo Frame, you load large chunks of views. In Turbo Streams, you render only the data you really need, specifying its behavior, such as whether to append, prepend, replace, etc.
This feature can optionally be combined with Websockets. Thus, any new information stored in your database will appear in real time to all your users!
However, the Turbo Stream documentation states the following:
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. This means you won’t come to rely on the updates for flows that need to work in native applications or elsewhere without them.
Also, if you use this in Ruby on Rails, you can leverage Action Cable and Active Jobs to render the content as needed. For this example, I am also using the gem for a Ruby on Rails project.
Additional behavior on rendering
If you want to trigger side effects when you perform a Turbo Stream rendering, you might want to use Stimulus controllers. For that, you will need to work with JavaScript files.
Actions
With Turbo Stream, we have eight actions (behaviors) available. These specify how your content will be added to the content already rendered.
Those actions are:
-
append
: Appends the given content to the end of the element specified by the target. -
prepend
: Adds the given content to the beginning of the element specified by the target. -
replace
: Replaces the entire content of the target element with the given content. -
update
: Replaces the target element with the given content. -
remove
: Removes the target element from the DOM. -
before
: Inserts the given content immediately before the target element. -
after
: Inserts the given content immediately after the target element. -
morph
: Replaces content using the morph technique in the target element. -
refresh
: Refreshes the content within the target element.
Shape of a Turbo Stream Render
Now, to allow Turbo Streams to insert that content into your HTML, you must pre-render an element with a target id, such as dom_id
, for example.
This means, if you want to replace or morph a specific element, your HTML should look like this:
But if we want to prepend, append, or perform any similar action to the parent element, we need to add a unique ID.
Then, if you want to create a new record and add it to your current table, the controller code will look like this:
That’s why you need a parent ID. Let’s now see an example where we need to remove a specific element from the table.
Using the Ruby Gem gives us many advantages and provides well battle-tested methods to interact with Turbo.
Testing for Turbo Streams
We need to be very specific about the type of response that we expect from the controller. According to the case, you might need to specify to your test suite that you are expecting a stream object.
For such testing behavior, add as: :turbo_stream
to the end of your route call. See the following example.
Conclusions
Remember that most of the content of this post has been condensed to fit into a one-hour slide presentation. Of course, I won’t cover every aspect, but the goal is to show you the basics so you can start your own adventure practicing and learning Turbo, and use it in your next project.
Use Turbo when you have the chance, especially if you work with a lot of talented Ruby Developers. The shift in context and knowledge will be minimized and the time will be spent on creating more features for the users rather than figuring out how to correctly transpile your TypeScript code and which tool you can use.
Would I recommend developers to stop learning about JavaScript because we have tools like Turbo? Not at all. Rather than that, I encourage them to continue learning, but once they’ve created something, whether it’s at work or on their own projects. JavaScript is widely used around the world and probably by any job that you will look for. They require some basic to intermediate JavaScript knowledge. Also, remember that if you need to do extra stuff with Turbo, sooner or later you will have to work with Stimulus Controllers, which are written in JavaScript.