Using Action Cable with Stimulus
Action Cable integrates WebSocket functionalities into Rails. With the help of Action Cable, one can update the page of a browser client without the client making a request first. This comes in handy for all kind of real-time applications, for example chat apps or for notifications.
By using Stimulus to grab the data that is sent by Action Cable, you get all the advantages that Stimulus provides, mainly readable and structured JavaScript.
What we will build
This is a simple example, where any connected browser client can create a new message. The goal is to update the page for all clients, whenever a new message is created by any of the clients. By using Action Cable, this can happen in real-time, without a page reload.
The Channel
For this example a channel name MessageChannel
is used. The boilerplate for the
channel can be generated with rails generate channel Message
.
In Rails 6 this creates two files, app/channels/message_channel.rb
for server-side code
and app/javascript/channels/message_channel.js
for client-side code.
For this example the MessageChannel
sets up all subscribed clients to stream_from 'message_channel'
.
1
2
3
4
5
6
7
8
9
class MessageChannel < ApplicationCable::Channel
def subscribed
stream_from 'message_channel'
end
def unsubscribed
stop_all_streams
end
end
app/channels/message_channel.rb
The following JavaScript file is generated. We will not use it though and instead use a Stimulus controller for the client-side code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import consumer from "./consumer"
consumer.subscriptions.create("MessagesChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
}
});
app/javascript/channels/message_channel.js
The Stimulus Controller
The Stimulus controller works similar to the one from the
previous post.
It has an input
target, which represents the form field to create a new message, and
a messages
target, which is the element containing all the messages.
In the connect
method the channel subscription is created, similar to how it is done
in the generated JavaScript file. For each of the connected
,
disconnected
and received
methods, a class method is defined on the Stimulus
controller and passed in when creating the subscription.
The received
method is the only one that is actually used in this example. It
is supposed to be called with an object that contains a new rendered message
on the message
key, which is then appended to the messagesTarget
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Controller } from 'stimulus';
import consumer from '../channels/consumer';
export default class extends Controller {
static targets = ['input', 'messages'];
connect() {
this.channel = consumer.subscriptions.create('MessageChannel', {
connected: this._cableConnected.bind(this),
disconnected: this._cableDisconnected.bind(this),
received: this._cableReceived.bind(this),
});
}
clearInput() {
this.inputTarget.value = '';
}
_cableConnected() {
// Called when the subscription is ready for use on the server
}
_cableDisconnected() {
// Called when the subscription has been terminated by the server
}
_cableReceived(data) {
// Called when there's incoming data on the websocket for this channel
this.messagesTarget.innerHTML += data.message;
}
}
app/javascript/controllers/message_list_controller.js
This Stimulus controller is used in the index view, the view that is shown in the video. It sets up the required targets and adds an action to clear the input after successful form submission.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<h1>Messages</h1>
<div data-controller='message-list'>
<div data-target='message-list.messages'>
<%= render @messages %>
</div>
<%= form_with(model: Message.new,
data: { action: 'ajax:success->message-list#clearInput' }
) do |form| %>
<%= form.text_area :content, data: { target: 'message-list.input' } %>
<%= form.submit style: 'display: block' %>
<% end %>
</div>
app/views/messages/index.html.erb
Adding a New Message
When the form is submitted to create a new message, the create
action of the
MessagesController
is called.
1
2
3
4
5
6
7
8
9
10
11
12
class MessagesController < ApplicationController
def index
@messages = Message.all
end
def create
@message = Message.new(params.require(:message).permit(:content))
@message.save!
ActionCable.server.broadcast('message_channel', message: (render @message))
head :ok
end
end
app/controllers/messages_controller.rb
It first creates a new message and saves it. Then to update the page on all clients,
a broadcast on the message_channel
is made on line 9. There, the rendered message partial is
sent along under the message
key. This will in turn call the _cableReceived
method
of the aforementioned Stimulus controller on all subscribed clients.
The controller then simply responds with head :ok
as no redirect has to be made
and nothing has to be rendered.
Discussion and feedback