LiveComponents
LiveComponents are stateful, reusable UI components that manage their own state and handle their own events. They’re useful when you need multiple independent instances of the same UI pattern, each with isolated state.
Note: LiveComponents require T-String Templates (Python 3.14+). They are not supported with Ibis HTML templates.
When to Use LiveComponents
Use LiveComponents when:
- You need multiple instances of the same UI with independent state (e.g., several counters, toggles, or cards)
- A piece of UI has its own event handlers that shouldn’t clutter the parent LiveView
- You want to encapsulate reusable stateful behavior
Keep it in the parent LiveView when:
- There’s only one instance
- The state is simple and closely tied to the parent
- You don’t need isolation between instances
Defining a Component
Create a component by extending LiveComponent with a context type:
from typing import TypedDictfrom pyview.components import LiveComponent, ComponentSocket, ComponentMeta
class CounterContext(TypedDict): count: int
class Counter(LiveComponent[CounterContext]): async def mount(self, socket: ComponentSocket[CounterContext], assigns: dict): """Initialize component state from parent assigns.""" socket.context = CounterContext(count=assigns.get("initial", 0))
async def handle_event(self, event: str, payload: dict, socket: ComponentSocket[CounterContext]): """Handle events targeted at this component.""" if event == "increment": socket.context["count"] += 1 elif event == "decrement": socket.context["count"] -= 1
def template(self, assigns: CounterContext, meta: ComponentMeta): """Render the component. Use meta.myself for event targeting.""" count = assigns["count"] myself = meta.myself
return t""" <div class="counter"> <button phx-click="decrement" phx-target="{myself}">-</button> <span>{count}</span> <button phx-click="increment" phx-target="{myself}">+</button> </div> """Rendering Components
Use live_component() in your parent template to render a component:
from pyview.template.live_view_template import live_component
class MyLiveView(LiveView[MyContext]): def template(self, assigns, meta): return t""" <div> <h1>My Counters</h1> {live_component(Counter, id="counter-1", initial=0)} {live_component(Counter, id="counter-2", initial=100)} </div> """Important: Every component instance needs a unique id. PyView uses (component_class, id) to identify instances across re-renders.
Component Lifecycle
| Method | When Called | Purpose |
|---|---|---|
mount(socket, assigns) | First time component is rendered | Initialize state from parent assigns |
update(socket, assigns) | Subsequent renders with new assigns | Handle changed props from parent |
handle_event(event, payload, socket) | User interaction with phx-target | Process events targeted at this component |
template(assigns, meta) | Every render | Return component HTML |
mount()
Called once when the component first appears. Initialize your state here:
async def mount(self, socket: ComponentSocket[CounterContext], assigns: dict): socket.context = CounterContext( count=assigns.get("initial", 0), label=assigns.get("label", "Counter") )update()
Called on subsequent renders when the parent passes new assigns. The default implementation does nothing—override it to handle prop changes:
async def update(self, socket: ComponentSocket[CounterContext], assigns: dict): # Update label if parent changed it, but preserve count if "label" in assigns: socket.context["label"] = assigns["label"]handle_event()
Called when a user interacts with an element that has phx-target pointing to this component:
async def handle_event(self, event: str, payload: dict, socket: ComponentSocket[CounterContext]): if event == "increment": socket.context["count"] += 1 elif event == "reset": socket.context["count"] = 0template()
Called every render. Returns the component’s HTML. The meta parameter provides myself for event targeting:
def template(self, assigns: CounterContext, meta: ComponentMeta): return t"<button phx-click='increment' phx-target='{meta.myself}'>{assigns['count']}</button>"Event Targeting with meta.myself
Each component instance gets a unique Component ID (CID). Use meta.myself in phx-target to route events to the component instead of the parent LiveView:
def template(self, assigns, meta): myself = meta.myself # e.g., 1, 2, 3...
return t""" <div> <!-- This event goes to the component's handle_event --> <button phx-click="increment" phx-target="{myself}">+</button>
<!-- Without phx-target, this would go to the parent LiveView --> <button phx-click="save">Save</button> </div> """Parent-Child Communication
Components can send events to their parent LiveView using send_parent():
class Counter(LiveComponent[CounterContext]): async def handle_event(self, event, payload, socket): if event == "increment": socket.context["count"] += 1 elif event == "notify_parent": # Send event to parent LiveView await socket.send_parent("counter_updated", { "count": socket.context["count"] })The parent receives this in its normal handle_event:
class ParentLiveView(LiveView[ParentContext]): async def handle_event(self, event, payload, socket): if event == "counter_updated": count = payload["count"] socket.context["messages"].append(f"Counter is now {count}")Slots
Slots allow parent templates to pass content into components, similar to React’s children or Phoenix LiveView’s slots. This enables flexible, composable components.
Basic Usage
Use the slots() helper to pass content when rendering a component:
from pyview.components import slotsfrom pyview.template.live_view_template import live_component
# Default slot onlylive_component(Card, id="card-1", slots=slots( t"<p>This is the card body content</p>"))
# Named slotslive_component(Card, id="card-2", slots=slots( t"<p>Body content</p>", header=t"<h2>Card Title</h2>", actions=t"<button>Save</button>"))Accessing Slots in Components
Access slots via meta.slots in your component’s template:
class Card(LiveComponent): async def mount(self, socket, assigns): socket.context = {}
def template(self, assigns, meta): # Use .get() for optional slots with fallbacks header = meta.slots.get("header", t"") body = meta.slots.get("default", t"<p>No content</p>") actions = meta.slots.get("actions", t"")
return t""" <div class="card"> <header>{header}</header> <main>{body}</main> <footer>{actions}</footer> </div> """Nested Components in Slots
Slots can contain live components, enabling nested interactivity:
live_component(Card, id="card-with-counter", slots=slots( t""" <p>This card contains a counter:</p> {live_component(Counter, id="nested-counter", initial=10)} """, header=t"<h2>Interactive Card</h2>"))The nested Counter component is fully interactive with its own state and event handling.
Slot Patterns
| Pattern | Example |
|---|---|
| Default slot only | slots(t"<p>Content</p>") |
| Named slots only | slots(header=t"...", footer=t"...") |
| Default + named | slots(t"Body", header=t"Title") |
| Optional with fallback | meta.slots.get("header", t"Default") |
| Check if provided | if "header" in meta.slots: |
Multiple Component Instances
Each component instance has isolated state. Create multiple instances with unique IDs:
def template(self, assigns, meta): # Using a list comprehension counters = [ live_component(Counter, id=f"counter-{i}", initial=i * 10, label=f"Counter {i+1}") for i in range(assigns["counter_count"]) ]
return t""" <div class="counter-grid"> {counters} </div> """Clicking increment on one counter doesn’t affect the others—each maintains its own state.
Complete Example
Here’s a full example with a parent LiveView managing multiple counter components:
from typing import TypedDictfrom pyview import LiveView, LiveViewSocketfrom pyview.components import LiveComponent, ComponentSocket, ComponentMetafrom pyview.template.live_view_template import live_componentfrom pyview.template.template_view import TemplateView
# Component contextclass CounterContext(TypedDict): count: int label: str
# Component definitionclass Counter(LiveComponent[CounterContext]): async def mount(self, socket: ComponentSocket[CounterContext], assigns: dict): socket.context = CounterContext( count=assigns.get("initial", 0), label=assigns.get("label", "Counter") )
async def handle_event(self, event, payload, socket): if event == "increment": socket.context["count"] += 1 elif event == "decrement": socket.context["count"] -= 1 elif event == "notify": await socket.send_parent("counter_changed", { "label": socket.context["label"], "count": socket.context["count"] })
def template(self, assigns: CounterContext, meta: ComponentMeta): count = assigns["count"] label = assigns["label"] myself = meta.myself
return t""" <div class="counter-card"> <h3>{label}</h3> <div class="controls"> <button phx-click="decrement" phx-target="{myself}">-</button> <span>{count}</span> <button phx-click="increment" phx-target="{myself}">+</button> </div> <button phx-click="notify" phx-target="{myself}">Notify Parent</button> </div> """
# Parent LiveView contextclass DemoContext(TypedDict): messages: list[str]
# Parent LiveViewclass CounterDemo(TemplateView, LiveView[DemoContext]): async def mount(self, socket: LiveViewSocket[DemoContext], session): socket.context = DemoContext(messages=[])
async def handle_event(self, event, payload, socket): if event == "counter_changed": label = payload["label"] count = payload["count"] socket.context["messages"].append(f"{label} is now {count}") # Keep only last 5 messages socket.context["messages"] = socket.context["messages"][-5:]
def template(self, assigns: DemoContext, meta): messages = assigns["messages"]
message_items = [ t'<li>{msg}</li>' for msg in messages ] if messages else [t'<li>No messages yet</li>']
return t""" <div> <h1>Counter Components Demo</h1>
<div class="counters"> {live_component(Counter, id="a", label="Alpha", initial=0)} {live_component(Counter, id="b", label="Beta", initial=50)} {live_component(Counter, id="c", label="Gamma", initial=100)} </div>
<div class="messages"> <h2>Messages from Components</h2> <ul>{message_items}</ul> </div> </div> """Related Documentation
- Event Handling - More on event handling patterns
- LiveView Lifecycle - Understanding the render lifecycle
- T-String Templates - Template syntax reference