Leveraging Turbo 8: Best Additions to Implement in Rails 8 Projects
This article will explore Turbo 8’s powerful features and a little bit of code. I swear, you will be surprised by how little extra code is needed to implement all of this!
We will be starting over but using many of the same concepts from the previous article. Let’s get into it!
Setup
First, we can create a new Rails app from the main
branch to get all of the Rails 8 goodies (though, soon, this will be available through alpha/beta gem releases).
Now, as we will be adding a few features related to user interactions, we will need to create users and a way to log them in. Short of using the new Rails 8 authentication generators, we can add two quick models, a small number of controller methods, and a few view templates.
First, the models via Rails generators:
These generators will create two models and migrations for the Users
and Posts
. Let’s add a few lines to the User model for validation and authentication:
The significant addition here is the has_secure_password
line which includes code to set and authenticate passwords via bcrypt
. Now, on the Post
model, the user reference should exist, and we can add a quick validation:
Finally, here is a quick bin/rails db:migrate
to get those tables created.
Now, we can move on to the controller and views for logging in. We want to make this simple, just for this tutorial, so we’ll handle logging in and logging out, their responsive routes, and some basic views.
First, the routes:
These will be all the routes we need for the whole app: the first three for sessions, a resource helper for posts
, and setting the root route.
Now we can add the SessionsController
at app/controllers/sessions_controller.rb
:
Pretty simple here. If we find a user and that user authenticates with the included authenticate
method from the User
model, we will call sign_in
, which sets a session for the user. Real quick, we can add a helper method in ApplicationController
to use current_user
throughout controllers and views through the app:
Next, we can quickly add a PostsController
that resembles much of what we used in the last tutorial, aside from now having a user_id:
Both the index
and create
actions are reasonably straightforward Rails-like methods for listing and creating Posts
. The last part of the authentication puzzle here would be to add the before_action:require_login
method to the ApplicationController and use the Rails Console to create two users (thus, we skipped building a signup interface).
Lastly, we can go ahead and knock out a few views to get all of the authentication, Bootstrap, and basic Post
markup out of the way
A few code blocks to note:
…will add the user ID to the front end for use in StimulusJS later.
…will allow Turbo 8 to morph the incoming DOM changes sent by Turbo.
Finally, Bootstrap is added over CDN for the simplicity of this tutorial, as well as a simple navbar and alerts.
Ok, three Post
templates, and we’re done with setup.
To display the list of posts, you can create an index.html.erb
file in the app/views/posts
directory. Here’s an example of how the index view could look like:
This code uses the _post.html.erb
partial to render each post in the @posts
collection.
…and then the _form.html.erb
This code uses the form_with
helper to create a form for the @post
object. The text_area
helper creates a textarea input for the post’s message
attribute.
Ok! That’s enough setup code!
Turbo Morph
If you followed along with the previous tutorial, there are only a couple of lines to add to get morphed updates out of the box! Earlier, we pre-empted the code to add with the line to the partial. To finish this off, we will add a line to the Post
model and two through the Post
views.
First, the line to the model:
That’s it! If you create two users in the console, log on in, and then log another in on an incognito tab, you will see each page get updated with new posts from each user!
Presence Channel
Presence channels are a great way to show who is online in your application.
In our case, we will use an indicator next to the users’ email addresses in the Post
card. This indicator will be a simple green dot if the user is online and a red dot if the user is offline.
The first step here will be to include icons to use a circle and then color it based on the user’s presence. We can easily include Bootstrap icons by adding the following line to the application.css
file:
Now, we can generate a quick migration to add an online
boolean to the User
model:
Next, we can add two simple methods to the User
to adjust a user’s presence status using the online
boolean. We will also add a method to broadcast changes to the presence channel after a user’s status changes and check for the online_previously_changed?
(a built-in ActiveRecord field status method) in that method used by the callback:
Finally, we can add a line to the application.html.erb
to listen for changes to the presence channel and then update the _post
partial to include the presence indicator:
The turbo_stream_from:presence
will use the turbo_stream
helpers from the turbo-rails
gem to handle the automatic subscription to the presence channel. The PresenceChannel
argument in this method sets which Channel will be specifically used in the streaming process.
This uses the bi-circle-fill
icon from Bootstrap and changes its color based on the user’s presence status.
Lastly, we can create the PresenceChannel
in the app/channels
directory:
This channel extends and includes some Turbo::Streams
modules, so that it can broadcast to the presence channel and handle the stream name. The subscribed
method will stream from the presence
channel and then call the came_online
method on the current user. The unsubscribed
method will call the went_offline
method on the current user. unsubscribe
is a method that is called when the client disconnects from the channel by closing the window or logging out.
Now, if you tried to use the browser, you would see errors from your channel in the logs, as we have to set up one last piece, the current_user
in the Connection
class. This class is much like an ApplicationController
but for the ActionCable connections. We can add a method to the Connection
class to set the current_user
:
This module will set the current_user
to the user found by the session[:user_id]
, which was set during the login, or reject the connection if the user is not found.
If you were to open two browsers (your current browser window and an incognito would work!) now and log into each user, you would see the presence indicator change in real time!
Typing Indicators
The last feature we’ll build here is a typing indicator, which will show when a user is typing a message, much like you see in Slack. Of course, as this is just a simple app to demonstrate the capability, the typing indicator will be shown to all users but not the user typing.
First, we can generate a channel to handle the typing events and pin a debounce library to handle the typing events in a javascript controller a few files later:
Inside the TypingChannel
, we can add a few methods to handle the typing and typing stopped events from the “Cable” side:
This channel will stream from the typing
channel and then broadcast the typing
and typing_stopped
events to the channel. The typing
event will include the current user’s uid
and user_email
. This information is needed to do two things. One, the user_id
will be used to filter out the currently typing user from seeing the typing indicator. Second, the email address is used to display the message so as not to send the data to the front end differently. The unsubscribed
method was added to the channel by default during the generation of the channel but can be removed as it is not needed.
With that out of the way, the next place to add some code is the markup by adjusting and adding some StimulusJS to the Posts
form:
We replace two lines, adding StimulusJS markup of <div class="form-group"
and action: 'keydown->typing#typing keyup->typing#typingStopped'
for the StimulusJS controller to listen for keydown and keyup events. We add turbo_permanent: true
so that the turbo morph updates will not clear out the form’s current state (i.e., text that has been typed or bound javascript that StimulusJS is handling). Finally, the data-typing-target="display"
will be used to update the message in the view when someone is typing.
Finally, we can add the StimulusJS controller to the app/javascript/controllers
directory:
This controller has many moving parts; we’ll go through them by behavior.
First, we create a getter for the userId
, which will get the current-user-id
from the meta tag in the head of the document, which was added way earlier when we first modified the application.html.erb
. This will be used later in the event filtering process.
Then, in the connect
method, we create a subscription to the TypingChannel
and then listen for the received
event. The received
happens when the broadcast is sent from the TypingChannel
. We’ll come back to the if
statement in a moment.
In the view, we used keydown->typing#typing
to call the typing
function when that event happens in the browser. The typing
function then calls the perform
method on the subscription to send the typing
event to the TypingChannel
. This event triggers the typing
method in the TypingChannel
to broadcast the typing
event to the typing
channel, which is handled by the received
handler.
Additionally, we used keyup->typing#typingStopped
in the view to call the typingStopped
function. Here, we debounce the typingStopped
function to only call it once every 1000 milliseconds or one second. This function will prevent the typingStopped
method from being called too frequently and basically allow the controller to wait for the typing to be stopped. Using a debounce function is a common pattern in JavaScript to prevent a function from being called too frequently. We are using debounce
from the lodash
library to do this and was added in the import statement at the top of the file. The trick to getting the StimulusJS-based keydown event to use the lodash debounce is to set the controller’s typingStopped
function in the initializer to itself inside the debounce function call.
The typingStopped
function then calls the perform
method on the subscription to broadcast the typing_stopped
event to the TypingChannel
much like the same round trip for typing
.
To get the controller ready to display text, we added data-typing-target="display"
in the view to display the typing indicator and then added a static targets = ["display"];
to allow this controller to interact with that element.
Now, finally getting back to the code inside the received
handler, we check if the uid
in the data object does not equal the userId
from the meta tag. If the uid
is not equal to the userId
, then we update the displayTarget
with the user’s email and a message that they are typing. Inside the innerHTML
setter, we use a ternary operator to check if the action
in the data object equals typing
. If it is, we set the innerHTML
to the user’s email and a message they are typing. If the action
is not equal to typing
, then we set the innerHTML
to an empty string which clears the message in the browser.
Now, suppose you were to open two browsers (again, a current browser and an incognito would work!) and log into each user. In that case, you would see the typing indicator change in real time as one user types; the other would see the indicator on their browser!
Conclusion
Anyway, closing out these tutorials always feel weird. Hope you learned something from this and leave a comment with any questions or feedback!