Skip to content

Commit 50ed0b3

Browse files
authored
Merge pull request #461 from matestack/20201006_action-cable-guide
Add action cable guide
2 parents 8843891 + 62bc78d commit 50ed0b3

File tree

1 file changed

+211
-41
lines changed

1 file changed

+211
-41
lines changed
Lines changed: 211 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,247 @@
11
# Action Cable
22

3-
Websockets can easily be integrated into matestack. Matestack uses Rails ActionCable
4-
for this feature.
3+
[ActionCable](https://guides.rubyonrails.org/action_cable_overview.html#server-side-components-connections) seamlessly integrates WebSockets in Ruby on Rails. It allows for real-time communication between your clients and server. ActionCable and matestack can be combined to emit events using matestacks event hub from the server side, for example triggering a rerendering of a chat view if a new message was created on the server.
54

6-
## Create a Channel on the serverside
5+
In this guide we will provide information on how to create channels, consumers and subscriptions to broadcast messages to all subscribed clients or target specific user via user authenticated connections.
76

8-
`app/channels/matestack_ui_core_channel.rb`
7+
## Setup
8+
9+
The setup differs slightly depending on your usage of websockets or the asset pipeline.
10+
11+
### Websockets
12+
13+
Create a channel using the rails generator. Run the command `rails generate channel MatestackUiCoreChannel`. This will create a `app/javascript/channels/matestack_ui_core_channel.js` file where you can setup your subscriptions. It also generates the corresponding server side `MatestackUiCoreChannel < ApplicationCable::Channel` class.
14+
15+
The `matestack_ui_core_channel.js` is responsible to create a subscription to the "MatestackUiCoreChannel". All we need to do is to tell this channel that it should trigger an event using the `MatestackUiCore.matestackEventHub` with the received data.
16+
17+
`app/javascript/channels/matestack_ui_core_channel.js`
18+
19+
```js
20+
import consumer from "./consumer"
21+
22+
consumer.subscriptions.create("MatestackUiCoreChannel", {
23+
connected() {
24+
// Called when the subscription is ready for use on the server
25+
},
26+
27+
disconnected() {
28+
// Called when the subscription has been terminated by the server
29+
},
30+
31+
received(data) {
32+
MatestackUiCore.matestackEventHub.$emit(data.event, data)
33+
}
34+
});
35+
```
36+
37+
We expect the pushed data to include an _event_ key with the name of the event that should be triggered. We also pass the _data_ as event payload to the event emit, giving you the possibility to work with server side send data.
38+
39+
If you do not want to use the rails generator just create the `matestack_ui_core_channel.js` yourself in `app/javascript/channels/` and paste the above code in it.
40+
41+
### Asset pipeline
42+
43+
Like with websockets you can use the rails generator to create a matestack ui core channel by running `rails generate channel MatestackUiCoreChannel`. This will create a `app/assets/javascript/channels/matestack_ui_core_channel.js` file where you can setup your subscriptions. It also generates the corresponding server side `MatestackUiCoreChannel < ApplicationCable::Channel` class.
44+
45+
```js
46+
App.matestack_ui_core = App.cable.subscriptions.create("MatestackUiCoreChannel", {
47+
connected() {
48+
// Called when the subscription is ready for use on the server
49+
},
50+
51+
disconnected() {
52+
// Called when the subscription has been terminated by the server
53+
},
54+
55+
received(data) {
56+
MatestackUiCore.matestackEventHub.$emit(data.event, data)
57+
}
58+
});
59+
```
60+
61+
We expect the pushed data to include an _event_ key with the name of the event that should be triggered. We also pass the _data_ as event payload to the event emit, giving you the possibility to work with server side send data.
62+
63+
If you do not want to use the rails generator just create the `matestack_ui_core_channel.js` yourself in `app/assets/javascript/channels/` and paste the above code in it.
64+
65+
## Usage
66+
67+
After setting up the client side javascript for our action cable we now take a look at how to create server side events to trigger for example rerenderings of `async`/isolated components or show/hide content with the `toggle` component. We will introduce two different types of creating server side events. First broadcasting events to all subscribed clients and secondly sending events to a user by authenticating a connection through a devise user.
68+
69+
### Broadcast
70+
71+
If you've used the generator to setup your channels you already have a `app/channels/matestack_ui_core_channel.rb`. If not create it now. Inside it we define that every subscriber of this channel should stream from the "matestack-ui-core" channel, which means that anything transmitted by a publisher to this channel is direcetly routed to the channel subscribers.
972

1073
```ruby
1174
class MatestackUiCoreChannel < ApplicationCable::Channel
12-
1375
def subscribed
1476
stream_from "matestack_ui_core"
1577
end
1678

79+
def unsubscribed
80+
# Any cleanup needed when channel is unsubscribed
81+
end
1782
end
1883
```
1984

20-
## Add a Subscription on the client side and link to matestack event hub
85+
Emitting events from controller actions or elsewhere in your Rails application can be done by calling:
2186

22-
`app/assets/javascripts/application.js`
87+
```ruby
88+
ActionCable.server.broadcast('matestack_ui_core', {
89+
event: 'update'
90+
})
91+
```
92+
93+
### User specific broadcast
2394

24-
```javascript
25-
//= require cable
26-
//= require matestack-ui-core
95+
You don't always want to broadcast messages to all other clients. You may only want to broadcast to a specific signed in user or multiple users. We now take a look at sending messages via websockets to an authenticated user using devise. Therefore we need to edit our `ApplicationCable::Connection` to identify connections by a current user.
2796

28-
App.cable.subscriptions.create("MatestackUiCoreChannel", {
29-
received(data) {
30-
MatestackUiCore.matestackEventHub.$emit('MatestackUiCoreChannel', data)
31-
}
32-
});
97+
```ruby
98+
# app/channels/application_cable/connection.rb
99+
module ApplicationCable
100+
class Connection < ActionCable::Connection::Base
101+
identified_by :current_user
102+
103+
def connect
104+
self.current_user = find_verified_user
105+
end
106+
107+
protected
108+
109+
def find_verified_admin
110+
if verified_user = env['warden'].user
111+
verified_user
112+
else
113+
reject_unauthorized_connection
114+
end
115+
end
116+
end
117+
end
33118
```
34119

35-
## Use it on a 'async' component in your response
120+
Every websocket connection that gets established will be authorized by a `current_user`. We check if a user is signed in by accessing `env['warden'].user` which gets set when a user is successfully authenticated. If `env['warden'].user` is not set we reject the connection.
36121

37-
`app/matestack/pages/your_page.rb`
122+
Now we can create a channel which streams for a specific user, enabling us to send broadcasts to all clients which are signed in with this user.
38123

39124
```ruby
40-
# rerender_on: "comments_changed" makes this div listen to
41-
# a event called "comments_changed"
42-
# if fired by the server (see controller action below), the div gets rerendered
43-
async rerender_on: "comments_changed", id: "unique-id" do
44-
div id: "tasks" do
45-
ul class: "mdl-list" do
46-
@comments.each do |comment|
47-
plain comment.content
48-
end
125+
# app/channels/current_user_channel.rb
126+
class CurrentUserChannel < ApplicationCable::Channel
127+
def subscribed
128+
stream_for current_user
129+
end
130+
131+
def unsubscribed
132+
# Any cleanup needed when channel is unsubscribed
133+
end
134+
end
135+
```
136+
137+
Emitting events for a user can now be done anywhere in your Rails application by calling:
138+
139+
```ruby
140+
# sending to the current user
141+
CurrentUserChannel.broadcast_to(current_user, {
142+
event: 'update'
143+
})
144+
145+
# sending to a user
146+
CurrentUserChannel.broadcast_to(User.first, {
147+
event: 'new_message'
148+
})
149+
```
150+
151+
### General broadcast and user specific broadcast
152+
153+
With the above implemented connection authorization in place we will not be able to create connections from clients where no one is signed in. To be able to connect unauthenticated and authenticated users and only give access to authenticated users to specific channels we have to remove the `reject_unauthorized_connection` call from our connection.
154+
155+
```ruby
156+
# app/channels/application_cable/connection.rb
157+
class Connection < ActionCable::Connection::Base
158+
identified_by :current_user
159+
160+
def connect
161+
self.current_user = find_verified_user
162+
end
163+
164+
protected
165+
166+
def find_verified_admin
167+
env['warden'].user
49168
end
50169
end
51170
end
52171
```
53-
`app/controllers/your_controller.rb`
172+
173+
We then can create two different channels. One allows all clients to subscribe to it and another which only allows signed in users to subscribe to it and handles user specific broadcasting.
174+
175+
First a **public channel** to which every user can subscribe.
54176

55177
```ruby
56-
def create_comment
57-
comment = DemoComment.create(comment_params)
58-
59-
unless comment.errors.any?
60-
broadcast
61-
render status: 201, json: { message: "comment created" }
62-
else
63-
render status: 422, json: { message: "comment creation failed" }
178+
# app/channels/public_channel.rb
179+
class PublicChannel < ApplicationCable::Channel
180+
def subscribe
181+
stream_from 'public'
64182
end
65183
end
184+
```
66185

67-
protected
186+
Corresponding front end channel subscription.
68187

69-
def broadcast
70-
ActionCable.server.broadcast("matestack_ui_core", {
71-
message: "comments_changed"
72-
})
188+
```js
189+
// app/javascript/channels/public_channel.js
190+
import consumer from "./consumer"
191+
192+
consumer.subscriptions.create("PublicChannel", {
193+
received(data) {
194+
MatestackUiCore.matestackEventHub.$emit(data.event, data)
195+
}
196+
});
197+
```
198+
199+
Second a **channel only available for signed in users**. We reject clients that try to subscribe but are not signed in by calling `reject` unless a `current_user` is present.
200+
201+
```ruby
202+
# app/channels/private_channel.rb
203+
class PrivateChannel < ApplicationCable::Channel
204+
def subscribe
205+
return reject unless current_user
206+
stream_for current_user
207+
end
73208
end
209+
```
210+
211+
Corresponding front end channel subscription.
74212

75-
#...
213+
```js
214+
// app/javascript/channels/private_channel.js
215+
import consumer from "./consumer"
76216

217+
consumer.subscriptions.create("PrivateChannel", {
218+
received(data) {
219+
MatestackUiCore.matestackEventHub.$emit(data.event, data)
220+
}
221+
});
77222
```
223+
224+
**Broadcasting messages**
225+
226+
With these two channels in place and our connection authentication we can now broadcast messages either to a specific clients with a signed in user or to all clients.
227+
228+
Broadcasting to all clients can be achieved with:
229+
230+
```ruby
231+
ActionCable.server.broadcast('public', {
232+
event: 'update'
233+
})
234+
```
235+
236+
Broadcasting to specific clients with a logged in user can be achieved with:
237+
238+
```ruby
239+
# sending to a user
240+
PrivateChannel.broadcast_to(user, {
241+
event: 'new_message'
242+
})
243+
```
244+
245+
## Conclusion
246+
247+
Creating channels and connections can be done like you want. To learn more about all the possibilities read Rails Guide about [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html). Important for the use with matestack is to emit events in the javascript `received(data)` callback and have a clear structure to determine what the name of the event is which should be emitted. Like shown above we recommend using an `:event` key in your websocket broadcast, which represents the event name that gets emitted through the event hub. You optionally can pass all the received data as payload to that event or also use a specific key. As this is optional you don't need to pass any data to the event emit.

0 commit comments

Comments
 (0)