Compound Components
Compound components allow you to organize complex components with multiple views into a directory structure, and dynamically switch between different templates based on component state.
Basic Compound Components
By default, components render the partial at app/views/live/component_name.html.erb. Mark a component as compound to organize templates in a directory:
module Live
class Checkout < LiveCable::Component
compound
reactive :step, -> { "cart" }
reactive :items, -> { [] }
actions :proceed_to_shipping
def proceed_to_shipping
self.step = "shipping"
end
end
endWith compound, the component looks for templates in app/views/live/checkout/. By default, it renders app/views/live/checkout/component.html.erb.
Dynamic Templates with template_state
Override the template_state method to dynamically switch between different templates:
module Live
class Wizard < LiveCable::Component
compound
reactive :current_step, -> { "account" }
reactive :form_data, -> { {} }
actions :next_step, :previous_step
def template_state
current_step # Renders app/views/live/wizard/{current_step}.html.erb
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
endThis creates a multi-step wizard with templates in:
app/views/live/wizard/account.html.erbapp/views/live/wizard/billing.html.erbapp/views/live/wizard/confirmation.html.erbapp/views/live/wizard/complete.html.erb
Example: Multi-Step Wizard
Component Class
module Live
class RegistrationWizard < LiveCable::Component
compound
reactive :current_step, -> { "personal_info" }
reactive :personal_info, -> { {} }
reactive :account_info, -> { {} }
reactive :preferences, -> { {} }
reactive :errors, -> { {} }
actions :next_step, :previous_step, :submit
def template_state
current_step
end
def next_step(params)
# Validate current step
case current_step
when "personal_info"
if validate_personal_info(params)
personal_info.merge!(params)
self.current_step = "account_info"
self.errors = {}
end
when "account_info"
if validate_account_info(params)
account_info.merge!(params)
self.current_step = "preferences"
self.errors = {}
end
end
end
def previous_step
self.current_step = case current_step
when "account_info" then "personal_info"
when "preferences" then "account_info"
else current_step
end
end
def submit(params)
preferences.merge!(params)
# Create user
user = User.create(
personal_info.merge(account_info).merge(preferences)
)
if user.persisted?
self.current_step = "success"
else
self.errors = user.errors.to_hash
end
end
private
def validate_personal_info(params)
errors = {}
errors[:first_name] = "can't be blank" if params[:first_name].blank?
errors[:last_name] = "can't be blank" if params[:last_name].blank?
errors[:email] = "is invalid" unless params[:email] =~ URI::MailTo::EMAIL_REGEXP
self.errors = errors
errors.empty?
end
def validate_account_info(params)
errors = {}
errors[:username] = "can't be blank" if params[:username].blank?
errors[:password] = "must be at least 8 characters" if params[:password].to_s.length < 8
self.errors = errors
errors.empty?
end
end
endTemplates
Personal Info (app/views/live/registration_wizard/personal_info.html.erb):
<%= live_component(component) do %>
<div class="wizard">
<h2>Personal Information</h2>
<div class="progress">Step 1 of 3</div>
<form data-action="submit->live#form:prevent" data-live-action-param="next_step">
<div>
<label>First Name</label>
<input type="text" name="first_name" value="<%= personal_info[:first_name] %>">
<% if errors[:first_name] %>
<span class="error"><%= errors[:first_name] %></span>
<% end %>
</div>
<div>
<label>Last Name</label>
<input type="text" name="last_name" value="<%= personal_info[:last_name] %>">
<% if errors[:last_name] %>
<span class="error"><%= errors[:last_name] %></span>
<% end %>
</div>
<div>
<label>Email</label>
<input type="email" name="email" value="<%= personal_info[:email] %>">
<% if errors[:email] %>
<span class="error"><%= errors[:email] %></span>
<% end %>
</div>
<button type="submit">Next</button>
</form>
</div>
<% end %>Account Info (app/views/live/registration_wizard/account_info.html.erb):
<%= live_component(component) do %>
<div class="wizard">
<h2>Account Information</h2>
<div class="progress">Step 2 of 3</div>
<form data-action="submit->live#form:prevent" data-live-action-param="next_step">
<div>
<label>Username</label>
<input type="text" name="username" value="<%= account_info[:username] %>">
<% if errors[:username] %>
<span class="error"><%= errors[:username] %></span>
<% end %>
</div>
<div>
<label>Password</label>
<input type="password" name="password">
<% if errors[:password] %>
<span class="error"><%= errors[:password] %></span>
<% end %>
</div>
<div class="actions">
<button type="button" data-action="live#call" data-live-action-param="previous_step">
Back
</button>
<button type="submit">Next</button>
</div>
</form>
</div>
<% end %>Success (app/views/live/registration_wizard/success.html.erb):
<%= live_component(component) do %>
<div class="wizard success">
<h2>Registration Complete!</h2>
<p>Your account has been created successfully.</p>
<a href="/dashboard" class="button">Go to Dashboard</a>
</div>
<% end %>Use Cases
Compound components are ideal for:
- Multi-step forms: Wizards, onboarding flows, checkout processes
- State machines: Components with distinct states (loading, success, error, empty)
- Modal dialogs: Different content based on the modal's purpose
- Tabs: Switch between different content areas
- Dashboard widgets: Different views based on data availability
Generating Compound Components
Use the --compound flag with the generator:
bin/rails generate live_cable:component Wizard --compound current_step:stringThis creates:
app/live/wizard.rbwithcompoundalready setapp/views/live/wizard/component.html.erbas the default template
Best Practices
Do
✅ Use compound components for complex, multi-state components
✅ Keep template names descriptive and reflective of the state
✅ Use template_state to return a simple string or symbol
✅ Organize shared partials in the component's directory
✅ Keep state transitions clear and documented
Don't
❌ Don't use compound components for simple components
❌ Don't make template_state logic complex
❌ Don't forget to create all referenced templates
❌ Don't use compound when a single template with conditionals would suffice