Actions¶
@action marks a model method as a UI button. One decorator, two transports — decided, as everywhere in situ, by the method's own effect shape: a sync body compiles to a client handler; an async body becomes a server command.
Client actions (sync, on forms)¶
@dataclass
class CounterForm:
count: Annotated[int, Field()] = 0
@action
def increment(self):
self.count += 1
@action
def clear(self):
self.count = 0
The method body is compiled: self.count becomes the generated count signal, and the result goes through the same dialect as a hand-written situ handler — the form's fields are the Locals. Several rules guard the translation, each failing closed:
- an
asyncaction on a form (no server seam there), - a
self.xthat isn't a rendered field, - a local variable that would shadow a field name (it would silently write the signal),
- a non-
(self)signature, a reserved name (reset,value,editing), visible=/message=on a form action (those are tracker vocabulary),
…are each a CompileError naming the fix.
Server actions (async, on the server tracker)¶
@dataclass
class Issue:
id: Annotated[int, Field(hidden=True)]
subject: Annotated[str, Field(label_field=True, search=True)]
status: Status
@action(visible="status != 'Closed'", message="Issue closed")
async def close(self): ...
@action(visible="status == 'Closed'", message="Issue reopened")
async def reopen(self): ...
Each async action becomes a per-row button that POSTs /cmd/<name>/<id>; the generated handler awaits the corresponding facade verb (your facade wraps the DI-resolved service), and the region re-renders from fresh server state — the standard command round trip.
Gating: visible=¶
A predicate over the model's fields, compiled to a server-side Jinja gate around the button ({% if t.status.value == 'Open' %}…— enum fields compare by .value, matching the rendered cell). The render is a hint; the command dispatcher independently rejects anything not classified as a server handler, so a hidden action's endpoint isn't an open door.
Feedback: message=¶
A confirmation line ("Issue closed") shown the moment the button is clicked, in a <header> status line that survives the region swap and auto-hides after a few seconds. It rides the optimistic-apply pairing, so a failed command rolls the message back along with everything else. The message text is validated at generation time (no quotes/braces/control characters — it splices into the compiled click).
Bulk actions: bulk=True¶
A bulk action makes the tracker grow a per-row checkbox column and a bulk bar; the handler receives the selected id set (a Local[set] riding the command as a JSON array — your facade verb takes the selection). Selection state lives client-side and the count updates with zero network.
What the capstone exercises¶
The issue_tracker example uses all of it from one model: three status-gated actions with messages, a bulk close, search=True filtering, and the relationship-walking detail pane — see Demos & examples.