turbo_ready 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  <p align="center">
2
- <img height="200" src="https://ik.imagekit.io/hopsoft/turbo-ready-logo_jYFJI1jgT.png?ik-sdk-version=javascript-1.4.3&updatedAt=1661471047153" />
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://ik.imagekit.io/hopsoft/turbo-ready-logo-light_df4jcvbDL.webp?ik-sdk-version=javascript-1.4.3&updatedAt=1661615678275">
4
+ <img height="200" src="https://ik.imagekit.io/hopsoft/turbo-ready-logo-dark_VN4hA2ctc.webp?ik-sdk-version=javascript-1.4.3&updatedAt=1661615678278" />
5
+ </picture>
3
6
  <h3 align="center">
4
7
  Turbo Stream's Swiss Army Knife
5
8
  </h3>
@@ -7,47 +10,76 @@
7
10
  Welcome to TurboReady 👋
8
11
  </h1>
9
12
  <p align="center">
10
- <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/" target="_blank">
11
- <img alt="Lines of Code" src="https://img.shields.io/badge/lines_of_code-278-brightgreen.svg?style=flat" />
13
+ <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
14
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-143-47d299.svg" />
12
15
  </a>
13
- <a href="https://github.com/testdouble/standard" target="_blank">
14
- <img alt="Ruby Code Style" src="https://img.shields.io/badge/Ruby_Code_Style-standard-brightgreen.svg" />
16
+ <a href="https://codeclimate.com/github/hopsoft/turbo_ready/maintainability">
17
+ <img src="https://api.codeclimate.com/v1/badges/a69b6f73abc3ccd49261/maintainability" />
15
18
  </a>
16
- <a href="https://github.com/sheerun/prettier-standard" target="_blank">
17
- <img alt="JavaScript Code Style" src="https://img.shields.io/badge/JavaScript_Code_Style-prettier_standard-ff69b4.svg" />
19
+ <a href="https://rubygems.org/gems/turbo_ready">
20
+ <img alt="GEM" src="https://img.shields.io/gem/v/turbo_ready?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
18
21
  </a>
19
- <a href="https://bundlephobia.com/package/turbo_ready" target="_blank">
20
- <img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/turbo_ready?label=minified%20size">
22
+ <a href="https://rubygems.org/gems/turbo_ready">
23
+ <img alt="Gem" src="https://img.shields.io/gem/dt/turbo_ready?color=168AFE&logo=ruby&logoColor=FE1616">
24
+ </a>
25
+ <a href="https://github.com/testdouble/standard">
26
+ <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
27
+ </a>
28
+ <a href="https://www.npmjs.com/package/turbo_ready">
29
+ <img alt="NPM" src="https://img.shields.io/npm/v/turbo_ready?color=168AFE&logo=npm">
30
+ </a>
31
+ <a href="https://www.npmjs.com/package/turbo_ready">
32
+ <img alt="npm" src="https://img.shields.io/npm/dm/turbo_ready?color=168AFE&logo=npm">
33
+ </a>
34
+ <a href="https://bundlephobia.com/package/turbo_ready@">
35
+ <img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/turbo_ready?label=bundle%20size&logo=npm&color=47d299">
36
+ </a>
37
+ <a href="https://github.com/sheerun/prettier-standard">
38
+ <img alt="JavaScript Style" src="https://img.shields.io/badge/style-prettier--standard-168AFE?logo=javascript&logoColor=f4e137" />
39
+ </a>
40
+ <a href="https://github.com/hopsoft/turbo_ready/actions/workflows/tests.yml">
41
+ <img alt="Tests" src="https://github.com/hopsoft/turbo_ready/actions/workflows/tests.yml/badge.svg" />
42
+ </a>
43
+ <a href="https://twitter.com/hopsoft">
44
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/hopsoft?logo=twitter&style=social">
21
45
  </a>
22
46
  </p>
23
47
  </p>
24
48
 
25
- TurboReady extends [Turbo Streams](https://turbo.hotwired.dev/reference/streams) to give you full control of the
26
- browser's [Document Object Model (DOM).](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)
27
-
28
- **Thats right!**
29
- You can `invoke` any DOM method on any DOM object *(including 3rd party libs)* using Turbo Streams.
49
+ **TurboReady extends [Turbo Streams](https://turbo.hotwired.dev/reference/streams) to give you full control of the
50
+ browser's [Document Object Model (DOM).](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)**
30
51
 
31
52
  ```ruby
32
- turbo_stream.invoke "console.log", "Hello World!"
53
+ turbo_stream.invoke "console.log", args: ["Hello World!"]
33
54
  ```
34
55
 
56
+ **Thats right!**
57
+ You can `invoke` any DOM method on the client with Turbo Streams.
58
+
35
59
  <!-- Tocer[start]: Auto-generated, don't remove. -->
36
60
 
37
61
  ## Table of Contents
38
62
 
39
63
  - [Why TurboReady?](#why-turboready)
40
- - [Discord Community](#discord-community)
41
64
  - [Sponsors](#sponsors)
42
65
  - [Dependencies](#dependencies)
43
66
  - [Installation](#installation)
44
67
  - [Setup](#setup)
45
68
  - [Usage](#usage)
46
- - [Endless Possibilities](#endless-possibilities)
47
- - [Advanced Usage](#advanced-usage)
69
+ - [Method Chaining](#method-chaining)
70
+ - [Dispatching Events](#dispatching-events)
71
+ - [Syntax Styles](#syntax-styles)
48
72
  - [Extending Behavior](#extending-behavior)
49
- - [Public API](#public-api)
73
+ - [Implementation Details](#implementation-details)
74
+ - [Broadcasting](#broadcasting)
75
+ - [Background Job Queues](#background-job-queues)
76
+ - [FAQ](#faq)
50
77
  - [A Word of Caution](#a-word-of-caution)
78
+ - [Community](#community)
79
+ - [Discord](#discord)
80
+ - [Discussions](#discussions)
81
+ - [Twitter](#twitter)
82
+ - [TODOs](#todos)
51
83
  - [Releasing](#releasing)
52
84
  - [License](#license)
53
85
 
@@ -55,21 +87,17 @@ turbo_stream.invoke "console.log", "Hello World!"
55
87
 
56
88
  ## Why TurboReady?
57
89
 
58
- Turbo Streams [intentionally restricts](https://turbo.hotwired.dev/handbook/streams#but-what-about-running-javascript)
90
+ Turbo Streams [intentionally restricts](https://turbo.hotwired.dev/handbook/streams#but-what-about-running-javascript%3F)
59
91
  official actions to CRUD related activity.
60
- The [official actions](https://turbo.hotwired.dev/reference/streams#the-seven-actions) work well for a
61
- considerable number of use cases and you should push Streams as far as possible before reaching for TurboReady.
92
+ These [official actions](https://turbo.hotwired.dev/reference/streams#the-seven-actions) work well for a considerable number of use cases.
93
+ *Try pushing Turbo Streams as far as possible before reaching for TurboReady.*
62
94
 
63
- If you discover that CRUD isn't enough, TurboReady covers pretty much everything else.
95
+ If you find that CRUD isn't enough, TurboReady is there to handle pretty much everything else.
64
96
 
65
- ## Community
66
-
67
- Please join nearly 2000 of us on [Discord](https://discord.gg/stimulus-reflex) for support getting started,
68
- as well as active discussions around Rails, Hotwire, Stimulus, Turbo (Drive, Frames, Streams), TurboReady, CableReady, StimulusReflex, ViewComponent, Phlex, and more.
69
-
70
- ![](https://img.shields.io/discord/629472241427415060)
97
+ > ⚠️ TurboReady is intended for Rails apps that use Hotwire but not [CableReady](https://github.com/stimulusreflex/cable_ready).
98
+ This is because CableReady already provides a rich set of powerful [DOM operations](https://cableready.stimulusreflex.com/reference/operations).
71
99
 
72
- Stop by #newcomers and introduce yourselves!
100
+ > 📘 **NOTE:** Efforts are underway to bring [CableReady's DOM operations to Turbo Streams](https://github.com/marcoroth/turbo_power).
73
101
 
74
102
  ## Sponsors
75
103
 
@@ -90,73 +118,84 @@ Stop by #newcomers and introduce yourselves!
90
118
 
91
119
  ## Installation
92
120
 
121
+ Be sure to install the same version for each libary.
122
+
93
123
  ```sh
94
124
  bundle add "turbo_ready --version VERSION"
95
125
  yarn add "turbo_ready@VERSION --exact"
96
126
  ```
97
127
 
98
- **IMPORTANT:** Be sure to use the same version for each libary.
99
-
100
128
  ## Setup
101
129
 
102
- 1. Import and intialize TurboReady in your JavaScript application.
130
+ Import and intialize TurboReady in your application.
103
131
 
104
- ```js
105
- // app/javascript/application.js
106
- import '@hotwired/turbo-rails'
107
- import TurboReady from 'turbo_ready'
132
+ ```diff
133
+ # Gemfile
134
+ +gem "turbo_ready", "~> 0.0.6"
135
+ ```
108
136
 
109
- TurboReady.initialize(Turbo.StreamActions) // Adds TurboReady stream actions to Turbo
110
- ```
137
+ ```diff
138
+ # package.json
139
+ "dependencies": {
140
+ + "@hotwired/turbo-rails": ">=7.2.0-beta.2",
141
+ + "turbo_ready": "^0.0.6"
142
+ ```
143
+
144
+ ```diff
145
+ # app/javascript/application.js
146
+ import '@hotwired/turbo-rails'
147
+ +import TurboReady from 'turbo_ready'
148
+
149
+ +TurboReady.initialize(Turbo.StreamActions) // Adds TurboReady stream actions to Turbo
150
+ ```
111
151
 
112
152
  ## Usage
113
153
 
114
- Manipulate the DOM from anywhere you use [official Turbo Streams](https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks).
115
- Namely, [**M**odels](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb),
116
- [**V**iews](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb),
117
- and [**C**ontrollers](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
154
+ Manipulate the DOM from anywhere you use official [Turbo Streams](https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks).
155
+ The possibilities are endless.
156
+ [Learn more about the DOM at MDN.](https://developer.mozilla.org/en-US/docs/Web/API.)
157
+
158
+ ```ruby
159
+ turbo_stream.invoke "console.log", args: ["Hello World!"]
160
+ ```
161
+
162
+ ### Method Chaining
118
163
 
119
- You can **chain invocations.** ❤️
164
+ You can use [dot notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#dot_notation)
165
+ or [selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) and even combine them!
120
166
 
121
167
  ```ruby
122
168
  turbo_stream
123
- .invoke("document.body.insertAdjacentHTML", "afterbegin", "<h1>Hello World!</h1>") # dot notation
124
- .invoke("setAttribute", "data-turbo-ready", true, selector: ".button") # selector
125
- .invoke("classList.add", "turbo-ready", selector: "a") # dot notation + selector
126
- .flush # flush must be called when chaining invocations
169
+ .invoke("document.body.insertAdjacentHTML", args: ["afterbegin", "<h1>Hello World!</h1>"]) # dot notation
170
+ .invoke("setAttribute", args: ["data-turbo-ready", true], selector: ".button") # selector
171
+ .invoke("classList.add", args: ["turbo-ready"], selector: "a") # dot notation + selector
172
+ .flush # call flush when chaining invocations
127
173
  ```
128
174
 
129
- You can use [dot notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors#dot_notation)
130
- or [selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)... and can even combine them!** 🤯
175
+ ### Dispatching Events
131
176
 
132
- Can I dispatch events? **You bet!** ⚡️
177
+ It's possible to fire events on `window`, `document`, and element(s).
133
178
 
134
179
  ```ruby
135
180
  turbo_stream
136
- .invoke("dispatchEvent", "turbo-ready:demo") // fires on window
137
- .invoke("dispatchEvent", "turbo-ready:demo", selector: "#my-element") // fires on matching element(s)
138
- .invoke("dispatchEvent", {bubbles: true, detail: {...}}) // set event options
181
+ .invoke("dispatchEvent", args: ["turbo-ready:demo"]) # fires on window
182
+ .invoke("document.dispatchEvent", args: ["turbo-ready:demo"]) # fires on document
183
+ .invoke("dispatchEvent", args: ["turbo-ready:demo"], selector: "#my-element") # fires on matching element(s)
184
+ .invoke("dispatchEvent", args: ["turbo-ready:demo", {bubbles: true, detail: {...}}]) # set event options
139
185
  .flush
140
186
  ```
141
187
 
142
- ## Endless Possibilities
143
-
144
- **What else can I do?**
145
- MDN has your back... [learn about the DOM and web APIs here.](https://developer.mozilla.org/en-US/docs/Web/API.)
188
+ ### Syntax Styles
146
189
 
147
- ## Advanced Usage
148
-
149
- You can use symbols and [snake case](https://en.wikipedia.org/wiki/Snake_case) when invoking DOM functionality.
150
- It'll implicitly convert to [camel case](https://en.wikipedia.org/wiki/Camel_case). 💎
190
+ You can use [`snake_case`](https://en.wikipedia.org/wiki/Snake_case) when invoking DOM functionality.
191
+ It will implicitly convert to [`camelCase`](https://en.wikipedia.org/wiki/Camel_case).
151
192
 
152
193
  ```ruby
153
- turbo_stream
154
- .invoke(:animate, [{opacity: 0}, {opacity: 1}], 2000)
155
- .invoke(:dispatch_event, {detail: {converts_to_camel_case: true}})
156
- .flush
194
+ turbo_stream.invoke :dispatch_event,
195
+ args: ["turbo-ready:demo", {detail: {converts_to_camel_case: true}}]
157
196
  ```
158
197
 
159
- Need to opt out of camelize? No problem... just disable it.
198
+ Need to opt-out? No problem... just disable it.
160
199
 
161
200
  ```ruby
162
201
  turbo_stream.invoke :contrived_demo, camelize: false
@@ -164,10 +203,10 @@ turbo_stream.invoke :contrived_demo, camelize: false
164
203
 
165
204
  ### Extending Behavior
166
205
 
167
- Want to extend things with custom functionality? **Let's do it.** 🔌
206
+ If you add new capabilities to the browser, you can control them from the server.
168
207
 
169
208
  ```js
170
- // JavaScript
209
+ // JavaScript on the client
171
210
  import morphdom from 'morphdom'
172
211
 
173
212
  window.MyNamespace = {
@@ -178,51 +217,180 @@ window.MyNamespace = {
178
217
  ```
179
218
 
180
219
  ```ruby
181
- # Ruby
182
- turbo_stream
183
- .invoke "MyNamespace.morph", "#demo", "<div id='demo'><p>You've changed...</p></div>", {childrenOnly: true}
220
+ # Ruby on the server
221
+ turbo_stream.invoke "MyNamespace.morph",
222
+ args: [
223
+ "#demo",
224
+ "<div id='demo'><p>You've changed...</p></div>",
225
+ {children_only: true}
226
+ ]
184
227
  ```
185
228
 
186
- ## Public API
229
+ ### Implementation Details
187
230
 
188
- There's only one method to consider, `invoke` defined in the
189
- [tag builder](https://github.com/hopsoft/turbo_ready/blob/main/lib/turbo_ready/tag_builder.rb).
231
+ There's basically one method to learn... `invoke`
190
232
 
191
233
  ```ruby
192
234
  # Ruby
193
235
  turbo_stream
194
- .invoke(method, *args, selector: nil, camelize: true, id: nil)
195
- # | | | | |
196
- # | | | | |- Identifies this invocation (optional)
197
- # | | | |
198
- # | | | |- Should we camelize the JavaScript stuff? (optional)
199
- # | | | (allows us to write snake_case Ruby)
200
- # | | |
201
- # | | |- An CSS selector for the element(s) to target (optional)
202
- # | |
203
- # | |- The arguments to pass to the JavaScript method being invoked (optional)
236
+ .invoke(method, args: [], selector: nil, camelize: true, id: nil)
237
+ # | | | | |
238
+ # | | | | |- Identifies this invocation (optional)
239
+ # | | | |
240
+ # | | | |- Should we camelize the JavaScript stuff? (optional)
241
+ # | | | (allows us to write snake_case in Ruby)
242
+ # | | |
243
+ # | | |- A CSS selector for the element(s) to target (optional)
244
+ # | |
245
+ # | |- The arguments to pass to the JavaScript method (optional)
204
246
  # |
205
247
  # |- The JavaScript method to invoke (can use dot notation)
206
248
  ```
207
249
 
208
- **NOTE:** The JavaScript method will be invoked on all matching elements when a `selector` is passed.
250
+ > 📘 **NOTE:** The method will be invoked on all matching elements if a `selector` is present.
251
+
252
+ The following Ruby code,
253
+
254
+ ```ruby
255
+ turbo_stream.invoke "console.log", args: ["Hello World!"], id: "1"
256
+ ```
257
+
258
+ emits this HTML markup.
259
+
260
+ ```html
261
+ <turbo-stream action="invoke" target="DOM">
262
+ <template>{"id":"1","receiver":"console","method":"log","args":["Hello World!"]}</template>
263
+ </turbo-stream>
264
+ ```
265
+
266
+ When this element enters the DOM,
267
+ Turbo Streams automatically executes `invoke` on the client with the template's JSON payload and then removes the element from the DOM.
268
+
269
+ ### Broadcasting
270
+
271
+ You can also broadcast DOM invocations to subscribed users.
272
+
273
+ 1. First, setup the stream subscription.
274
+
275
+ ```erb
276
+ <!-- app/views/posts/show.html.erb -->
277
+ <%= turbo_stream_from @post %>
278
+ <!-- |
279
+ |- *streamables - model(s), string(s), etc...
280
+ -->
281
+ ```
282
+
283
+ 2. Then, broadcast to the subscription.
284
+
285
+ ```ruby
286
+ # app/models/post.rb
287
+ class Post < ApplicationRecord
288
+ after_save do
289
+ # emit a message in the browser conosle for anyone subscribed to this post
290
+ broadcast_invoke "console.log", args: ["Post was saved! #{to_gid.to_s}"]
291
+
292
+ # broadcast with a background job
293
+ broadcast_invoke_later "console.log", args: ["Post was saved! #{to_gid.to_s}"]
294
+ end
295
+ end
296
+ ```
297
+
298
+ ```ruby
299
+ # app/controllers/posts_controller.rb
300
+ class PostsController < ApplicationController
301
+ def create
302
+ @post = Post.find params[:id]
303
+
304
+ if @post.update post_params
305
+ # emit a message in the browser conosle for anyone subscribed to this post
306
+ @post.broadcast_invoke "console.log", args: ["Post was saved! #{to_gid.to_s}"]
307
+
308
+ # broadcast with a background job
309
+ @post.broadcast_invoke_later "console.log", args: ["Post was saved! #{to_gid.to_s}"]
310
+
311
+ # you can also broadcast directly from the channel
312
+ Turbo::StreamsChannel.broadcast_invoke_to @post, "console.log",
313
+ args: ["Post was saved! #{@post.to_gid.to_s}"]
314
+
315
+ # broadcast with a background job
316
+ Turbo::StreamsChannel.broadcast_invoke_later_to @post, "console.log",
317
+ args: ["Post was saved! #{@post.to_gid.to_s}"]
318
+ end
319
+ end
320
+ end
321
+ ```
322
+
323
+ > 📘 **NOTE:** [Method Chaining](#method-chaining) is not currently supported when broadcasting.
324
+
325
+ #### Background Job Queues
326
+
327
+ You may want to change the queue name for Turbo Stream background jobs in order to isolate, prioritize, and scale the workers independently.
328
+
329
+ ```ruby
330
+ # config/initializers/turbo_streams.rb
331
+ Turbo::Streams::BroadcastJob.queue_name = :turbo_streams
332
+ TurboReady::BroadcastInvokeJob.queue_name = :turbo_streams
333
+ ```
334
+
335
+ ## FAQ
336
+
337
+ - Isn't this just RJS?
338
+
339
+ > No. But, perhaps it could be considered RJS's "modern" spirtual successor. 🤷‍♂️
340
+ > Though it embraces JavaScript instead of trying to avoid it.
341
+
342
+ - Does it use `eval`?
343
+
344
+ > **No.** TurboReady can only invoke existing functions on the client.
345
+ > It's not a carte blanche invitation to emit free-form JavaScript to be evaluated on the client.
209
346
 
210
347
  ## A Word of Caution
211
348
 
212
- Manually orchestrating DOM activity gets tedious fast.
213
- **⚠️ Don't abuse this superpower!**
349
+ **Don't abuse this superpower!**
214
350
 
215
351
  > With great power comes great responsibility. *-Uncle Ben*
216
352
 
353
+ Manually orchestrating DOM activity is tedious.
354
+ *Don't overdo it... or you may find that you've created spaghetti reminiscent of the jQuery days.*
355
+
217
356
  This library is an extremely sharp tool. 🔪
218
- Consider it a low-level building block that can be used to craft additional libraries with
219
- great [DX](https://en.wikipedia.org/wiki/User_experience#Developer_experience)
357
+ Consider it a low-level building block that can be used to craft additional libraries
220
358
  like [CableReady](https://github.com/stimulusreflex/cable_ready)
221
359
  and [StimulusReflex](https://github.com/stimulusreflex/stimulus_reflex).
222
360
 
223
- Restrict your direct application usage to DOM manipulation that falls outside the purview of
224
- [Turbo's official actions](https://turbo.hotwired.dev/reference/streams#the-seven-actions)...
225
- *don't overdo it and find yourself maintaining spaghetti code reminiscent of the jQuery days.*
361
+ ## Community
362
+
363
+ ### Discord
364
+
365
+ Please join nearly 2000 of us on [Discord](https://discord.gg/stimulus-reflex) for support getting started,
366
+ as well as active discussions around Rails, Hotwire, Stimulus, Turbo (Drive, Frames, Streams), TurboReady, CableReady, StimulusReflex, ViewComponent, Phlex, and more.
367
+
368
+ <a href="https://discord.gg/stimulus-reflex" target="_blank">
369
+ <img alt="Discord" src="https://img.shields.io/discord/629472241427415060?color=168AFE&logo=discord&logoColor=FFF">
370
+ </a>
371
+
372
+ Be sure to introduce yourselves in the #newcomers channel!
373
+
374
+ ### Discussions
375
+
376
+ Feel free to add to the conversation here on [GitHub Discussions](https://github.com/hopsoft/turbo_ready/discussions).
377
+
378
+ <a href="https://github.com/hopsoft/turbo_ready/discussions" target="_blank">
379
+ <img alt="GitHub Discussions" src="https://img.shields.io/github/discussions/hopsoft/turbo_ready?color=168AFE&logo=github">
380
+ </a>
381
+
382
+ ### Twitter
383
+
384
+ Connect with the core team on Twitter.
385
+
386
+ <a href="https://twitter.com/hopsoft" target="_blank">
387
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/hopsoft?logo=twitter&style=social">
388
+ </a>
389
+
390
+ ## TODOs
391
+
392
+ - [ ] Add system tests [(review turbo-rails for guidance)](https://github.com/hotwired/turbo-rails/blob/main/test/system/broadcasts_test.rb)
393
+ - [ ] Look into adding method chaining for broadcasts
226
394
 
227
395
  ## Releasing
228
396