Components
A Conduit component is a Python class with public state, actions, and a render() method. That’s the unit of interactivity.
Public state
Section titled “Public state”Class attributes that aren’t methods (and don’t start with _) become public properties. They’re snapshotted into the page, checksummed, and syncable from the browser via wire:model / $wire.
from almasix.conduit import Component
class Search(Component): query = "" page = 1 query_string = ["query", "page"] # keep URL in syncquery_string mirrors listed properties onto the URL query string — handy for shareable search/filter UIs.
Locked properties
Section titled “Locked properties”Some state should travel with the snapshot but never be writable from the client (think user_id). Mark it locked:
from almasix.conduit import Component, Locked
class Profile(Component): user_id = Locked(0) # client cannot $set this name = ""
def mount(self, user_id: int = 0) -> None: self.user_id = user_idAttempts to update a locked property from the wire raise — the server stays honest.
Derived values
Section titled “Derived values”Keep writable public state small. Derive the rest in methods and read them from the view via this (Conduit passes the component instance into the render context):
from almasix.conduit import Component
class Cart(Component): items: list[dict] = []
def total(self) -> float: return sum(float(i.get("price", 0)) for i in self.items)<p>Total: {{ this.total() }}</p>There’s also a @Computed helper in almasix.conduit for marking derived methods — handy when you want that intent visible in the class body.
Lifecycle
Section titled “Lifecycle”| Hook | When |
|---|---|
mount(**kwargs) |
First create — set up from route/embed params |
hydrate() |
After snapshot restore on a subsequent request |
booted() |
After mount or hydrate, before render |
dehydrate() |
Before the snapshot is written for the client |
rendering() |
Immediately before render() |
rendered(html) |
After HTML is produced — return possibly modified HTML |
class Dashboard(Component): user_id = 0 greeting = ""
def mount(self, user_id: int = 0) -> None: self.user_id = user_id self.greeting = f"Hey, #{user_id}"
def booted(self) -> None: # Runs on first load and every wire request ...Actions
Section titled “Actions”Any public method can be an action. Call it from HTML or Alpine:
class Todos(Component): title = "" items: list[str] = []
def add(self) -> None: text = self.title.strip() if not text: self.add_error("title", "Give the todo a name.") return self.items = [*self.items, text] self.title = ""
def remove(self, index: int) -> None: self.items = [item for i, item in enumerate(self.items) if i != index]
def quiet_ping(self) -> None: self.notify_elsewhere() self.renderless() # skip HTML morph — state/events only<form wire:submit="add"> <input wire:model="title" placeholder="New todo"> <button type="submit">Add</button></form>
<ul> @foreach(items as item) <li wire:key="{{ loop.index0 }}"> {{ item }} <button type="button" wire:click="remove({{ loop.index0 }})">×</button> </li> @endforeach</ul>From Alpine: $wire.add(), $wire.remove(0), $wire.title = 'Buy milk'.
Events
Section titled “Events”Broadcast browser events from the server after an action:
def save(self) -> None: self.validate() self.dispatch("saved", id=self.record_id)Conduit turns that into a CustomEvent on window. Alpine (or plain JS) can listen:
<div x-data @saved.window="console.log($event.detail)"> ...</div>Helpers related to targeting: dispatch_self, dispatch_to. Run a snippet of JS after the response with self.js("$wire.$el.querySelector('input')?.focus()") — great for focus and scroll.
Toggle helper
Section titled “Toggle helper”self.toggle("open") # flips a boolean public property# or from the client: $wire.$toggle('open')Validation
Section titled “Validation”Point rules at a Pydantic model (or pass rules into validate()):
from pydantic import BaseModel, Fieldfrom almasix.conduit import Component
class Rules(BaseModel): email: str = Field(min_length=3) name: str = Field(min_length=1)
class Signup(Component): email = "" name = "" rules = Rules
def save(self) -> None: self.validate() # persist…<form wire:submit="save"> <input type="email" wire:model="email"> @if(errors.get('email')) <p class="error">{{ errors['email'][0] }}</p> @endif
<input wire:model="name"> @if(errors.get('name')) <p class="error">{{ errors['name'][0] }}</p> @endif
<button type="submit">Sign up</button></form>Errors land in component.errors, the view’s errors, Alpine’s $errors, and effects.errors. Use add_error / reset_error_bag for handmade messages.
Nesting
Section titled “Nesting”Components compose. Embed children from Prism or Python:
<section> <h1>Settings</h1> <conduit:forms.profile :user_id="user_id" /> <conduit:forms.password /> <!-- or @conduit('forms.profile', user_id=user_id) --></section>from almasix.conduit import conduit
html = conduit("forms.profile", user_id=user.id)Registry names are dotted (forms.profile → app.conduit.forms.profile.Profile by convention). Register explicitly when you prefer:
from almasix.conduit import Conduitfrom app.conduit.counter import Counter
Conduit.register("counter", Counter)Lazy / defer
Section titled “Lazy / defer”Heavy widgets don’t need to block first paint:
class Analytics(Component): lazy = True # load when scrolled into view # defer = True # load on next frame after paint lazy_placeholder = "conduit.placeholders.spinner"Pagination
Section titled “Pagination”from almasix.conduit import Component, WithPagination
class UsersTable(Component, WithPagination): per_page = 20
def render(self) -> str: users = self.paginate(User.query().order_by("name")) # pass users into the view via render context / properties as you prefer return "conduit.users-table"page syncs like any other public property — add next_page / prev_page actions (or $wire.page = n from Alpine) and re-render the table.
Inline HTML render
Section titled “Inline HTML render”Prefer a view file for anything real, but tiny demos can return HTML from render() if your setup supports it — keep the component self-contained while you sketch. See Full-page components for using that with Route.conduit.
Full-page components
Section titled “Full-page components”When the route is the component (dashboards, settings, resource screens), use Route.conduit(...).
That’s a major Conduit pattern — layouts, route params → public state, query strings, nesting, islands — covered in full here:
Next: the Directives vocabulary, or Alpine and islands for $wire deep-dives.