npm package: @isometriks/live_cable
Description: LiveCable is a Phoenix LiveView-style live component system for Ruby on Rails that tracks state server-side and allows you to call actions from the frontend using Stimulus with a React style state management API.
Latest version: 0.1.1

# LiveCable

LiveCable is a Phoenix LiveView-style live component system for Ruby on Rails that tracks state server-side and allows
you to call actions from the frontend using Stimulus with a React style state management API.

**Full documentation: [livecable.io](https://livecable.io)**

## Features

- **Server-side state management**: Component state is maintained on the server using ActionCable
- **Reactive variables**: Automatic UI updates when state changes with smart change tracking
- **Automatic change detection**: Arrays, Hashes, and ActiveRecord models automatically trigger updates when mutated
- **Action dispatch**: Call server-side methods from the frontend
- **Lifecycle hooks**: Hook into component lifecycle events
- **Stimulus integration**: Seamless integration with Stimulus controllers and blessings API

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'live_cable'
```

And then execute:

```bash
bundle install
```

## Configuration

To use LiveCable, you need to set up your `ApplicationCable::Connection` to initialize a `LiveCable::Connection`.

Add this to your `app/channels/application_cable/connection.rb`:

```ruby
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :live_connection

    def connect
      self.live_connection = LiveCable::Connection.new(self.request)
    end
  end
end
```

## JavaScript Setup

LiveCable works with both **importmap-rails** (default for Rails 7+) and **JavaScript bundlers** (esbuild, Vite, webpack, Rollup via jsbundling-rails or vite_ruby).

### Using importmap-rails (default)

The gem automatically pins all necessary modules when you install it. No additional setup is needed.

### Using a JavaScript bundler (yarn/npm)

Install the npm package:

```bash
yarn add @isometriks/live_cable
# or
npm install @isometriks/live_cable
```

This will also install the required peer dependencies (`@rails/actioncable`, `@hotwired/stimulus`, and `morphdom`).

### Register the LiveController

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:

```javascript
import { Application } from "@hotwired/stimulus"
import LiveController from "@isometriks/live_cable/controller"
import "@isometriks/live_cable"  // Automatically starts DOM observer

const application = Application.start()
application.register("live", LiveController)
```

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.

### 2. Enable LiveCable Blessing (Optional)

If you want to call LiveCable actions from your own Stimulus controllers, add the LiveCable blessing:

```javascript
import { Application, Controller } from "@hotwired/stimulus"
import LiveController from "@isometriks/live_cable/controller"
import LiveCableBlessing from "@isometriks/live_cable/blessing"
import "@isometriks/live_cable"  // Automatically starts DOM observer

// Enable the blessing for all controllers
Controller.blessings = [
  ...Controller.blessings,
  LiveCableBlessing
]

const application = Application.start()
application.register("live", LiveController)
```

This adds the `liveCableAction(action, params)` method to all your Stimulus controllers:

```javascript
// In your custom controller
export default class extends Controller {
  submit() {
    // Dispatch an action to the LiveCable component
    this.liveCableAction('save', {
      title: this.titleTarget.value
    })
  }
}
```

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.

## Subscription Persistence

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.

### Benefits

- **Reduced WebSocket churn**: No reconnection overhead during within-page Stimulus reconnects
- **No race conditions**: Avoids issues from rapid connect/disconnect cycles
- **Better performance**: Eliminates unnecessary subscription setup/teardown

### Turbo Drive

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.

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.

### Automatic Management

Subscription persistence is handled automatically. Components are identified by their `live_id`, and the
subscription manager ensures each component has exactly one active subscription at any time.

When the server sends a `destroy` status, the subscription is removed from the client side and the server side
channel is destroyed and unsubscribed.

## Lifecycle Callbacks

Note on component location and namespacing:

- Live components must be defined inside the `Live::` module so they can be safely loaded from a string name.
- We recommend placing component classes under `app/live/` (so `Live::Counter` maps to `app/live/counter.rb`).
- Corresponding views should live under `app/views/live/...` (e.g. `app/views/live/counter/component.html.live.erb`).
- When rendering a component from a view, pass the namespaced underscored path, e.g. `live/counter` (which camelizes to `Live::Counter`).

LiveCable uses `ActiveModel::Callbacks` to provide lifecycle callbacks that you can hook into at different stages of a component's lifecycle.

### Available Callbacks

- **`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.

- **`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.

- **`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.

### Registering Callbacks

Use standard ActiveModel callback syntax to register your callbacks:

```ruby
module Live
  class ChatRoom < LiveCable::Component
    reactive :messages, -> { [] }

    after_render :log_render_time

    actions :add_message

    def add_message(params)
      messages << { text: params[:text], timestamp: Time.current }
    end

    private

    def log_render_time
      Rails.logger.info "Rendered at #{Time.current}"
    end
  end
end
```

You can also use callbacks with conditionals:

```ruby
module Live
  class Dashboard < LiveCable::Component
    before_connect :authenticate_user, if: :requires_auth?
    after_render :track_analytics, unless: :development_mode?

    private

    def requires_auth?
      # Your auth logic
    end

    def development_mode?
      Rails.env.development?
    end
  end
end
```

### Callback Execution Order

When a component is subscribed:
1. Component is instantiated
2. `before_connect` callbacks are called
3. Connection is established and stream starts
4. `after_connect` callbacks are called
5. `before_render` callbacks are called
6. Component is rendered and broadcast
7. `after_render` callbacks are called

On subsequent updates (action calls, reactive variable changes):
1. State changes occur
2. `before_render` callbacks are called
3. Component is rendered and broadcast
4. `after_render` callbacks are called

When a component is unsubscribed:
1. `before_disconnect` callbacks are called
2. Connection is cleaned up and streams are stopped
3. `after_disconnect` callbacks are called
4. Component is cleaned up

## Generators

LiveCable includes a Rails generator to quickly scaffold components:

```bash
# Basic component
bin/rails generate live_cable:component counter

# With reactive variables and actions
bin/rails generate live_cable:component counter --reactive count:integer step:integer --actions increment decrement

# Compound component
bin/rails generate live_cable:component wizard --compound

# Namespaced component
bin/rails generate live_cable:component chat/message --reactive body:string
```

Supported reactive variable types: `integer`, `string`, `boolean`, `array`, `hash` (defaults to `nil` if omitted).

## Basic Usage

### 1. Create a Component

```ruby
# app/components/live/counter.rb
module Live
  class Counter < LiveCable::Component
    reactive :count, -> { 0 }

    actions :increment, :decrement

    def increment
      self.count += 1
    end

    def decrement
      self.count -= 1
    end
  end
end
```

### 2. Create a Template

Component templates should start with a root element. LiveCable will automatically add the necessary attributes to wire up the component:

```erb
<%# app/views/live/counter/component.html.live.erb %>
<div>
  <h2>Counter: <%= count %></h2>
  <button live-action="increment">+</button>
  <button live-action="decrement">-</button>
</div>
```

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.

#### Optimized Rendering with `.live.erb`

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:

```erb
<%# app/views/live/counter/component.html.live.erb %>
<div>
  <h2>Counter: <%= count %></h2>
  <button live-action="increment">+</button>
  <button live-action="decrement">-</button>
</div>
```

**Important notes about `.live.erb` files:**
- They should only be used for component templates, not for regular Rails views
- They return a special partial object that tracks static and dynamic parts, not a string
- They're optional—`.html.erb` files, and other templating systems will work, but they will send full template diffs on changes.
- When a `.live.erb` template is first rendered, LiveCable sends the full template and tracks which parts are static
- On subsequent renders, only the changed dynamic parts are sent to the client
- This can significantly reduce bandwidth and improve performance for frequently updating components

**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.

##### How Smart Re-rendering Works

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.

**Template Parsing and Dependency Analysis:**

When a `.live.erb` template is compiled, LiveCable:

1. **Splits the template into parts**: Herb parses the template and identifies static text, Ruby code blocks (`<% ... %>`), and expressions (`<%= ... %>`)

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)

3. **Tracks dependencies**: A `DependencyVisitor` walks the AST to identify:
   - **Reactive variable reads**: Direct references to reactive variables (e.g., `count`, `step`)
   - **Component method calls**: Methods called on the component (e.g., `component.products`, `total_price`)
   - **Local variable dependencies**: Local variables defined in one part and used in another

4. **Builds metadata**: Each dynamic part gets metadata about what it depends on:
   ```ruby
   {
     component_dependencies: [:count, :step],      # Reactive vars this part reads
     component_method_calls: [:products],          # Methods called on component
     local_dependencies: [:user],                  # Locals from previous parts
     defines_locals: [:total]                      # Locals this part defines
   }
   ```

**Method Dependency Expansion:**

For component methods, LiveCable goes deeper by analyzing the method implementation itself:

```ruby
# Component class
def total_price
  items.sum { |item| item.price * item.quantity }
end
```

The `MethodAnalyzer` uses Prism to parse the component's source file and build a dependency graph:
- `total_price` method reads the `items` reactive variable
- When `items` changes, any template part calling `component.total_price` is re-rendered

This analysis is done once and cached, creating a transitive dependency map for all component methods.

**Runtime Re-rendering Decision:**

When a reactive variable changes (e.g., `self.count = 5`), LiveCable:

1. **Receives change notification**: The change tracking system reports which variables changed (e.g., `[:count]`)

2. **Evaluates each template part**: For each dynamic part, the `PartialRenderer` checks:
   ```ruby
   should_skip_part?(changes, component_dependencies, component_method_calls, local_dependencies)
   ```

3. **Expands method dependencies**: If the part calls `component.products`, the method analyzer expands this to find which reactive variables `products` depends on

4. **Makes the skip decision**:
   - **Skip** if none of the part's dependencies changed
   - **Render** if any component dependency, method dependency, or local dependency changed
   - **Always render** on initial render (`:all`) or template switches (`:dynamic`)

5. **Returns selective updates**: Only the parts that need updating are rendered and sent to the client as an array:
   ```ruby
   [nil, nil, "<span>5</span>", nil, "<button>Reset</button>"]
   #  ^    ^   └─ changed      ^    └─ changed
   #  |    └─ skipped          └─ skipped
   #  └─ skipped
   ```

**Example: Counter Template Analysis**

Given this template:
```erb
<div>
  <h2>Counter: <%= count %></h2>
  <button live-action="increment">+ <%= step %></button>
  <button live-action="reset">Reset</button>
  <input name="step" value="<%= step %>" live-reactive>
</div>
```

LiveCable analyzes and tracks:
- `<h2>Counter: <%= count %></h2>` depends on `count`
- `<button>+ <%= step %></button>` depends on `step`
- `<button>Reset</button>` is static (never changes)
- `<input value="<%= step %>">` depends on `step`

When `count` changes: Only the `<h2>` element is re-rendered and sent to the client.

When `step` changes: Both the button label and input value are re-rendered and sent.

The `Reset` button is never re-rendered since it contains no dynamic content.

**Local Variable Tracking:**

Templates can define local variables that are used in later parts:

```erb
<% user = component.current_user %>
<% total = component.calculate_total %>

<div>Welcome <%= user.name %></div>
<div>Total: <%= total %></div>
```

LiveCable tracks:
- First part defines `user` and `total` (always executes to define locals)
- Second part depends on `user` local (re-renders only if `user` local was redefined)
- Third part depends on `total` local (re-renders only if `total` local was redefined)

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.

**Performance Benefits:**

This intelligent analysis provides significant performance improvements:

- **Reduced bandwidth**: Only changed HTML fragments are sent over WebSocket
- **Faster rendering**: Server only executes code for parts that changed
- **No client-side diffing needed**: Client receives exact parts to update
- **Efficient method calls**: Component methods are only called if their dependencies changed
- **Cache-friendly**: Dependency analysis happens once at compile time, not on every render

### 3. Use in Your View

Render components using the `live` helper method:

```erb
<%# Simple usage %>
<%= live('counter', id: 'my-counter') %>

<%# With default values %>
<%= live('counter', id: 'my-counter', count: 10, step: 5) %>
```

The `live` helper automatically:
- Creates component instances with unique IDs
- Wraps the component in proper Stimulus controller attributes
- Passes default values to reactive variables
- Reuses existing component instances when navigating back

If you already have a component instance, use `render` directly:

```erb
<%
  @counter = Live::Counter.new('my-counter')
  @counter.count = 10
%>
<%= render(@counter) %>
```

## Reactive Variables

Reactive variables automatically trigger re-renders when changed. Define them with default values using lambdas:

```ruby
module Live
  class ShoppingCart < LiveCable::Component
    reactive :items, -> { [] }
    reactive :discount_code, -> { nil }, writable: true
    reactive :total, -> { 0.0 }

    actions :add_item, :remove_item, :apply_discount

    def add_item(params)
      items << { id: params[:id], name: params[:name], price: params[:price].to_f }
      calculate_total
    end

    def remove_item(params)
      items.reject! { |item| item[:id] == params[:id] }
      calculate_total
    end

    def apply_discount(params)
      self.discount_code = params[:code]
      calculate_total
    end

    private

    def calculate_total
      subtotal = items.sum { |item| item[:price] }
      discount = discount_code ? apply_discount_rate(subtotal) : 0
      self.total = subtotal - discount
    end

    def apply_discount_rate(subtotal)
      discount_code == "SAVE10" ? subtotal * 0.1 : 0
    end
  end
end
```

## Automatic Change Tracking

LiveCable automatically tracks changes to reactive variables containing Arrays, Hashes, and ActiveRecord models. You can mutate these objects directly without manual re-assignment:

```ruby
module Live
  class TaskManager < LiveCable::Component
    reactive :tasks, -> { [] }
    reactive :settings, -> { {} }
    reactive :project, -> { Project.find_by(id: params[:project_id]) }

    actions :add_task, :update_setting, :update_project_name

    # Arrays - direct mutation triggers re-render
    def add_task(params)
      tasks << { title: params[:title], completed: false }
    end

    # Hashes - direct mutation triggers re-render
    def update_setting(params)
      settings[params[:key]] = params[:value]
    end

    # ActiveRecord - direct mutation triggers re-render
    def update_project_name(params)
      project.name = params[:name]
    end
  end
end
```

### Nested Structures

Change tracking works recursively through nested structures:

```ruby
module Live
  class Organization < LiveCable::Component
    reactive :data, -> { { teams: [{ name: 'Engineering', members: [] }] } }

    actions :add_member

    def add_member(params)
      # Deeply nested mutation - automatically triggers re-render
      data[:teams].first[:members] << params[:name]
    end
  end
end
```

### How It Works

When you store an Array, Hash, or ActiveRecord model in a reactive variable:

1. **Automatic Wrapping**: LiveCable wraps the value in a transparent Delegator
2. **Observer Attachment**: An Observer is attached to track mutations
3. **Change Detection**: When you call mutating methods (`<<`, `[]=`, `update`, etc.), the Observer is notified
4. **Smart Re-rendering**: Only components with changed variables are re-rendered

This means you can write natural Ruby code without worrying about triggering updates:

```ruby
# These all work and trigger updates automatically:
tags << 'ruby'
tags.concat(%w[rails rspec])
settings[:theme] = 'dark'
user.update(name: 'Jane')
```

### Primitives (Strings, Numbers, etc.)

Primitive values (String, Integer, Float, Boolean, Symbol) cannot be mutated in place, so you must reassign them:

```ruby
reactive :count, -> { 0 }
reactive :name, -> { "" }

# ✅ This works (reassignment)
self.count = count + 1
self.name = "John"

# ❌ This won't trigger updates (mutation, but primitives are immutable)
self.count.+(1)
self.name.concat("Doe")
```

For more details on the change tracking architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).

## Shared Variables

Shared variables allow multiple components on the same connection to access the same state. There are two types:

### Shared Reactive Variables

Shared reactive variables trigger re-renders on **all** components that use them:

```ruby
module Live
  class ChatMessage < LiveCable::Component
    reactive :messages, -> { [] }, shared: true
    reactive :username, -> { "Guest" }

    actions :send_message

    def send_message(params)
      messages << { user: username, text: params[:text], time: Time.current }
    end
  end
end
```

When any component updates `messages`, all components using this shared reactive variable will re-render.

### Shared Non-Reactive Variables

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:

```ruby
module Live
  class FilterPanel < LiveCable::Component
    shared :cart_items, -> { [] }  # Access cart but don't re-render on cart changes
    reactive :filter, -> { "all" }

    actions :update_filter

    def update_filter(params)
      self.filter = params[:filter]
      # Can read cart_items.length but changing cart elsewhere won't re-render this
    end
  end
end

module Live
  class CartDisplay < LiveCable::Component
    reactive :cart_items, -> { [] }, shared: true  # Re-renders on cart changes

    actions :add_to_cart

    def add_to_cart(params)
      cart_items << params[:item]
      # CartDisplay re-renders, but FilterPanel does not
    end
  end
end
```

**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.

## Action Whitelisting

For security, explicitly declare which actions can be called from the frontend:

```ruby
module Live
  class Secure < LiveCable::Component
    actions :safe_action, :another_safe_action

    def safe_action
      # This can be called from the frontend
    end

    def another_safe_action(params)
      # This can also be called with parameters
    end

    private

    def internal_method
      # This cannot be called from the frontend
    end
  end
end
```

**Note on `params` argument**: The `params` argument is optional. Action methods only receive `params` if you declare the argument in the method signature:

```ruby
# These are both valid:
def increment
  self.count += 1  # No params needed
end

def add_todo(params)
  todos << params[:text]  # Params are used
end
```

If you don't need parameters from the frontend, simply omit the `params` argument from your method definition.

## Writable Reactive Variables

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`.

To allow a reactive variable to be updated from the client via `live-reactive`, mark it as `writable:`:

```ruby
module Live
  class Counter < LiveCable::Component
    reactive :count, -> { 0 }                       # Server-only, cannot be set from the client
    reactive :step, -> { 1 }, writable: true         # Can be updated via live-reactive inputs

    actions :increment

    def increment
      self.count += step.to_i
    end
  end
end
```

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.

The `writable:` option works with all reactive variable types:

```ruby
reactive :filter, -> { "all" }, writable: true                  # Writable local variable
reactive :search, -> { "" }, shared: true, writable: true       # Writable shared variable
```

### Working with ActionController::Parameters

The `params` argument is an `ActionController::Parameters` instance, which means you can use strong parameters and all the standard Rails parameter handling methods:

```ruby
module Live
  class UserProfile < LiveCable::Component
    reactive :user, ->(component) { User.find(component.defaults[:user_id]) }
    reactive :errors, -> { {} }

    actions :update_profile

    def update_profile(params)
      # Use params.expect (Rails 8+) or params.require/permit for strong parameters
      user_params = params.expect(user: [:name, :email, :bio])

      if user.update(user_params)
        self.errors = {}
      else
        self.errors = user.errors.messages
      end
    end
  end
end
```

You can also use `assign_attributes` if you want to validate before saving:

```ruby
def update_profile(params)
  user_params = params.expect(user: [:name, :email, :bio])

  user.assign_attributes(user_params)

  if user.valid?
    user.save
    self.errors = {}
  else
    self.errors = user.errors.messages
  end
end
```

This works seamlessly with form helpers:

```erb
<form live-form="update_profile">
  <div>
    <label>Name</label>
    <input type="text" name="user[name]" value="<%= user.name %>" />
    <% if errors[:name] %>
      <span class="error"><%= errors[:name].join(", ") %></span>
    <% end %>
  </div>

  <div>
    <label>Email</label>
    <input type="email" name="user[email]" value="<%= user.email %>" />
    <% if errors[:email] %>
      <span class="error"><%= errors[:email].join(", ") %></span>
    <% end %>
  </div>

  <div>
    <label>Bio</label>
    <textarea name="user[bio]"><%= user.bio %></textarea>
  </div>

  <button type="submit">Update Profile</button>
</form>
```

## Custom HTML Attributes

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.

### `live-action`

Triggers a component action when an event occurs.

**Syntax:**
- `live-action="action_name"` - Uses Stimulus default event (click for buttons, submit for forms)
- `live-action="event->action_name"` - Custom event
- `live-action="event1->action1 event2->action2"` - Multiple actions

**Examples:**

```html
<!-- Default event (click) -->
<button live-action="save">Save</button>

<!-- Custom event -->
<button live-action="mouseover->highlight">Hover Me</button>

<!-- Multiple actions -->
<button live-action="click->save focus->track_focus">Save and Track</button>
```

**Transformation:** `live-action="save"` becomes `data-action="live#action_$save"`

### `live-form`

Serializes a form and submits it to a component action.

**Syntax:**
- `live-form="action_name"` - Uses Stimulus default event (submit)
- `live-form="event->action_name"` - Custom event
- `live-form="event1->action1 event2->action2"` - Multiple actions

**Examples:**

```html
<!-- Default event (submit) -->
<form live-form="save">
  <input type="text" name="title">
  <button type="submit">Save</button>
</form>

<!-- On change event -->
<form live-form="change->filter">
  <select name="category">...</select>
</form>

<!-- Multiple actions -->
<form live-form="submit->save change->auto_save">
  <input type="text" name="content">
</form>
```

**Transformation:** `live-form="save"` becomes `data-action="live#form_$save"`

### `live-value-*`

Passes parameters to actions on the same element.

**Syntax:** `live-value-param-name="value"`

**Examples:**

```html
<!-- Single parameter -->
<button live-action="update" live-value-id="123">Update Item</button>

<!-- Multiple parameters -->
<button live-action="create"
        live-value-type="task"
        live-value-priority="high">
  Create Task
</button>
```

**Transformation:** `live-value-id="123"` becomes `data-live-id-param="123"`

### `live-reactive`

Updates a reactive variable when an input changes. The corresponding reactive variable must be declared with `writable: true` in the component.

**Syntax:**
- `live-reactive` - Uses Stimulus default event (input for text fields)
- `live-reactive="event"` - Single event
- `live-reactive="event1 event2"` - Multiple events

**Examples:**

```html
<!-- Default event (input) -->
<input type="text" name="username" value="<%= username %>" live-reactive>

<!-- Specific event -->
<input type="text" name="search" live-reactive="keydown">

<!-- Multiple events -->
<input type="text" name="query" live-reactive="keydown keyup">
```

**Transformation:** `live-reactive` becomes `data-action="live#reactive"`, and `live-reactive="keydown"` becomes `data-action="keydown->live#reactive"`

### `live-debounce`

Adds debouncing to reactive and form updates to reduce network traffic.

**Syntax:** `live-debounce="milliseconds"`

**Examples:**

```html
<!-- Debounced reactive input (300ms delay) -->
<input type="text" name="search" live-reactive live-debounce="300">

<!-- Debounced form submission (1000ms delay) -->
<form live-form="change->filter" live-debounce="1000">
  <select name="category">...</select>
</form>
```

**Transformation:** `live-debounce="300"` becomes `data-live-debounce-param="300"`

### Complete Example

```html
<div>
  <h2>Search Products</h2>

  <!-- Reactive search with debouncing -->
  <input type="text"
         name="query"
         value="<%= query %>"
         live-reactive
         live-debounce="300">

  <!-- Form with multiple actions and parameters -->
  <form live-form="submit->filter change->auto_filter" live-debounce="500">
    <select name="category">
      <option value="all">All</option>
      <option value="electronics">Electronics</option>
    </select>
  </form>

  <!-- Action buttons with parameters -->
  <button live-action="add_to_cart"
          live-value-product-id="<%= product.id %>"
          live-value-quantity="1">
    Add to Cart
  </button>

  <!-- Multiple events -->
  <button live-action="click->save mouseover->preview">
    Save & Preview
  </button>
</div>
```

### Race Condition Handling

When a form action is triggered, the controller manages potential race conditions with pending reactive updates:

1.  **Priority**: Any pending debounced `reactive` message is sent **immediately before** the form action message in the same payload.
2.  **Order**: This guarantees that the server applies the reactive update first, then the form action.
3.  **Debounce Cancellation**: Any pending debounced form or reactive submissions are canceled, ensuring only the latest state is processed.

This mechanism prevents scenarios where a delayed reactive update (e.g., from typing quickly) could arrive after a form
submission and overwrite the changes made by the form action.

## DOM Control Attributes

LiveCable supports special HTML attributes to control how the DOM is updated during morphing.

### `live-ignore`

When `live-ignore` is present on an element, LiveCable (via morphdom) will skip updating that element's children during a re-render.

-   **Usage**: `<div live-ignore>...</div>`
-   **Behavior**: Prevents the element's content from being modified by server updates.
-   **Default**: Live components automatically have this attribute to ensure the parent component doesn't overwrite the child component's state.

### `live-key`

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).

-   **Usage**: `<div live-key="unique_id">...</div>`
-   **Behavior**: Matches elements across renders to maintain identity.
-   **Notes**:
    -   The key must be unique within the context of the parent element.
    -   `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.
    -   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.

**Example:**

```erb
<% todos.each do |todo| %>
  <li live-key="<%= todo.id %>">
    ...
  </li>
<% end %>
```

## Compound Components

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`.

```ruby
module Live
  class Checkout < LiveCable::Component
    compound
    # Component will look for templates in app/views/live/checkout/
  end
end
```

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`.

### Dynamic Templates with `variant`

Override the `variant` method to dynamically switch between different templates:

```ruby
module Live
  class Wizard < LiveCable::Component
    compound
    reactive :current_step, -> { "account" }
    reactive :form_data, -> { {} }

    actions :next_step, :previous_step

    def variant
      current_step  # Renders app/views/live/wizard/account.html.live.erb, etc.
    end

    def next_step(params)
      form_data.merge!(params)
      self.current_step = case current_step
        when "account" then "billing"
        when "billing" then "confirmation"
        else "complete"
      end
    end

    def previous_step
      self.current_step = case current_step
        when "billing" then "account"
        when "confirmation" then "billing"
        else current_step
      end
    end
  end
end
```

This creates a multi-step wizard with templates in:
- `app/views/live/wizard/account.html.live.erb`
- `app/views/live/wizard/billing.html.live.erb`
- `app/views/live/wizard/confirmation.html.live.erb`
- `app/views/live/wizard/complete.html.live.erb`

## Using Component Methods for Memory Efficiency

You can call component methods instead of storing large datasets in reactive variables.

**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.

**Best practice:** Use reactive variables for state (like page numbers, filters), but call methods to fetch data on-demand during rendering:

```ruby
module Live
  class ProductList < LiveCable::Component
    reactive :page, -> { 0 }
    reactive :category, -> { "all" }

    actions :next_page, :prev_page, :change_category

    def products
      # Fetched fresh on each render, not stored in memory
      Product.where(category_filter)
             .offset(page * 20)
             .limit(20)
    end

    def next_page
      self.page += 1
    end

    def prev_page
      self.page = [page - 1, 0].max
    end

    def change_category(params)
      self.category = params[:category]
      self.page = 0
    end

    private

    def category_filter
      category == "all" ? {} : { category: category }
    end
  end
end
```

### In `.live.erb` Templates

`.live.erb` templates automatically forward method calls to your component through `method_missing`, so you can call component methods and reactive variables directly:

```erb
<%# app/views/live/product_list/component.html.live.erb %>
<div class="products">
  <% products.each do |product| %>
    <div class="product">
      <h3><%= product.name %></h3>
      <p><%= product.price %></p>
    </div>
  <% end %>
</div>

<div class="pagination">
  <button live-action="prev_page">Previous</button>
  <span>Page <%= page + 1 %></span>
  <button live-action="next_page">Next</button>
</div>
```

### In Regular `.erb` or Other Templating Languages

If you're using regular `.erb` files or other templating languages, you must use the `component` local to access component methods and reactive variables:

```erb
<%# app/views/live/product_list/component.html.erb %>
<div class="products">
  <% component.products.each do |product| %>
    <div class="product">
      <h3><%= product.name %></h3>
      <p><%= product.price %></p>
    </div>
  <% end %>
</div>

<div class="pagination">
  <button live-action="prev_page">Previous</button>
  <span>Page <%= component.page + 1 %></span>
  <button live-action="next_page">Next</button>
</div>
```

This approach:
- Keeps only `page` and `category` in memory (lightweight)
- Fetches the 20 products fresh on each render
- Prevents memory bloat when dealing with large datasets
- Still provides reactive updates when `page` or `category` changes

## Streaming from ActionCable Channels

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.

### Basic Usage

Call `stream_from` in the `after_connect` lifecycle callback to subscribe to a channel:

```ruby
module Live
  module Chat
    class ChatRoom < LiveCable::Component
      reactive :messages, -> { [] }, shared: true

      after_connect :subscribe_to_chat

      private

      def subscribe_to_chat
        stream_from("chat_messages", coder: ActiveSupport::JSON) do |data|
          messages << data
        end
      end
    end
  end
end
```

### Broadcasting to Streams

Any part of your application can broadcast to the stream using ActionCable's broadcast API:

```ruby
module Live
  module Chat
    class ChatInput < LiveCable::Component
      reactive :message
      actions :send_message

      def send_message(params)
        return if params[:message].blank?

        message_data = {
          id: SecureRandom.uuid,
          text: params[:message],
          timestamp: Time.now.to_i,
          user: current_user.as_json(only: [:id, :first_name, :last_name])
        }

        # Broadcast to the chat stream
        ActionCable.server.broadcast("chat_messages", message_data)

        # Clear the input
        self.message = ""
      end
    end
  end
end
```

### How It Works

When a broadcast is received:

1. The stream callback is executed with the broadcast payload
2. You can update reactive variables inside the callback
3. LiveCable automatically detects the changes and broadcasts updates to all affected components
4. All components sharing the same reactive variables are re-rendered

### Key Features

- **Automatic re-rendering**: Changes to reactive variables inside stream callbacks trigger re-renders
- **Shared state**: Combine with `shared: true` reactive variables to sync state across multiple component instances
- **Connection-scoped**: Each user's component instances receive broadcasts independently
- **Coder support**: Use `coder: ActiveSupport::JSON` to automatically decode JSON payloads

### Use Cases

- **Chat applications**: Real-time message updates across all participants
- **Live notifications**: Push notifications to specific users or groups
- **Collaborative editing**: Sync changes across multiple users viewing the same document
- **Live dashboards**: Update metrics and charts in real-time
- **Presence tracking**: Show who's currently online or viewing a resource

## Error Handling

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.

### Default Behaviour

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:

```
MyComponent - RuntimeError: something went wrong
  app/live/my_component.rb:12:in 'do_something'
  ...
```

In production (`verbose_errors` defaults to `false`), a generic message is shown with no internal details:

```
An error occurred
```

### Configuration

Override the default in an initializer:

```ruby
# config/initializers/live_cable.rb
LiveCable.configure do |config|
  config.verbose_errors = false  # never show details
  # or
  config.verbose_errors = true   # always show details (not recommended in production)
end
```

## License

This project is available as open source under the terms of the MIT License.