live_cable 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +1275 -0
  4. data/app/assets/javascript/controllers/live_controller.js +79 -44
  5. data/app/assets/javascript/dom.js +161 -0
  6. data/app/assets/javascript/live_cable.js +50 -0
  7. data/app/assets/javascript/live_cable_blessing.js +1 -1
  8. data/app/assets/javascript/observer.js +74 -0
  9. data/app/assets/javascript/subscriptions.js +396 -37
  10. data/app/channels/live_channel.rb +17 -14
  11. data/app/helpers/live_cable_helper.rb +28 -39
  12. data/config/importmap.rb +9 -3
  13. data/lib/generators/live_cable/component/component_generator.rb +58 -0
  14. data/lib/generators/live_cable/component/templates/component.rb.tt +29 -0
  15. data/lib/generators/live_cable/component/templates/view.html.live.erb.tt +2 -0
  16. data/lib/live_cable/component/broadcasting.rb +30 -0
  17. data/lib/live_cable/component/identification.rb +31 -0
  18. data/lib/live_cable/component/lifecycle.rb +67 -0
  19. data/lib/live_cable/component/method_dependency_tracking.rb +22 -0
  20. data/lib/live_cable/component/reactive_variables.rb +125 -0
  21. data/lib/live_cable/component/rendering.rb +177 -0
  22. data/lib/live_cable/component/streaming.rb +43 -0
  23. data/lib/live_cable/component.rb +21 -236
  24. data/lib/live_cable/configuration.rb +29 -0
  25. data/lib/live_cable/connection/broadcasting.rb +33 -0
  26. data/lib/live_cable/connection/channel_management.rb +13 -0
  27. data/lib/live_cable/connection/component_management.rb +38 -0
  28. data/lib/live_cable/connection/error_handling.rb +40 -0
  29. data/lib/live_cable/connection/messaging.rb +84 -0
  30. data/lib/live_cable/connection/state_management.rb +56 -0
  31. data/lib/live_cable/connection.rb +11 -180
  32. data/lib/live_cable/container.rb +25 -0
  33. data/lib/live_cable/delegation/array.rb +1 -0
  34. data/lib/live_cable/delegator.rb +0 -7
  35. data/lib/live_cable/engine.rb +15 -3
  36. data/lib/live_cable/observer.rb +5 -1
  37. data/lib/live_cable/observer_tracking.rb +20 -0
  38. data/lib/live_cable/render_context.rb +55 -8
  39. data/lib/live_cable/rendering/compiler.rb +80 -0
  40. data/lib/live_cable/rendering/dependency_visitor.rb +100 -0
  41. data/lib/live_cable/rendering/handler.rb +19 -0
  42. data/lib/live_cable/rendering/method_analyzer.rb +94 -0
  43. data/lib/live_cable/rendering/method_collector.rb +51 -0
  44. data/lib/live_cable/rendering/method_dependency_visitor.rb +51 -0
  45. data/lib/live_cable/rendering/partial.rb +93 -0
  46. data/lib/live_cable/rendering/partial_renderer.rb +145 -0
  47. data/lib/live_cable/rendering/render_result.rb +38 -0
  48. data/lib/live_cable/rendering/renderer.rb +150 -0
  49. data/lib/live_cable/version.rb +5 -0
  50. data/lib/live_cable.rb +15 -15
  51. metadata +124 -4
data/README.md ADDED
@@ -0,0 +1,1275 @@
1
+ # LiveCable
2
+
3
+ LiveCable is a Phoenix LiveView-style live component system for Ruby on Rails that tracks state server-side and allows
4
+ you to call actions from the frontend using Stimulus with a React style state management API.
5
+
6
+ **Full documentation: [livecable.io](https://livecable.io)**
7
+
8
+ ## Features
9
+
10
+ - **Server-side state management**: Component state is maintained on the server using ActionCable
11
+ - **Reactive variables**: Automatic UI updates when state changes with smart change tracking
12
+ - **Automatic change detection**: Arrays, Hashes, and ActiveRecord models automatically trigger updates when mutated
13
+ - **Action dispatch**: Call server-side methods from the frontend
14
+ - **Lifecycle hooks**: Hook into component lifecycle events
15
+ - **Stimulus integration**: Seamless integration with Stimulus controllers and blessings API
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'live_cable'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ To use LiveCable, you need to set up your `ApplicationCable::Connection` to initialize a `LiveCable::Connection`.
34
+
35
+ Add this to your `app/channels/application_cable/connection.rb`:
36
+
37
+ ```ruby
38
+ module ApplicationCable
39
+ class Connection < ActionCable::Connection::Base
40
+ identified_by :live_connection
41
+
42
+ def connect
43
+ self.live_connection = LiveCable::Connection.new(self.request)
44
+ end
45
+ end
46
+ end
47
+ ```
48
+
49
+ ## JavaScript Setup
50
+
51
+ LiveCable works with both **importmap-rails** (default for Rails 7+) and **JavaScript bundlers** (esbuild, Vite, webpack, Rollup via jsbundling-rails or vite_ruby).
52
+
53
+ ### Using importmap-rails (default)
54
+
55
+ The gem automatically pins all necessary modules when you install it. No additional setup is needed.
56
+
57
+ ### Using a JavaScript bundler (yarn/npm)
58
+
59
+ Install the npm package:
60
+
61
+ ```bash
62
+ yarn add @isometriks/live_cable
63
+ # or
64
+ npm install @isometriks/live_cable
65
+ ```
66
+
67
+ This will also install the required peer dependencies (`@rails/actioncable`, `@hotwired/stimulus`, and `morphdom`).
68
+
69
+ ### Register the LiveController
70
+
71
+ Register the `LiveController` in your Stimulus application (`app/javascript/controllers/application.js`). The imports are the same regardless of whether you use importmap or a bundler:
72
+
73
+ ```javascript
74
+ import { Application } from "@hotwired/stimulus"
75
+ import LiveController from "@isometriks/live_cable/controller"
76
+ import "@isometriks/live_cable" // Automatically starts DOM observer
77
+
78
+ const application = Application.start()
79
+ application.register("live", LiveController)
80
+ ```
81
+
82
+ The `live_cable` import automatically starts a DOM observer that watches for LiveCable components and transforms custom attributes (`live-action`, `live-form`, `live-reactive`, etc.) into Stimulus attributes.
83
+
84
+ ### 2. Enable LiveCable Blessing (Optional)
85
+
86
+ If you want to call LiveCable actions from your own Stimulus controllers, add the LiveCable blessing:
87
+
88
+ ```javascript
89
+ import { Application, Controller } from "@hotwired/stimulus"
90
+ import LiveController from "@isometriks/live_cable/controller"
91
+ import LiveCableBlessing from "@isometriks/live_cable/blessing"
92
+ import "@isometriks/live_cable" // Automatically starts DOM observer
93
+
94
+ // Enable the blessing for all controllers
95
+ Controller.blessings = [
96
+ ...Controller.blessings,
97
+ LiveCableBlessing
98
+ ]
99
+
100
+ const application = Application.start()
101
+ application.register("live", LiveController)
102
+ ```
103
+
104
+ This adds the `liveCableAction(action, params)` method to all your Stimulus controllers:
105
+
106
+ ```javascript
107
+ // In your custom controller
108
+ export default class extends Controller {
109
+ submit() {
110
+ // Dispatch an action to the LiveCable component
111
+ this.liveCableAction('save', {
112
+ title: this.titleTarget.value
113
+ })
114
+ }
115
+ }
116
+ ```
117
+
118
+ The action will be dispatched as a DOM event that bubbles up to the nearest LiveCable component. This is useful when you need to trigger LiveCable actions from custom controllers or third-party integrations.
119
+
120
+ ## Subscription Persistence
121
+
122
+ LiveCable's subscription manager keeps subscriptions alive when a Stimulus controller disconnects and reconnects within the same page — for example when a parent component re-renders and morphs its children, or when a sortable list reorders its items. The subscription is only destroyed when the parent component does another render cycle and sees that the child is no longer rendered.
123
+
124
+ ### Benefits
125
+
126
+ - **Reduced WebSocket churn**: No reconnection overhead during within-page Stimulus reconnects
127
+ - **No race conditions**: Avoids issues from rapid connect/disconnect cycles
128
+ - **Better performance**: Eliminates unnecessary subscription setup/teardown
129
+
130
+ ### Turbo Drive
131
+
132
+ The underlying WebSocket connection stays open across Turbo Drive page navigations. When navigating to a new page, LiveCable closes subscriptions for components that do not appear on the new page and removes their server-side instances. Components that appear on both pages — such as a persistent nav widget — keep their subscriptions and server-side state untouched.
133
+
134
+ LiveCable automatically adds `<meta name="turbo-cache-control" content="no-cache">` to pages that contain live components, preventing Turbo from restoring a stale cached snapshot on back/forward navigation. If you need to override this — for example to set `no-store` — add your own `turbo-cache-control` meta tag and LiveCable will leave it alone.
135
+
136
+ ### Automatic Management
137
+
138
+ Subscription persistence is handled automatically. Components are identified by their `live_id`, and the
139
+ subscription manager ensures each component has exactly one active subscription at any time.
140
+
141
+ When the server sends a `destroy` status, the subscription is removed from the client side and the server side
142
+ channel is destroyed and unsubscribed.
143
+
144
+ ## Lifecycle Callbacks
145
+
146
+ Note on component location and namespacing:
147
+
148
+ - Live components must be defined inside the `Live::` module so they can be safely loaded from a string name.
149
+ - We recommend placing component classes under `app/live/` (so `Live::Counter` maps to `app/live/counter.rb`).
150
+ - Corresponding views should live under `app/views/live/...` (e.g. `app/views/live/counter/component.html.live.erb`).
151
+ - When rendering a component from a view, pass the namespaced underscored path, e.g. `live/counter` (which camelizes to `Live::Counter`).
152
+
153
+ LiveCable uses `ActiveModel::Callbacks` to provide lifecycle callbacks that you can hook into at different stages of a component's lifecycle.
154
+
155
+ ### Available Callbacks
156
+
157
+ - **`before_connect`** / **`after_connect`**: Called when the component is first subscribed to the channel. Use `after_connect` for initializing timers, subscribing to external services, or loading additional data.
158
+
159
+ - **`before_disconnect`** / **`after_disconnect`**: Called when the component is unsubscribed from the channel. Use `before_disconnect` for cleanup: stop timers, unsubscribe from external services, or save state before disconnection.
160
+
161
+ - **`before_render`** / **`after_render`**: Called before and after each render and broadcast, including the initial render. Use `before_render` for preparing data, performing calculations, or validating state. Use `after_render` for triggering side effects or cleanup after the DOM has been updated.
162
+
163
+ ### Registering Callbacks
164
+
165
+ Use standard ActiveModel callback syntax to register your callbacks:
166
+
167
+ ```ruby
168
+ module Live
169
+ class ChatRoom < LiveCable::Component
170
+ reactive :messages, -> { [] }
171
+
172
+ after_render :log_render_time
173
+
174
+ actions :add_message
175
+
176
+ def add_message(params)
177
+ messages << { text: params[:text], timestamp: Time.current }
178
+ end
179
+
180
+ private
181
+
182
+ def log_render_time
183
+ Rails.logger.info "Rendered at #{Time.current}"
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
189
+ You can also use callbacks with conditionals:
190
+
191
+ ```ruby
192
+ module Live
193
+ class Dashboard < LiveCable::Component
194
+ before_connect :authenticate_user, if: :requires_auth?
195
+ after_render :track_analytics, unless: :development_mode?
196
+
197
+ private
198
+
199
+ def requires_auth?
200
+ # Your auth logic
201
+ end
202
+
203
+ def development_mode?
204
+ Rails.env.development?
205
+ end
206
+ end
207
+ end
208
+ ```
209
+
210
+ ### Callback Execution Order
211
+
212
+ When a component is subscribed:
213
+ 1. Component is instantiated
214
+ 2. `before_connect` callbacks are called
215
+ 3. Connection is established and stream starts
216
+ 4. `after_connect` callbacks are called
217
+ 5. `before_render` callbacks are called
218
+ 6. Component is rendered and broadcast
219
+ 7. `after_render` callbacks are called
220
+
221
+ On subsequent updates (action calls, reactive variable changes):
222
+ 1. State changes occur
223
+ 2. `before_render` callbacks are called
224
+ 3. Component is rendered and broadcast
225
+ 4. `after_render` callbacks are called
226
+
227
+ When a component is unsubscribed:
228
+ 1. `before_disconnect` callbacks are called
229
+ 2. Connection is cleaned up and streams are stopped
230
+ 3. `after_disconnect` callbacks are called
231
+ 4. Component is cleaned up
232
+
233
+ ## Generators
234
+
235
+ LiveCable includes a Rails generator to quickly scaffold components:
236
+
237
+ ```bash
238
+ # Basic component
239
+ bin/rails generate live_cable:component counter
240
+
241
+ # With reactive variables and actions
242
+ bin/rails generate live_cable:component counter --reactive count:integer step:integer --actions increment decrement
243
+
244
+ # Compound component
245
+ bin/rails generate live_cable:component wizard --compound
246
+
247
+ # Namespaced component
248
+ bin/rails generate live_cable:component chat/message --reactive body:string
249
+ ```
250
+
251
+ Supported reactive variable types: `integer`, `string`, `boolean`, `array`, `hash` (defaults to `nil` if omitted).
252
+
253
+ ## Basic Usage
254
+
255
+ ### 1. Create a Component
256
+
257
+ ```ruby
258
+ # app/components/live/counter.rb
259
+ module Live
260
+ class Counter < LiveCable::Component
261
+ reactive :count, -> { 0 }
262
+
263
+ actions :increment, :decrement
264
+
265
+ def increment
266
+ self.count += 1
267
+ end
268
+
269
+ def decrement
270
+ self.count -= 1
271
+ end
272
+ end
273
+ end
274
+ ```
275
+
276
+ ### 2. Create a Template
277
+
278
+ Component templates should start with a root element. LiveCable will automatically add the necessary attributes to wire up the component:
279
+
280
+ ```erb
281
+ <%# app/views/live/counter/component.html.live.erb %>
282
+ <div>
283
+ <h2>Counter: <%= count %></h2>
284
+ <button live-action="increment">+</button>
285
+ <button live-action="decrement">-</button>
286
+ </div>
287
+ ```
288
+
289
+ LiveCable automatically injects the required attributes (`live-id`, `live-component`, `live-actions`, and `live-defaults`) into your root element and transforms them into Stimulus attributes.
290
+
291
+ #### Optimized Rendering with `.live.erb`
292
+
293
+ For better performance, you should use `.html.live.erb` templates instead of `.html.erb`. These templates use the Herb templating engine to parse your template and send only the updated parts over the wire, rather than the full HTML on every render:
294
+
295
+ ```erb
296
+ <%# app/views/live/counter/component.html.live.erb %>
297
+ <div>
298
+ <h2>Counter: <%= count %></h2>
299
+ <button live-action="increment">+</button>
300
+ <button live-action="decrement">-</button>
301
+ </div>
302
+ ```
303
+
304
+ **Important notes about `.live.erb` files:**
305
+ - They should only be used for component templates, not for regular Rails views
306
+ - They return a special partial object that tracks static and dynamic parts, not a string
307
+ - They're optional—`.html.erb` files, and other templating systems will work, but they will send full template diffs on changes.
308
+ - When a `.live.erb` template is first rendered, LiveCable sends the full template and tracks which parts are static
309
+ - On subsequent renders, only the changed dynamic parts are sent to the client
310
+ - This can significantly reduce bandwidth and improve performance for frequently updating components
311
+
312
+ **Performance tip:** Use CSS classes instead of wrapping large blocks in control statements. Wrapping content in `<% if condition %>` creates a single large chunk that must be entirely replaced. Instead, use `<div class="<%= 'hidden' unless condition %>">` to only update the class attribute while keeping the content static.
313
+
314
+ ##### How Smart Re-rendering Works
315
+
316
+ LiveCable uses **Herb** (an ERB parser) and **Prism** (a Ruby parser) to analyze your `.live.erb` templates at compile time and determine which parts need to be re-rendered when reactive variables change.
317
+
318
+ **Template Parsing and Dependency Analysis:**
319
+
320
+ When a `.live.erb` template is compiled, LiveCable:
321
+
322
+ 1. **Splits the template into parts**: Herb parses the template and identifies static text, Ruby code blocks (`<% ... %>`), and expressions (`<%= ... %>`)
323
+
324
+ 2. **Analyzes each dynamic part with Prism**: For every Ruby code block and expression, Prism parses the Ruby code into an Abstract Syntax Tree (AST)
325
+
326
+ 3. **Tracks dependencies**: A `DependencyVisitor` walks the AST to identify:
327
+ - **Reactive variable reads**: Direct references to reactive variables (e.g., `count`, `step`)
328
+ - **Component method calls**: Methods called on the component (e.g., `component.products`, `total_price`)
329
+ - **Local variable dependencies**: Local variables defined in one part and used in another
330
+
331
+ 4. **Builds metadata**: Each dynamic part gets metadata about what it depends on:
332
+ ```ruby
333
+ {
334
+ component_dependencies: [:count, :step], # Reactive vars this part reads
335
+ component_method_calls: [:products], # Methods called on component
336
+ local_dependencies: [:user], # Locals from previous parts
337
+ defines_locals: [:total] # Locals this part defines
338
+ }
339
+ ```
340
+
341
+ **Method Dependency Expansion:**
342
+
343
+ For component methods, LiveCable goes deeper by analyzing the method implementation itself:
344
+
345
+ ```ruby
346
+ # Component class
347
+ def total_price
348
+ items.sum { |item| item.price * item.quantity }
349
+ end
350
+ ```
351
+
352
+ The `MethodAnalyzer` uses Prism to parse the component's source file and build a dependency graph:
353
+ - `total_price` method reads the `items` reactive variable
354
+ - When `items` changes, any template part calling `component.total_price` is re-rendered
355
+
356
+ This analysis is done once and cached, creating a transitive dependency map for all component methods.
357
+
358
+ **Runtime Re-rendering Decision:**
359
+
360
+ When a reactive variable changes (e.g., `self.count = 5`), LiveCable:
361
+
362
+ 1. **Receives change notification**: The change tracking system reports which variables changed (e.g., `[:count]`)
363
+
364
+ 2. **Evaluates each template part**: For each dynamic part, the `PartialRenderer` checks:
365
+ ```ruby
366
+ should_skip_part?(changes, component_dependencies, component_method_calls, local_dependencies)
367
+ ```
368
+
369
+ 3. **Expands method dependencies**: If the part calls `component.products`, the method analyzer expands this to find which reactive variables `products` depends on
370
+
371
+ 4. **Makes the skip decision**:
372
+ - **Skip** if none of the part's dependencies changed
373
+ - **Render** if any component dependency, method dependency, or local dependency changed
374
+ - **Always render** on initial render (`:all`) or template switches (`:dynamic`)
375
+
376
+ 5. **Returns selective updates**: Only the parts that need updating are rendered and sent to the client as an array:
377
+ ```ruby
378
+ [nil, nil, "<span>5</span>", nil, "<button>Reset</button>"]
379
+ # ^ ^ └─ changed ^ └─ changed
380
+ # | └─ skipped └─ skipped
381
+ # └─ skipped
382
+ ```
383
+
384
+ **Example: Counter Template Analysis**
385
+
386
+ Given this template:
387
+ ```erb
388
+ <div>
389
+ <h2>Counter: <%= count %></h2>
390
+ <button live-action="increment">+ <%= step %></button>
391
+ <button live-action="reset">Reset</button>
392
+ <input name="step" value="<%= step %>" live-reactive>
393
+ </div>
394
+ ```
395
+
396
+ LiveCable analyzes and tracks:
397
+ - `<h2>Counter: <%= count %></h2>` depends on `count`
398
+ - `<button>+ <%= step %></button>` depends on `step`
399
+ - `<button>Reset</button>` is static (never changes)
400
+ - `<input value="<%= step %>">` depends on `step`
401
+
402
+ When `count` changes: Only the `<h2>` element is re-rendered and sent to the client.
403
+
404
+ When `step` changes: Both the button label and input value are re-rendered and sent.
405
+
406
+ The `Reset` button is never re-rendered since it contains no dynamic content.
407
+
408
+ **Local Variable Tracking:**
409
+
410
+ Templates can define local variables that are used in later parts:
411
+
412
+ ```erb
413
+ <% user = component.current_user %>
414
+ <% total = component.calculate_total %>
415
+
416
+ <div>Welcome <%= user.name %></div>
417
+ <div>Total: <%= total %></div>
418
+ ```
419
+
420
+ LiveCable tracks:
421
+ - First part defines `user` and `total` (always executes to define locals)
422
+ - Second part depends on `user` local (re-renders only if `user` local was redefined)
423
+ - Third part depends on `total` local (re-renders only if `total` local was redefined)
424
+
425
+ The `mark_locals_dirty` mechanism ensures that if a local is recomputed (because its dependencies changed), any parts using that local are also re-rendered.
426
+
427
+ **Performance Benefits:**
428
+
429
+ This intelligent analysis provides significant performance improvements:
430
+
431
+ - **Reduced bandwidth**: Only changed HTML fragments are sent over WebSocket
432
+ - **Faster rendering**: Server only executes code for parts that changed
433
+ - **No client-side diffing needed**: Client receives exact parts to update
434
+ - **Efficient method calls**: Component methods are only called if their dependencies changed
435
+ - **Cache-friendly**: Dependency analysis happens once at compile time, not on every render
436
+
437
+ ### 3. Use in Your View
438
+
439
+ Render components using the `live` helper method:
440
+
441
+ ```erb
442
+ <%# Simple usage %>
443
+ <%= live('counter', id: 'my-counter') %>
444
+
445
+ <%# With default values %>
446
+ <%= live('counter', id: 'my-counter', count: 10, step: 5) %>
447
+ ```
448
+
449
+ The `live` helper automatically:
450
+ - Creates component instances with unique IDs
451
+ - Wraps the component in proper Stimulus controller attributes
452
+ - Passes default values to reactive variables
453
+ - Reuses existing component instances when navigating back
454
+
455
+ If you already have a component instance, use `render` directly:
456
+
457
+ ```erb
458
+ <%
459
+ @counter = Live::Counter.new('my-counter')
460
+ @counter.count = 10
461
+ %>
462
+ <%= render(@counter) %>
463
+ ```
464
+
465
+ ## Reactive Variables
466
+
467
+ Reactive variables automatically trigger re-renders when changed. Define them with default values using lambdas:
468
+
469
+ ```ruby
470
+ module Live
471
+ class ShoppingCart < LiveCable::Component
472
+ reactive :items, -> { [] }
473
+ reactive :discount_code, -> { nil }, writable: true
474
+ reactive :total, -> { 0.0 }
475
+
476
+ actions :add_item, :remove_item, :apply_discount
477
+
478
+ def add_item(params)
479
+ items << { id: params[:id], name: params[:name], price: params[:price].to_f }
480
+ calculate_total
481
+ end
482
+
483
+ def remove_item(params)
484
+ items.reject! { |item| item[:id] == params[:id] }
485
+ calculate_total
486
+ end
487
+
488
+ def apply_discount(params)
489
+ self.discount_code = params[:code]
490
+ calculate_total
491
+ end
492
+
493
+ private
494
+
495
+ def calculate_total
496
+ subtotal = items.sum { |item| item[:price] }
497
+ discount = discount_code ? apply_discount_rate(subtotal) : 0
498
+ self.total = subtotal - discount
499
+ end
500
+
501
+ def apply_discount_rate(subtotal)
502
+ discount_code == "SAVE10" ? subtotal * 0.1 : 0
503
+ end
504
+ end
505
+ end
506
+ ```
507
+
508
+ ## Automatic Change Tracking
509
+
510
+ LiveCable automatically tracks changes to reactive variables containing Arrays, Hashes, and ActiveRecord models. You can mutate these objects directly without manual re-assignment:
511
+
512
+ ```ruby
513
+ module Live
514
+ class TaskManager < LiveCable::Component
515
+ reactive :tasks, -> { [] }
516
+ reactive :settings, -> { {} }
517
+ reactive :project, -> { Project.find_by(id: params[:project_id]) }
518
+
519
+ actions :add_task, :update_setting, :update_project_name
520
+
521
+ # Arrays - direct mutation triggers re-render
522
+ def add_task(params)
523
+ tasks << { title: params[:title], completed: false }
524
+ end
525
+
526
+ # Hashes - direct mutation triggers re-render
527
+ def update_setting(params)
528
+ settings[params[:key]] = params[:value]
529
+ end
530
+
531
+ # ActiveRecord - direct mutation triggers re-render
532
+ def update_project_name(params)
533
+ project.name = params[:name]
534
+ end
535
+ end
536
+ end
537
+ ```
538
+
539
+ ### Nested Structures
540
+
541
+ Change tracking works recursively through nested structures:
542
+
543
+ ```ruby
544
+ module Live
545
+ class Organization < LiveCable::Component
546
+ reactive :data, -> { { teams: [{ name: 'Engineering', members: [] }] } }
547
+
548
+ actions :add_member
549
+
550
+ def add_member(params)
551
+ # Deeply nested mutation - automatically triggers re-render
552
+ data[:teams].first[:members] << params[:name]
553
+ end
554
+ end
555
+ end
556
+ ```
557
+
558
+ ### How It Works
559
+
560
+ When you store an Array, Hash, or ActiveRecord model in a reactive variable:
561
+
562
+ 1. **Automatic Wrapping**: LiveCable wraps the value in a transparent Delegator
563
+ 2. **Observer Attachment**: An Observer is attached to track mutations
564
+ 3. **Change Detection**: When you call mutating methods (`<<`, `[]=`, `update`, etc.), the Observer is notified
565
+ 4. **Smart Re-rendering**: Only components with changed variables are re-rendered
566
+
567
+ This means you can write natural Ruby code without worrying about triggering updates:
568
+
569
+ ```ruby
570
+ # These all work and trigger updates automatically:
571
+ tags << 'ruby'
572
+ tags.concat(%w[rails rspec])
573
+ settings[:theme] = 'dark'
574
+ user.update(name: 'Jane')
575
+ ```
576
+
577
+ ### Primitives (Strings, Numbers, etc.)
578
+
579
+ Primitive values (String, Integer, Float, Boolean, Symbol) cannot be mutated in place, so you must reassign them:
580
+
581
+ ```ruby
582
+ reactive :count, -> { 0 }
583
+ reactive :name, -> { "" }
584
+
585
+ # ✅ This works (reassignment)
586
+ self.count = count + 1
587
+ self.name = "John"
588
+
589
+ # ❌ This won't trigger updates (mutation, but primitives are immutable)
590
+ self.count.+(1)
591
+ self.name.concat("Doe")
592
+ ```
593
+
594
+ For more details on the change tracking architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
595
+
596
+ ## Shared Variables
597
+
598
+ Shared variables allow multiple components on the same connection to access the same state. There are two types:
599
+
600
+ ### Shared Reactive Variables
601
+
602
+ Shared reactive variables trigger re-renders on **all** components that use them:
603
+
604
+ ```ruby
605
+ module Live
606
+ class ChatMessage < LiveCable::Component
607
+ reactive :messages, -> { [] }, shared: true
608
+ reactive :username, -> { "Guest" }
609
+
610
+ actions :send_message
611
+
612
+ def send_message(params)
613
+ messages << { user: username, text: params[:text], time: Time.current }
614
+ end
615
+ end
616
+ end
617
+ ```
618
+
619
+ When any component updates `messages`, all components using this shared reactive variable will re-render.
620
+
621
+ ### Shared Non-Reactive Variables
622
+
623
+ Use `shared` (without `reactive`) when you need to share state but don't want updates to trigger re-renders in the component that doesn't display that data:
624
+
625
+ ```ruby
626
+ module Live
627
+ class FilterPanel < LiveCable::Component
628
+ shared :cart_items, -> { [] } # Access cart but don't re-render on cart changes
629
+ reactive :filter, -> { "all" }
630
+
631
+ actions :update_filter
632
+
633
+ def update_filter(params)
634
+ self.filter = params[:filter]
635
+ # Can read cart_items.length but changing cart elsewhere won't re-render this
636
+ end
637
+ end
638
+ end
639
+
640
+ module Live
641
+ class CartDisplay < LiveCable::Component
642
+ reactive :cart_items, -> { [] }, shared: true # Re-renders on cart changes
643
+
644
+ actions :add_to_cart
645
+
646
+ def add_to_cart(params)
647
+ cart_items << params[:item]
648
+ # CartDisplay re-renders, but FilterPanel does not
649
+ end
650
+ end
651
+ end
652
+ ```
653
+
654
+ **Use case**: FilterPanel can read the cart to show item count in a badge, but doesn't need to re-render every time an item is added—only when the filter changes.
655
+
656
+ ## Action Whitelisting
657
+
658
+ For security, explicitly declare which actions can be called from the frontend:
659
+
660
+ ```ruby
661
+ module Live
662
+ class Secure < LiveCable::Component
663
+ actions :safe_action, :another_safe_action
664
+
665
+ def safe_action
666
+ # This can be called from the frontend
667
+ end
668
+
669
+ def another_safe_action(params)
670
+ # This can also be called with parameters
671
+ end
672
+
673
+ private
674
+
675
+ def internal_method
676
+ # This cannot be called from the frontend
677
+ end
678
+ end
679
+ end
680
+ ```
681
+
682
+ **Note on `params` argument**: The `params` argument is optional. Action methods only receive `params` if you declare the argument in the method signature:
683
+
684
+ ```ruby
685
+ # These are both valid:
686
+ def increment
687
+ self.count += 1 # No params needed
688
+ end
689
+
690
+ def add_todo(params)
691
+ todos << params[:text] # Params are used
692
+ end
693
+ ```
694
+
695
+ If you don't need parameters from the frontend, simply omit the `params` argument from your method definition.
696
+
697
+ ## Writable Reactive Variables
698
+
699
+ By default, reactive variables are **read-only from the client**. This prevents users from manipulating the DOM (e.g., via browser dev tools) to update variables that were never intended to be client-settable, such as a `user_id` or `total_price`.
700
+
701
+ To allow a reactive variable to be updated from the client via `live-reactive`, mark it as `writable:`:
702
+
703
+ ```ruby
704
+ module Live
705
+ class Counter < LiveCable::Component
706
+ reactive :count, -> { 0 } # Server-only, cannot be set from the client
707
+ reactive :step, -> { 1 }, writable: true # Can be updated via live-reactive inputs
708
+
709
+ actions :increment
710
+
711
+ def increment
712
+ self.count += step.to_i
713
+ end
714
+ end
715
+ end
716
+ ```
717
+
718
+ If a client attempts to update a non-writable variable (e.g., by changing an input's `name` attribute in the browser), the server will reject the update and raise an error.
719
+
720
+ The `writable:` option works with all reactive variable types:
721
+
722
+ ```ruby
723
+ reactive :filter, -> { "all" }, writable: true # Writable local variable
724
+ reactive :search, -> { "" }, shared: true, writable: true # Writable shared variable
725
+ ```
726
+
727
+ ### Working with ActionController::Parameters
728
+
729
+ The `params` argument is an `ActionController::Parameters` instance, which means you can use strong parameters and all the standard Rails parameter handling methods:
730
+
731
+ ```ruby
732
+ module Live
733
+ class UserProfile < LiveCable::Component
734
+ reactive :user, ->(component) { User.find(component.defaults[:user_id]) }
735
+ reactive :errors, -> { {} }
736
+
737
+ actions :update_profile
738
+
739
+ def update_profile(params)
740
+ # Use params.expect (Rails 8+) or params.require/permit for strong parameters
741
+ user_params = params.expect(user: [:name, :email, :bio])
742
+
743
+ if user.update(user_params)
744
+ self.errors = {}
745
+ else
746
+ self.errors = user.errors.messages
747
+ end
748
+ end
749
+ end
750
+ end
751
+ ```
752
+
753
+ You can also use `assign_attributes` if you want to validate before saving:
754
+
755
+ ```ruby
756
+ def update_profile(params)
757
+ user_params = params.expect(user: [:name, :email, :bio])
758
+
759
+ user.assign_attributes(user_params)
760
+
761
+ if user.valid?
762
+ user.save
763
+ self.errors = {}
764
+ else
765
+ self.errors = user.errors.messages
766
+ end
767
+ end
768
+ ```
769
+
770
+ This works seamlessly with form helpers:
771
+
772
+ ```erb
773
+ <form live-form="update_profile">
774
+ <div>
775
+ <label>Name</label>
776
+ <input type="text" name="user[name]" value="<%= user.name %>" />
777
+ <% if errors[:name] %>
778
+ <span class="error"><%= errors[:name].join(", ") %></span>
779
+ <% end %>
780
+ </div>
781
+
782
+ <div>
783
+ <label>Email</label>
784
+ <input type="email" name="user[email]" value="<%= user.email %>" />
785
+ <% if errors[:email] %>
786
+ <span class="error"><%= errors[:email].join(", ") %></span>
787
+ <% end %>
788
+ </div>
789
+
790
+ <div>
791
+ <label>Bio</label>
792
+ <textarea name="user[bio]"><%= user.bio %></textarea>
793
+ </div>
794
+
795
+ <button type="submit">Update Profile</button>
796
+ </form>
797
+ ```
798
+
799
+ ## Custom HTML Attributes
800
+
801
+ LiveCable provides custom HTML attributes that are automatically transformed into Stimulus attributes. These attributes use a shortened syntax similar to Stimulus but are more concise.
802
+
803
+ ### `live-action`
804
+
805
+ Triggers a component action when an event occurs.
806
+
807
+ **Syntax:**
808
+ - `live-action="action_name"` - Uses Stimulus default event (click for buttons, submit for forms)
809
+ - `live-action="event->action_name"` - Custom event
810
+ - `live-action="event1->action1 event2->action2"` - Multiple actions
811
+
812
+ **Examples:**
813
+
814
+ ```html
815
+ <!-- Default event (click) -->
816
+ <button live-action="save">Save</button>
817
+
818
+ <!-- Custom event -->
819
+ <button live-action="mouseover->highlight">Hover Me</button>
820
+
821
+ <!-- Multiple actions -->
822
+ <button live-action="click->save focus->track_focus">Save and Track</button>
823
+ ```
824
+
825
+ **Transformation:** `live-action="save"` becomes `data-action="live#action_$save"`
826
+
827
+ ### `live-form`
828
+
829
+ Serializes a form and submits it to a component action.
830
+
831
+ **Syntax:**
832
+ - `live-form="action_name"` - Uses Stimulus default event (submit)
833
+ - `live-form="event->action_name"` - Custom event
834
+ - `live-form="event1->action1 event2->action2"` - Multiple actions
835
+
836
+ **Examples:**
837
+
838
+ ```html
839
+ <!-- Default event (submit) -->
840
+ <form live-form="save">
841
+ <input type="text" name="title">
842
+ <button type="submit">Save</button>
843
+ </form>
844
+
845
+ <!-- On change event -->
846
+ <form live-form="change->filter">
847
+ <select name="category">...</select>
848
+ </form>
849
+
850
+ <!-- Multiple actions -->
851
+ <form live-form="submit->save change->auto_save">
852
+ <input type="text" name="content">
853
+ </form>
854
+ ```
855
+
856
+ **Transformation:** `live-form="save"` becomes `data-action="live#form_$save"`
857
+
858
+ ### `live-value-*`
859
+
860
+ Passes parameters to actions on the same element.
861
+
862
+ **Syntax:** `live-value-param-name="value"`
863
+
864
+ **Examples:**
865
+
866
+ ```html
867
+ <!-- Single parameter -->
868
+ <button live-action="update" live-value-id="123">Update Item</button>
869
+
870
+ <!-- Multiple parameters -->
871
+ <button live-action="create"
872
+ live-value-type="task"
873
+ live-value-priority="high">
874
+ Create Task
875
+ </button>
876
+ ```
877
+
878
+ **Transformation:** `live-value-id="123"` becomes `data-live-id-param="123"`
879
+
880
+ ### `live-reactive`
881
+
882
+ Updates a reactive variable when an input changes. The corresponding reactive variable must be declared with `writable: true` in the component.
883
+
884
+ **Syntax:**
885
+ - `live-reactive` - Uses Stimulus default event (input for text fields)
886
+ - `live-reactive="event"` - Single event
887
+ - `live-reactive="event1 event2"` - Multiple events
888
+
889
+ **Examples:**
890
+
891
+ ```html
892
+ <!-- Default event (input) -->
893
+ <input type="text" name="username" value="<%= username %>" live-reactive>
894
+
895
+ <!-- Specific event -->
896
+ <input type="text" name="search" live-reactive="keydown">
897
+
898
+ <!-- Multiple events -->
899
+ <input type="text" name="query" live-reactive="keydown keyup">
900
+ ```
901
+
902
+ **Transformation:** `live-reactive` becomes `data-action="live#reactive"`, and `live-reactive="keydown"` becomes `data-action="keydown->live#reactive"`
903
+
904
+ ### `live-debounce`
905
+
906
+ Adds debouncing to reactive and form updates to reduce network traffic.
907
+
908
+ **Syntax:** `live-debounce="milliseconds"`
909
+
910
+ **Examples:**
911
+
912
+ ```html
913
+ <!-- Debounced reactive input (300ms delay) -->
914
+ <input type="text" name="search" live-reactive live-debounce="300">
915
+
916
+ <!-- Debounced form submission (1000ms delay) -->
917
+ <form live-form="change->filter" live-debounce="1000">
918
+ <select name="category">...</select>
919
+ </form>
920
+ ```
921
+
922
+ **Transformation:** `live-debounce="300"` becomes `data-live-debounce-param="300"`
923
+
924
+ ### Complete Example
925
+
926
+ ```html
927
+ <div>
928
+ <h2>Search Products</h2>
929
+
930
+ <!-- Reactive search with debouncing -->
931
+ <input type="text"
932
+ name="query"
933
+ value="<%= query %>"
934
+ live-reactive
935
+ live-debounce="300">
936
+
937
+ <!-- Form with multiple actions and parameters -->
938
+ <form live-form="submit->filter change->auto_filter" live-debounce="500">
939
+ <select name="category">
940
+ <option value="all">All</option>
941
+ <option value="electronics">Electronics</option>
942
+ </select>
943
+ </form>
944
+
945
+ <!-- Action buttons with parameters -->
946
+ <button live-action="add_to_cart"
947
+ live-value-product-id="<%= product.id %>"
948
+ live-value-quantity="1">
949
+ Add to Cart
950
+ </button>
951
+
952
+ <!-- Multiple events -->
953
+ <button live-action="click->save mouseover->preview">
954
+ Save & Preview
955
+ </button>
956
+ </div>
957
+ ```
958
+
959
+ ### Race Condition Handling
960
+
961
+ When a form action is triggered, the controller manages potential race conditions with pending reactive updates:
962
+
963
+ 1. **Priority**: Any pending debounced `reactive` message is sent **immediately before** the form action message in the same payload.
964
+ 2. **Order**: This guarantees that the server applies the reactive update first, then the form action.
965
+ 3. **Debounce Cancellation**: Any pending debounced form or reactive submissions are canceled, ensuring only the latest state is processed.
966
+
967
+ This mechanism prevents scenarios where a delayed reactive update (e.g., from typing quickly) could arrive after a form
968
+ submission and overwrite the changes made by the form action.
969
+
970
+ ## DOM Control Attributes
971
+
972
+ LiveCable supports special HTML attributes to control how the DOM is updated during morphing.
973
+
974
+ ### `live-ignore`
975
+
976
+ When `live-ignore` is present on an element, LiveCable (via morphdom) will skip updating that element's children during a re-render.
977
+
978
+ - **Usage**: `<div live-ignore>...</div>`
979
+ - **Behavior**: Prevents the element's content from being modified by server updates.
980
+ - **Default**: Live components automatically have this attribute to ensure the parent component doesn't overwrite the child component's state.
981
+
982
+ ### `live-key`
983
+
984
+ The `live-key` attribute acts as a hint for the diffing algorithm to identify elements in a list. This allows elements to be reordered rather than destroyed and recreated, preserving their internal state (like input focus or selection).
985
+
986
+ - **Usage**: `<div live-key="unique_id">...</div>`
987
+ - **Behavior**: Matches elements across renders to maintain identity.
988
+ - **Notes**:
989
+ - The key must be unique within the context of the parent element.
990
+ - `id` attributes are also used as keys if `live-key` is not present, but `live-key` is preferred in loops to avoid ID collisions or valid HTML ID constraints.
991
+ - Do not use array indices as keys; use a stable identifier from your data (e.g., database ID). If you reorder or add / remove elements from your array the index will no longer match the proper component.
992
+
993
+ **Example:**
994
+
995
+ ```erb
996
+ <% todos.each do |todo| %>
997
+ <li live-key="<%= todo.id %>">
998
+ ...
999
+ </li>
1000
+ <% end %>
1001
+ ```
1002
+
1003
+ ## Compound Components
1004
+
1005
+ By default, components render the partial at `app/views/live/component_name.html.live.erb`. You can organize your templates differently by marking a component as `compound`.
1006
+
1007
+ ```ruby
1008
+ module Live
1009
+ class Checkout < LiveCable::Component
1010
+ compound
1011
+ # Component will look for templates in app/views/live/checkout/
1012
+ end
1013
+ end
1014
+ ```
1015
+
1016
+ When `compound` is used, the component will look for its template in a directory named after the component. By default, it renders `app/views/live/component_name/component.html.live.erb`.
1017
+
1018
+ ### Dynamic Templates with `variant`
1019
+
1020
+ Override the `variant` method to dynamically switch between different templates:
1021
+
1022
+ ```ruby
1023
+ module Live
1024
+ class Wizard < LiveCable::Component
1025
+ compound
1026
+ reactive :current_step, -> { "account" }
1027
+ reactive :form_data, -> { {} }
1028
+
1029
+ actions :next_step, :previous_step
1030
+
1031
+ def variant
1032
+ current_step # Renders app/views/live/wizard/account.html.live.erb, etc.
1033
+ end
1034
+
1035
+ def next_step(params)
1036
+ form_data.merge!(params)
1037
+ self.current_step = case current_step
1038
+ when "account" then "billing"
1039
+ when "billing" then "confirmation"
1040
+ else "complete"
1041
+ end
1042
+ end
1043
+
1044
+ def previous_step
1045
+ self.current_step = case current_step
1046
+ when "billing" then "account"
1047
+ when "confirmation" then "billing"
1048
+ else current_step
1049
+ end
1050
+ end
1051
+ end
1052
+ end
1053
+ ```
1054
+
1055
+ This creates a multi-step wizard with templates in:
1056
+ - `app/views/live/wizard/account.html.live.erb`
1057
+ - `app/views/live/wizard/billing.html.live.erb`
1058
+ - `app/views/live/wizard/confirmation.html.live.erb`
1059
+ - `app/views/live/wizard/complete.html.live.erb`
1060
+
1061
+ ## Using Component Methods for Memory Efficiency
1062
+
1063
+ You can call component methods instead of storing large datasets in reactive variables.
1064
+
1065
+ **Why this matters:** Reactive variables are stored in memory in the server-side container. For large datasets (like paginated results), this can add up quickly and consume unnecessary memory.
1066
+
1067
+ **Best practice:** Use reactive variables for state (like page numbers, filters), but call methods to fetch data on-demand during rendering:
1068
+
1069
+ ```ruby
1070
+ module Live
1071
+ class ProductList < LiveCable::Component
1072
+ reactive :page, -> { 0 }
1073
+ reactive :category, -> { "all" }
1074
+
1075
+ actions :next_page, :prev_page, :change_category
1076
+
1077
+ def products
1078
+ # Fetched fresh on each render, not stored in memory
1079
+ Product.where(category_filter)
1080
+ .offset(page * 20)
1081
+ .limit(20)
1082
+ end
1083
+
1084
+ def next_page
1085
+ self.page += 1
1086
+ end
1087
+
1088
+ def prev_page
1089
+ self.page = [page - 1, 0].max
1090
+ end
1091
+
1092
+ def change_category(params)
1093
+ self.category = params[:category]
1094
+ self.page = 0
1095
+ end
1096
+
1097
+ private
1098
+
1099
+ def category_filter
1100
+ category == "all" ? {} : { category: category }
1101
+ end
1102
+ end
1103
+ end
1104
+ ```
1105
+
1106
+ ### In `.live.erb` Templates
1107
+
1108
+ `.live.erb` templates automatically forward method calls to your component through `method_missing`, so you can call component methods and reactive variables directly:
1109
+
1110
+ ```erb
1111
+ <%# app/views/live/product_list/component.html.live.erb %>
1112
+ <div class="products">
1113
+ <% products.each do |product| %>
1114
+ <div class="product">
1115
+ <h3><%= product.name %></h3>
1116
+ <p><%= product.price %></p>
1117
+ </div>
1118
+ <% end %>
1119
+ </div>
1120
+
1121
+ <div class="pagination">
1122
+ <button live-action="prev_page">Previous</button>
1123
+ <span>Page <%= page + 1 %></span>
1124
+ <button live-action="next_page">Next</button>
1125
+ </div>
1126
+ ```
1127
+
1128
+ ### In Regular `.erb` or Other Templating Languages
1129
+
1130
+ If you're using regular `.erb` files or other templating languages, you must use the `component` local to access component methods and reactive variables:
1131
+
1132
+ ```erb
1133
+ <%# app/views/live/product_list/component.html.erb %>
1134
+ <div class="products">
1135
+ <% component.products.each do |product| %>
1136
+ <div class="product">
1137
+ <h3><%= product.name %></h3>
1138
+ <p><%= product.price %></p>
1139
+ </div>
1140
+ <% end %>
1141
+ </div>
1142
+
1143
+ <div class="pagination">
1144
+ <button live-action="prev_page">Previous</button>
1145
+ <span>Page <%= component.page + 1 %></span>
1146
+ <button live-action="next_page">Next</button>
1147
+ </div>
1148
+ ```
1149
+
1150
+ This approach:
1151
+ - Keeps only `page` and `category` in memory (lightweight)
1152
+ - Fetches the 20 products fresh on each render
1153
+ - Prevents memory bloat when dealing with large datasets
1154
+ - Still provides reactive updates when `page` or `category` changes
1155
+
1156
+ ## Streaming from ActionCable Channels
1157
+
1158
+ LiveCable components can subscribe to ActionCable channels using the `stream_from` method. This allows components to react to real-time broadcasts from anywhere in your application, making it easy to build collaborative features like chat rooms, live notifications, or shared dashboards.
1159
+
1160
+ ### Basic Usage
1161
+
1162
+ Call `stream_from` in the `after_connect` lifecycle callback to subscribe to a channel:
1163
+
1164
+ ```ruby
1165
+ module Live
1166
+ module Chat
1167
+ class ChatRoom < LiveCable::Component
1168
+ reactive :messages, -> { [] }, shared: true
1169
+
1170
+ after_connect :subscribe_to_chat
1171
+
1172
+ private
1173
+
1174
+ def subscribe_to_chat
1175
+ stream_from("chat_messages", coder: ActiveSupport::JSON) do |data|
1176
+ messages << data
1177
+ end
1178
+ end
1179
+ end
1180
+ end
1181
+ end
1182
+ ```
1183
+
1184
+ ### Broadcasting to Streams
1185
+
1186
+ Any part of your application can broadcast to the stream using ActionCable's broadcast API:
1187
+
1188
+ ```ruby
1189
+ module Live
1190
+ module Chat
1191
+ class ChatInput < LiveCable::Component
1192
+ reactive :message
1193
+ actions :send_message
1194
+
1195
+ def send_message(params)
1196
+ return if params[:message].blank?
1197
+
1198
+ message_data = {
1199
+ id: SecureRandom.uuid,
1200
+ text: params[:message],
1201
+ timestamp: Time.now.to_i,
1202
+ user: current_user.as_json(only: [:id, :first_name, :last_name])
1203
+ }
1204
+
1205
+ # Broadcast to the chat stream
1206
+ ActionCable.server.broadcast("chat_messages", message_data)
1207
+
1208
+ # Clear the input
1209
+ self.message = ""
1210
+ end
1211
+ end
1212
+ end
1213
+ end
1214
+ ```
1215
+
1216
+ ### How It Works
1217
+
1218
+ When a broadcast is received:
1219
+
1220
+ 1. The stream callback is executed with the broadcast payload
1221
+ 2. You can update reactive variables inside the callback
1222
+ 3. LiveCable automatically detects the changes and broadcasts updates to all affected components
1223
+ 4. All components sharing the same reactive variables are re-rendered
1224
+
1225
+ ### Key Features
1226
+
1227
+ - **Automatic re-rendering**: Changes to reactive variables inside stream callbacks trigger re-renders
1228
+ - **Shared state**: Combine with `shared: true` reactive variables to sync state across multiple component instances
1229
+ - **Connection-scoped**: Each user's component instances receive broadcasts independently
1230
+ - **Coder support**: Use `coder: ActiveSupport::JSON` to automatically decode JSON payloads
1231
+
1232
+ ### Use Cases
1233
+
1234
+ - **Chat applications**: Real-time message updates across all participants
1235
+ - **Live notifications**: Push notifications to specific users or groups
1236
+ - **Collaborative editing**: Sync changes across multiple users viewing the same document
1237
+ - **Live dashboards**: Update metrics and charts in real-time
1238
+ - **Presence tracking**: Show who's currently online or viewing a resource
1239
+
1240
+ ## Error Handling
1241
+
1242
+ When an unhandled exception is raised inside a component action, LiveCable replaces the component in the DOM with an error message and cleans up the server-side component.
1243
+
1244
+ ### Default Behaviour
1245
+
1246
+ In development and test environments (`verbose_errors` is `true` by default), the error message shows the component class name, the exception class and message, and a full backtrace:
1247
+
1248
+ ```
1249
+ MyComponent - RuntimeError: something went wrong
1250
+ app/live/my_component.rb:12:in 'do_something'
1251
+ ...
1252
+ ```
1253
+
1254
+ In production (`verbose_errors` defaults to `false`), a generic message is shown with no internal details:
1255
+
1256
+ ```
1257
+ An error occurred
1258
+ ```
1259
+
1260
+ ### Configuration
1261
+
1262
+ Override the default in an initializer:
1263
+
1264
+ ```ruby
1265
+ # config/initializers/live_cable.rb
1266
+ LiveCable.configure do |config|
1267
+ config.verbose_errors = false # never show details
1268
+ # or
1269
+ config.verbose_errors = true # always show details (not recommended in production)
1270
+ end
1271
+ ```
1272
+
1273
+ ## License
1274
+
1275
+ This project is available as open source under the terms of the MIT License.