-
Notifications
You must be signed in to change notification settings - Fork 328
/
Copy pathbroadcastable.rb
388 lines (349 loc) · 20.6 KB
/
broadcastable.rb
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# Turbo streams can be broadcast directly from models that include this module (this is automatically done for Active Records).
# This makes it convenient to execute both synchronous and asynchronous updates, and render directly from callbacks in models
# or from controllers or jobs that act on those models. Here's an example:
#
# class Clearance < ApplicationRecord
# belongs_to :petitioner, class_name: "Contact"
# belongs_to :examiner, class_name: "User"
#
# after_create_commit :broadcast_later
#
# private
# def broadcast_later
# broadcast_prepend_later_to examiner.identity, :clearances
# end
# end
#
# This is an example from [HEY](https://hey.com), and the clearance is the model that drives
# [the screener](https://hey.com/features/the-screener/), which gives users the power to deny first-time senders (petitioners)
# access to their attention (as the examiner). When a new clearance is created upon receipt of an email from a first-time
# sender, that'll trigger the call to broadcast_later, which in turn invokes <tt>broadcast_prepend_later_to</tt>.
#
# That method enqueues a <tt>Turbo::Streams::ActionBroadcastJob</tt> for the prepend, which will render the partial for clearance
# (it knows which by calling Clearance#to_partial_path, which in this case returns <tt>clearances/_clearance.html.erb</tt>),
# send that to all users that have subscribed to updates (using <tt>turbo_stream_from(examiner.identity, :clearances)</tt> in a view)
# using the <tt>Turbo::StreamsChannel</tt> under the stream name derived from <tt>[ examiner.identity, :clearances ]</tt>,
# and finally prepend the result of that partial rendering to the target identified with the dom id "clearances"
# (which is derived by default from the plural model name of the model, but can be overwritten).
#
# You can also choose to render html instead of a partial inside of a broadcast
# you do this by passing the `html:` option to any broadcast method that accepts the **rendering argument. Example:
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message_count
#
# private
# def update_message_count
# broadcast_update_to(user, :messages, target: "message-count", html: "<p> #{user.messages.count} </p>")
# end
# end
#
# If you want to render a template instead of a partial, e.g. ('messages/index' or 'messages/show'), you can use the `template:` option.
# Again, only to any broadcast method that accepts the `**rendering` argument. Example:
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message
#
# private
# def update_message
# broadcast_replace_to(user, :message, target: "message", template: "messages/show", locals: { message: self })
# end
# end
#
# If you want to render a renderable object you can use the `renderable:` option.
#
# class Message < ApplicationRecord
# belongs_to :user
#
# after_create_commit :update_message
#
# private
# def update_message
# broadcast_replace_to(user, :message, target: "message", renderable: MessageComponent.new)
# end
# end
#
# There are four basic actions you can broadcast: <tt>remove</tt>, <tt>replace</tt>, <tt>append</tt>, and
# <tt>prepend</tt>. As a rule, you should use the <tt>_later</tt> versions of everything except for remove when broadcasting
# within a real-time path, like a controller or model, since all those updates require a rendering step, which can slow down
# execution. You don't need to do this for remove, since only the dom id for the model is used.
#
# In addition to the four basic actions, you can also use <tt>broadcast_render</tt>,
# <tt>broadcast_render_to</tt> <tt>broadcast_render_later</tt>, and <tt>broadcast_render_later_to</tt>
# to render a turbo stream template with multiple actions.
module Turbo::Broadcastable
extend ActiveSupport::Concern
module ClassMethods
# Configures the model to broadcast creates, updates, and destroys to a stream name derived at runtime by the
# <tt>stream</tt> symbol invocation. By default, the creates are appended to a dom id target name derived from
# the model's plural name. The insertion can also be made to be a prepend by overwriting <tt>inserts_by</tt> and
# the target dom id overwritten by passing <tt>target</tt>. Examples:
#
# class Message < ApplicationRecord
# belongs_to :board
# broadcasts_to :board
# end
#
# class Message < ApplicationRecord
# belongs_to :board
# broadcasts_to ->(message) { [ message.board, :messages ] }, inserts_by: :prepend, target: "board_messages"
# end
#
# class Message < ApplicationRecord
# belongs_to :board
# broadcasts_to ->(message) { [ message.board, :messages ] }, partial: "messages/custom_message"
# end
def broadcasts_to(stream, inserts_by: :append, target: broadcast_target_default, **rendering)
after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, target: target.try(:call, self) || target, **rendering) }
after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
end
# Same as <tt>#broadcasts_to</tt>, but the designated stream for updates and destroys is automatically set to
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
def broadcasts(stream = model_name.plural, inserts_by: :append, target: broadcast_target_default, **rendering)
after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, target: target.try(:call, self) || target, **rendering) }
after_update_commit -> { broadcast_replace_later(**rendering) }
after_destroy_commit -> { broadcast_remove }
end
# All default targets will use the return of this method. Overwrite if you want something else than <tt>model_name.plural</tt>.
def broadcast_target_default
model_name.plural
end
end
# Remove this broadcastable model from the dom for subscribers of the stream name identified by the passed streamables.
# Example:
#
# # Sends <turbo-stream action="remove" target="clearance_5"></turbo-stream> to the stream named "identity:2:clearances"
# clearance.broadcast_remove_to examiner.identity, :clearances
def broadcast_remove_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_remove_to(*streamables, **extract_options_and_add_target(rendering, target: self))
end
# Same as <tt>#broadcast_remove_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_remove(**rendering)
broadcast_remove_to self, **rendering
end
# Replace this broadcastable model in the dom for subscribers of the stream name identified by the passed
# <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_replace_to examiner.identity, :clearances
#
# # Sends <turbo-stream action="replace" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_replace_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_replace_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_replace_to(*streamables, **extract_options_and_add_target(rendering, target: self))
end
# Same as <tt>#broadcast_replace_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_replace(**rendering)
broadcast_replace_to self, **rendering
end
# Update this broadcastable model in the dom for subscribers of the stream name identified by the passed
# <tt>streamables</tt>. The rendering parameters can be set by appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_update_to examiner.identity, :clearances
#
# # Sends <turbo-stream action="update" target="clearance_5"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_update_to examiner.identity, :clearances, partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_update_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_update_to(*streamables, **extract_options_and_add_target(rendering, target: self))
end
# Same as <tt>#broadcast_update_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_update(**rendering)
broadcast_update_to self, **rendering
end
# Insert a rendering of this broadcastable model before the target identified by it's dom id passed as <tt>target</tt>
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
# appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5"
#
# # Sends <turbo-stream action="before" target="clearance_5"><template><div id="clearance_4">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_before_to examiner.identity, :clearances, target: "clearance_5",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_before_to(*streamables, target: nil, targets: nil, **rendering)
raise ArgumentError, "at least one of target or targets is required" unless target || targets
Turbo::StreamsChannel.broadcast_before_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
end
# Insert a rendering of this broadcastable model after the target identified by it's dom id passed as <tt>target</tt>
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
# appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5"
#
# # Sends <turbo-stream action="after" target="clearance_5"><template><div id="clearance_6">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_after_to examiner.identity, :clearances, target: "clearance_5",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_after_to(*streamables, target: nil, targets: nil, **rendering)
raise ArgumentError, "at least one of target or targets is required" unless target || targets
Turbo::StreamsChannel.broadcast_after_to(*streamables, **extract_options_and_add_target(rendering.merge(target: target, targets: targets)))
end
# Append a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
# appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances"
#
# # Sends <turbo-stream action="append" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_append_to examiner.identity, :clearances, target: "clearances",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_append_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_append_to(*streamables, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_append_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_append(**rendering)
broadcast_append_to self, **rendering
end
# Prepend a rendering of this broadcastable model to the target identified by it's dom id passed as <tt>target</tt>
# for subscribers of the stream name identified by the passed <tt>streamables</tt>. The rendering parameters can be set by
# appending named arguments to the call. Examples:
#
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances"
#
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">Other partial</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_prepend_to examiner.identity, :clearances, target: "clearances",
# partial: "clearances/other_partial", locals: { a: 1 }
def broadcast_prepend_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_prepend_to(*streamables, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_prepend_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_prepend(**rendering)
broadcast_prepend_to self, **rendering
end
# Broadcast a named <tt>action</tt>, allowing for dynamic dispatch, instead of using the concrete action methods. Examples:
#
# # Sends <turbo-stream action="prepend" target="clearances"><template><div id="clearance_5">My Clearance</div></template></turbo-stream>
# # to the stream named "identity:2:clearances"
# clearance.broadcast_action_to examiner.identity, :clearances, action: :prepend, target: "clearances"
def broadcast_action_to(*streamables, action:, **rendering)
Turbo::StreamsChannel.broadcast_action_to(*streamables, action: action, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_action_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_action(action, **rendering)
broadcast_action_to self, action: action, **rendering
end
# Same as <tt>broadcast_replace_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_replace_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_replace_later_to(*streamables, **extract_options_and_add_target(rendering, target: self))
end
# Same as <tt>#broadcast_replace_later_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_replace_later(**rendering)
broadcast_replace_later_to self, **rendering
end
# Same as <tt>broadcast_update_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_update_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_update_later_to(*streamables, **extract_options_and_add_target(rendering, target: self))
end
# Same as <tt>#broadcast_update_later_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_update_later(**rendering)
broadcast_update_later_to self, **rendering
end
# Same as <tt>broadcast_append_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_append_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_append_later_to(*streamables, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_append_later_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_append_later(**rendering)
broadcast_append_later_to self, **rendering
end
# Same as <tt>broadcast_prepend_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_prepend_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_prepend_later_to(*streamables, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_prepend_later_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_prepend_later(**rendering)
broadcast_prepend_later_to self, **rendering
end
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_action_later_to(*streamables, action:, **rendering)
Turbo::StreamsChannel.broadcast_action_later_to(*streamables, action: action, **extract_options_and_add_target(rendering))
end
# Same as <tt>#broadcast_action_later_to</tt>, but the designated stream is automatically set to the current model.
def broadcast_action_later(action:, **rendering)
broadcast_action_later_to self, action: action, **rendering
end
# Render a turbo stream template with this broadcastable model passed as the local variable. Example:
#
# # Template: entries/_entry.turbo_stream.erb
# <%= turbo_stream.remove entry %>
#
# <%= turbo_stream.append "entries", entry if entry.active? %>
#
# Sends:
#
# <turbo-stream action="remove" target="entry_5"></turbo-stream>
# <turbo-stream action="append" target="entries"><template><div id="entry_5">My Entry</div></template></turbo-stream>
#
# ...to the stream named "entry:5".
#
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
# be using `broadcast_render_later`, unless you specifically know why synchronous rendering is needed.
def broadcast_render(**rendering)
broadcast_render_to self, **rendering
end
# Same as <tt>broadcast_render</tt> but run with the added option of naming the stream using the passed
# <tt>streamables</tt>.
#
# Note that rendering inline via this method will cause template rendering to happen synchronously. That is usually not
# desireable for model callbacks, certainly not if those callbacks are inside of a transaction. Most of the time you should
# be using `broadcast_render_later_to`, unless you specifically know why synchronous rendering is needed.
def broadcast_render_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_render_to(*streamables, **extract_options_and_add_target(rendering))
end
# Same as <tt>broadcast_action_to</tt> but run asynchronously via a <tt>Turbo::Streams::BroadcastJob</tt>.
def broadcast_render_later(**rendering)
broadcast_render_later_to self, **rendering
end
# Same as <tt>broadcast_render_later</tt> but run with the added option of naming the stream using the passed
# <tt>streamables</tt>.
def broadcast_render_later_to(*streamables, **rendering)
Turbo::StreamsChannel.broadcast_render_later_to(*streamables, **extract_options_and_add_target(rendering))
end
private
def broadcast_target_default
self.class.broadcast_target_default
end
def extract_options_and_add_target(rendering, target: broadcast_target_default)
broadcast_rendering_with_defaults(rendering).tap do |options|
options[:target] = target if !options.key?(:target) && !options.key?(:targets)
end
end
def broadcast_rendering_with_defaults(options)
options.tap do |o|
# Add the current instance into the locals with the element name (which is the un-namespaced name)
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
o[:locals] = (o[:locals] || {}).reverse_merge!(model_name.element.to_sym => self)
if o[:html] || o[:partial]
return o
elsif o[:template] || o[:renderable]
o[:layout] = false
elsif o[:render] == false
return o
else
# if none of these options are passed in, it will set a partial from #to_partial_path
o[:partial] ||= to_partial_path
end
end
end
end