Screens & mounting¶
Screen¶
from situ.declui import Screen, zones
ISSUE = Screen(
model=Issue,
zones={
"list": zones(main=["subject", "status", "priority"]),
"detail": zones(header=["subject", "status"], body=["body", "owner", "notes"]),
"form": zones(main=["subject", "body"]),
},
object_editable="not suspended", # optional whole-object predicate
)
| Parameter | Meaning |
|---|---|
model |
the typed class (any supported source) |
zones |
per-screen layout — see below |
object_editable |
a predicate AND-ed into every field's editability |
rules |
the rule sheet — selector→property rules resolved at mount time (override + AND-chained predicates) |
row_links |
server-tracker per-row navigation links (RowLink(...)), optionally gated by a predicate |
Zones¶
zones(name=[fields...], ...) lays fields into named <div class="declui-zone--name"> slots, in declaration order — an explicit compile-time layout. Each zone renders a role="group" with a visible title. For a list screen, zones["list"] selects and orders the columns.
Zones are explicit
A field placed in no zone of a screen is not rendered on that screen — deliberately, so the layout is the layout. Referencing an unplaced field from a predicate gives a precise error. Unknown field names and non-identifier zone names fail closed.
mount_model and the screen modes¶
A form (the default)¶
A list¶
A client-seeded :each: one read-only cell per non-hidden scalar column (booleans render Yes/No, enums by value; long text cells get hover title tooltips). Zero network after load.
A table¶
The same client-seeded columns rendered as the kit's <DataGrid> instead of a :each list — so the rows get sort (click a header, shift-click for a secondary key), a global filter box, pagination with a page-size selector, a column-visibility picker, and drag-to-resize columns, all client-side and free. Each column's presentation comes from its Field — cell_format= a display formatter, filter=True a per-column filter, a numeric field right-aligns and sorts numerically, a date sorts chronologically. declui emits only the typed <thead>; the runtime builds the body. A bad cell_format is a CompileError.
Master-detail¶
A list plus a detail pane in one mount. A row click copies the row into per-field detail signals and reveals the pane — selection is entirely client-side.
The server tracker¶
mount_model(path="/issues", screen=ISSUE,
service_type=IssueService, # from your Dishka container (mount_model is the Litestar mount)
facade=Issues, # wraps the service for the handlers
context=context) # (service, view) -> the rows the region renders
The full round trip: the region is server-rendered from context() on every command; the model's async @actions become per-row command buttons; zones["detail"] adds a server-rendered detail pane that walks relationships from the object graph — a reference field shows the target's label, a list[SubModel] renders as a nested list; Field(search=True) adds a zero-network client filter over the server-rendered rows; selection survives command swaps.
The server tracker requires an id field on the model (the command key t.id).
mount_model(new_link=("+ New", "/issues/new/")) adds a "New" link to the tracker header — the create affordance a list needs, pointing at the sibling create form. It's a server-tracker option (a write form / pure-client mount has no header for it, and says so).
Write forms — create & edit¶
A server mount whose screens is ("create",) or ("edit",) generates a write form: the magic form wrapped in a <form> whose submit is a server command. This is the write half of CRUD — the C and field-level U — from the same model that drives the read screens.
# create: a blank form; submit creates the record, then navigates back to the list
mount_model(path="/tickets/new", screen=SCR, screens=("create",),
service_type=TicketService, facade=Tickets, after="/tickets")
# edit: an entity path pre-fills the form from the record; submit updates it
mount_model(path="/tickets/{id:int}/edit", screen=SCR, screens=("edit",),
service_type=TicketService, facade=Tickets, context=edit_context, after="/tickets")
The generated submit awaits the facade verb — facade.create(**fields) for create, facade.update(id, **fields) for edit — with the field values riding the POST by name (situ's with channel; no name= attributes). On success it navigates to after= (a same-origin path; omit it to stay on a cleared form).
- Create seeds each control from the field default;
context=is optional (it defaults to empty). - Edit is an entity mount: the
{id:int}path converter names the record,context()loads it, and the controls pre-fill from it. Command and island URLs compile relative, so one island serves any id — the same mechanism the Flask entity mounts use. - The submit gate.
Field(required=True)andvalid=disable the submit button until the form is valid — client-side, zero network. The server re-validates on the facade; the button is a hint. - References submit the target's FK key; the facade receives it.
Fail-closed: an edit mount needs an {id:int}-style path converter (the record key threads the update); a write form rejects the tracker-only kwargs (theme= / page_size= / Screen(row_links=...)).
Seeding client data¶
Two mount kwargs feed the pure-client modes:
rows=[...]— model instances forlist/ master-detail screens (values coerced exactly like defaults).choices={"owner": [user1, user2]}— the option lists for reference fields; option value is the target's key, label itslabel_field.
Fail-closed mounting¶
mount_model refuses ambiguous configurations:
service_type=withoutcontext=→CompileError(there would be dead buttons over an empty region).- A server mount passed the client kwargs
screens=/rows=/choices=→CompileError. - An unsupported
screenstuple →CompileError. - A generated command calling a facade verb the facade lacks — or a verb whose signature can't accept the fields a form submits — →
CompileErrorat mount. That drift used to surface only as a 500 on the first command; now the check runs when you mount, against the facade class (orservice_typefor the identity default). A callable facade factory (a lambda) opts out.
template= and meta= work as on every situ mount; declui ships a default page style (declui.css) and injects the kit assets only when a kit widget is used.