Credited to: Rodrigo Souza
In this guide we'll create a real-time chat application using Rails 8.0.1 and Ruby 3.4.2.
Project Setup
source "https://rubygems.org" ruby "3.4.2" gem "rails", "~> 8.0.1" gem "turbo-rails"
Prerequisites
To get started, ensure you have:
- Proper Action Cable configuration in cable.yml
- Working user authentication system
- Turbo Rails properly installed and configured
This implementation provides a robust foundation for a real-time chat application, leveraging Rails 8's modern features for seamless real-time updates with minimal JavaScript.
Key Technical Aspects
Turbo Streams and Broadcasting
- Turbo Streams: Handles real-time updates through WebSocket connections
- Action Cable: Powers the WebSocket functionality (built into Rails)
- Scoped Broadcasting: Messages only broadcast to specific room subscribers
- Partial Rendering: Keeps code DRY and maintains consistent UI updates
Let's break down the key broadcasting mechanisms:
Room Broadcasting:
broadcasts_to ->(room) { room }
This establishes the room as a broadcast target, allowing Turbo to track changes to the room itself.
Message Broadcasting:
after_create_commit -> { broadcast_append_to room }
This ensures new messages are automatically broadcast to all room subscribers.
JavaScript Integration
- Stimulus: Manages form behavior and DOM interactions
- Minimal JavaScript: Most real-time functionality handled by Turbo
- Automatic DOM Updates: No manual DOM manipulation required
Models
Room Model
class Room < ApplicationRecord has_many :messages, dependent: :destroy validates :name, presence: true, uniqueness: true broadcasts_to ->(room) { room } end
Message Model
class Message < ApplicationRecord belongs_to :room belongs_to :user validates :content, presence: true after_create_commit -> { broadcast_append_to room } end
Controllers
Rooms Controller
class RoomsController < ApplicationController def index @rooms = Room.all end def show @room = Room.find(params[:id]) @messages = @room.messages.includes(:user) @message = Message.new end def create @room = Room.create!(room_params) redirect_to @room end private def room_params params.require(:room).permit(:name) end end
Messages Controller
class MessagesController < ApplicationController def create @message = Message.new(message_params) @message.user_id = session[:user_id] || create_anonymous_user.id @message.save! respond_to do |format| format.turbo_stream end end private def message_params params.require(:message).permit(:content, :room_id) end def create_anonymous_user random_id = SecureRandom.hex(4) user = User.create!( nickname: "Anonymous_#{random_id}", email: "new-email-#{random_id}@test.com", ) session[:user_id] = user.id user end end
Views
Room Index
<div class="container mx-auto px-4"> <h1 class="text-2xl font-bold mb-4">Chat Rooms</h1> <div class="mb-4"> <%= form_with(model: Room.new, class: "flex gap-2") do |f| %> <%= f.text_field :name, class: "rounded border px-2 py-1" %> <%= f.submit "Create Room", class: "bg-blue-500 text-white px-4 py-1 rounded" %> <% end %> </div> <%= turbo_frame_tag "rooms" do %> <div class="grid gap-4"> <%= render @rooms %> </div> <% end %> </div>
Room Partial
<%= link_to room_path(room), class: "block p-4 border rounded hover:bg-gray-50", data: { turbo_frame: "_top" } do %> <%= room.name %> <% end %>
Room Show (Chat Interface)
<div class="container mx-auto px-4" data-controller="reset-form"> <h1 class="text-2xl font-bold mb-4"><%= @room.name %></h1> <%= turbo_stream_from @room %> <%= turbo_frame_tag "messages", class: "block mb-4 h-96 overflow-y-auto border rounded p-4", data: { reset_form_target: "messages" } do %> <%= render @messages %> <% end %> <%= turbo_frame_tag "new_message", target: "_top" do %> <%= form_with(model: [@room, @message], class: "flex gap-2", data: { action: "turbo:submit-end->reset-form#reset" }) do |f| %> <%= f.hidden_field :room_id, value: @room.id %> <%= f.text_field :content, class: "flex-1 rounded border px-2 py-1", data: { reset_form_target: "content" } %> <%= f.submit "Send", class: "bg-blue-500 text-white px-4 py-1 rounded" %> <% end %> <% end %> </div>
Message Partial
<div class="message mb-3"> <strong><%= message.user.email %>:</strong> <%= content_tag :span, message.content, class: "break-words" %> </div>
JavaScript
Reset Form Controller
import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["content", "messages"] connect() { this.scrollToBottom() } reset() { this.contentTarget.value = "" this.scrollToBottom() } scrollToBottom() { this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight } }
Routes
resources :rooms do resources :messages, only: [:create] end root 'rooms#index'
How It All Works Together
-
Room Creation and Listing
- Users can view available rooms on the index page
- Each room is rendered using the
_room.html.erb
partial
-
Entering a Chat Room
- Clicking a room link takes users to the show page
- The show page establishes a Turbo Stream connection
- Existing messages are loaded and displayed
-
Real-time Message Broadcasting
- When a message is created:
- The form submits to MessagesController#create
- Message is saved to the database
- after_create_commit triggers broadcasting
- All room subscribers receive the update
- New message appears instantly for all users
- When a message is created:
-
Form Handling
- The Stimulus controller manages form behavior
- After successful submission, the form is cleared
- The UI remains responsive throughout
Code In Action
You should see something like this in your browser:
The Chat room should be like this:
Deploying the application on Heroku
To deployment on Heroku platform is pretty straighfoward. The prerequisites are:
- Heroku CLI installed
- Git repository initialized
After covering all the prerequisites, let's dive into the steps to the deployment:
- Create a new Heroku application
heroku create your-chat-app-name
- Add the necessary Add-ons
# Add Redis add-on heroku addons:create heroku-redis:hobby-dev # Add PostgreSQL add-on heroku addons:create heroku-postgresql:mini
- Configure RAILS_MASTER_KEY ENV variable
heroku config:set RAILS_MASTER_KEY=$(cat config/master.key)
- Deploy the application
# Push to Heroku git push heroku main # Run database migrations heroku run rails db:migrate
- Request a Web Dyno
heroku ps:scale web=1
- Verify the deployment
Check the logs from the deployment process and open the application:
# Open the application heroku open # Monitor logs heroku logs --tail
Top comments (0)