I’d consider our simple email client fairly complete in regards of its functionality. It can send and receive emails, group emails to related topics and work with attachments. That’s about it if you want a working email client. But this is a Rails tutorial and I want to push it forward few more steps. And this step is all about adding reactivity to our application.
TLDR: You can find the code in this GitHub repository. Every part has a corresponding commit.
By interactivity, I mean our application reacting to events without the need to refresh whole page. And we can achieve that using Turbo, specifically Turbo Streams and broadcasts in its Rails version. And it should be fairly simple!
First, we need to think about what events we want to propagate to the client (that would be browser) without the need of refreshing whole page. First thing that comes to mind is event of receiving new email. In our case, that either creates new Topic
or adds Message
to existing one in case of a reply. There’s also a possibility user has Topic
detail already opened. In that case, we want to append new Message
to that page.
We need to add few turbo_frames
to our HTML. Frames are blocks of HTML code Turbo can interact with. Frames can be replaced or removed completely, or content can be added to them. Each frame has its unique name to address it. We have to add turbo_stream
tags to our templates in a similar fashion. Adding a turbo_stream
tag connects the page to the stream through socket that’s listening to any Turbo broadcasted message. That way, we can propagate events to every client connected to the stream without any action required from the client itself.
Note: There’s also approach where you can return Turbo Stream response directly from Rails controller without leveraging Turbo Rails broadcasts. Naming here is little confusing. For Turbo itself, stream is just HTML response with control block to describe required action (i.e. append). Although Turbo Rails calls it broadcast, you have to connect to a stream to receive broadcasted messages. In this tutorial, we won’t be using Turbo Stream responses directly.
Let’s tackle Topics first.
1
2
3
4
5
6
<div id="topics" class="min-w-full">
<%= turbo_stream_from :topics %>
<%= turbo_frame_tag :topics , target: '_top' do %>
<%= render @topics %>
<% end %>
</div>
And to the broadcasts setup itself.
1
2
3
4
5
6
7
8
9
10
11
12
class Topic < ApplicationRecord
# ...
after_create_commit -> { broadcast_prepend_to :topics }
after_update_commit -> do
broadcast_remove_to :topics
broadcast_prepend_to :topics
end
after_destroy_commit -> { broadcast_remove_to :topics }
# ...
end
There’s too little code yet so many things happen! Those after_...
callbacks are called automatically after Topic
record is created/updated/destroyed and broadcasts out Turbo Stream response to every client connected to appropriate stream, in this case :topics
. prepend
(append, replace and many more) actions renders Topic
itself (using app/views/topics/_topic.html.erb
) template and prepends it to the topics
block defined by a turbo_frame
tag. So whenever new Topic
record is created (new email was received), after_create_commit
callback is called, broadcasting broadcast_prepend_to
resulting in new Topic appearing in a Topics list for everyone with the page opened. It’s that simple!
Opposite action is broadcasted if Topic
is deleted. For Turbo to know which Topic to remove or update, every Topic is rendered with unique dom_id
. You can check the template and find the ID easily on a first line.
after_update_commit
callback has little more fines. If we can update or replace Topic on the page directly, why remove it first and add it back right away? That’s because we want new or updated Topic
to appear right on top of the Imbox list. As it would if the page is first rendered. There’s no easy way to tell Turbo to update contents of the block and move it around, so that’s our workaround to that little problem.
We can use similar approach to Messages
as we want Message
to append, be updated or removed from Topic detail list.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
<%= turbo_stream_from @topic %>
<%= turbo_frame_tag @topic, target: '_top' do %>
<div class="flex justify-between items-center mb-5">
<h1 class="h1-main"><%= @topic.subject %></h1>
<%= link_to 'Inbox', topics_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
</div>
<%= turbo_frame_tag :messages do %>
<% @topic.messages.latest_first.each do |message| %>
<%= render message %>
<% end %>
<% end %>
<div class="inline-block ml-2">
<%= button_to 'Destroy this topic', topic_path(@topic), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
</div>
<%= link_to 'Back to topics', topics_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<% end %>
...
You’ve probably noticed, that we don’t use named stream and turbo_frame
in Topic detail page. Instead we use @topic
as a name. Rails generates name for us from that ActiveRecord
instance. This way we connect to a stream belonging to specific Topic record. We will levarage it in Message
callbacks broadcasting to specific Topic
record. If we’d used general name like :messages
for our Stream name, those broadcasts would propagate to every Topic resulting in new Message appearing in every opened Topic detail page instead of a Topic related to the Message.
1
2
3
4
5
6
7
8
9
class Message < ApplicationRecord
# ...
after_create_commit -> { broadcast_prepend_to topic }
after_update_commit -> { broadcast_replace_to topic }
after_destroy_commit -> { broadcast_remove_to topic }
# ...
end
As mentioned above, we don’t use general name of a Stream to broadcast to, instead we use related Topic
record as a name of stream. Callbacks are also simplified compared to Topic
callbacks as we don’t want to move updated Message
on top of the list. There’s also no way Message
record could be updated in real world, but we may update it in a database manually and see changes being propageted. And that’s what you should do next. Fire up Rails console and try creating new Topic/Message records and observe how those records are being automatically added to the page.
Conclusion
This part of the tutorial could probably be the last part. We have a working email client. It can send and receive emails with attachemnts and emails are being groupped into related Topics for better user experience. There’s not much to ask of a simple client. However we may add few more parts to this tutorial exploring more useful Gems such as view_component
. Our app is also already in a need of Rails update since we started this tutorial long time ago. I have also prepared part where we’d deploy our application to Heroku, but since then, Heroku stopped offering free applications so I might explore our options further.
For now I’d like to Thank you for your interrest and hopefully see you in future posts!
Comments powered by Disqus.