live_cable 0.0.1 → 0.1.1

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