In this episode we're going to be adding realtime notifications into your app using ActionCable. We've talked about notifications a few times in the past and we used AJAX polling for that. 95% of the time, polling is the solution that would be recommended for it.
But if you're looking for a good introduction into ActionCable then this is a decent one because we're only really using it for one way from the server side to the client side.
So to get started we're starting with an app that has Bootstrap installed and then we created a Main controller with an index view which is where we will list our Notifications as for this example.
Before we generate our channels let's install a few things
Gemfile
# Uncomment out redis, or add it if you don't have it.
gem 'redis', '~> 3.0'
gem 'devise'
Make sure you have redis installed if you haven't already and make sure it is running.
config/cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
Here we've configured the development to use the Redis server running locally now.
Then run bundle install
and restart your rails server so the new gems and the ActionCable configuration will take effect.
We need to then install our users with rails g devise:install
and rails g devise User
Now lets generate our Notifications model
rails g model Notification user:references recipient_id:integer action notifiable_type notifiable_id:integer
Run your migrations with rails db:migrate
if you're on Rails 5 or above or use rake db:migrate
if you're on Rails 4.2 or below.
We're going to be referencing two users in this model because the user reference will be who did the action and the recipient_id will be who is getting the Notification.
app/models/user.rb
has_many :notifications, as: :recipient
app/models/notification.rb
belongs_to :user
belongs_to :recipient, class_name: "User"
belongs_to :notifiable, polymorphic: true
Now you can restart your rails server
rails server
app/views/main/index.html.erb
<div id="notifications">
</div>
That will be where we insert the notifications when they show up live. Now we'll generate a channel for the Notifications
rails g channel Notifications
We'll use this to scope by the user so that when you join you'll only receive your notifications.
app/channels/notifications_channel.rb
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications:#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
end
Now we need to go into the connection class for ActionCable and make the current_user work with ActionCable. So let's open that up and modify it to look like so
app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verfied_user
end
protected
def find_verfied_user
if current_user = env['warden'].user
current_user
else
reject_unauthorized_connection
end
end
end
end
This prevents users from connecting to the websockets without a user account logged in.
app/assets/javascripts/channels/notifications.js
App.notifications = App.cable.subscriptions.create("NotificationsChannel", {
connected: function() {
// Called when the subscription is ready for use on the server
},
disconnected: function() {
// Called when the subscription has been terminated by the server
},
received: function(data) {
// Called when there's incoming data on the websocket for this channel
$("#notifications").prepend(data.html);
}
});
Now we can jump into the console with rails console
and run
ActionCable.server.broadcast "notifications:1", {html: "<div>Hello world</div>"}
Now we just need to send over the HTML over the websockets and the app will render it onto the page.
We can use the new ApplicationController.render functionality to accomplish this.
So you'll want to make a second user if you don't already have more than one, and then in the rails console you can run
Notification.create(recipient: User.first, user: User.last, action: "followed", notifiable: User.first)
So our last user is following the first as far as the notification is concerned.
Now we need to make the notifications view folders. This is exactly like our refactored notifications episode we did previously. So if you have questions you can go back and check out that episode.
mkdir app/views/notifications/users
app/views/notifications/users/_followed.html.erb
<div><%= notification.user.email %> <%= notification.action %> you!</div>
Now in the console you can run
ApplicationController.render partial: "notifications/#{notification.notifiable_type.underscore.pluralize}/#{notification.action}", locals: {notification: notification}, formats:[:html]
If you run that you'll see it runs and renders the partial to a string that returns that for us. So what we can do is broadcast this to the users. We'll put this in a background job.
We use background jobs because we want them to be done in the background so they don't hang up the site for the users.
rails g job NotificationRelay
app/jobs/notification_relay_job.rb
class NotificationRelayJob < ApplicationJob
queue_as :default
def perform(notification)
html = ApplicationController.render partial: "notifications/#{notification.notifiable_type.underscore.pluralize}/#{notification.action}", locals: {notification: notification}, formats: [:html]
ActionCable.server.broadcast "notifications:#{notification.recipient_id}", html: html
end
end
This job will broadcast the rendered partial to the recipient of the notification.
Now you're wondering how we trigger the job. So let's go back to the console after reloading it and run
notification = Notification.first
NotificationRelayJob.perform_later(notification)
Now you'll see the notification partial rendered on the browser properly. So what's the best way to trigger this NotificationRelayJob? Well one way you could do this is an after_commit hook on the Notification model.
app/models/notification.rb
after_commit -> { NotificationRelayJob.perform_later(self) }
Now if you go test it out and create a new notification you should have the job performing automatically per the after_commit hook. You can find more information on the callbacks here: ActiveRecord Callbacks
This has been a whirlwind tour of ActionCable, there are tons of things to add in to manage things like the front-end. As you may have noticed there seems to be a lot more complexity compared to our previous episodes with the AJAX polling version.
If you enjoyed this episode please remember to give the video a thumbs up or heart it. Thanks for reading and watching everyone!
Great write-up .
I believe
should be
as:
is used for polymorphic associations and is not applicable here. Running @user.notifications will return an error as it will look for recipient_type column in the notifications table which doesn't exist.