Skip to content

declui — a model becomes a UI

declui is situ's declarative layer: a typed Python model plus a small Screen declaration generate a working form, list, master-detail, or server-backed tracker. It revives the metadata-driven UI idea (AribaWeb/MetaUI, Naked Objects, admin generators) with one inversion that changes its nature: everything resolves once, at mount time — there is no runtime rule engine.

flowchart LR
    MODEL["typed model + Screen<br/>(dataclass · attrs · msgspec ·<br/>pydantic · SQLAlchemy)"]
    MODEL --> GEN["declui generators<br/>form · list · master-detail · tracker"]
    GEN --> SRC["ordinary situ source<br/>(.html + .py — readable, ejectable)"]
    SRC --> COMP["the unmodified situ compiler"]
    COMP --> ISL["one island + derived routes"]

The generated artifact is an ordinary situ component — indistinguishable from hand-written source, riding the same compiler, the same seam, and the same wire. Nothing declui-specific ships to the browser.

The magic form

The canonical demo: a bare typed model, zero template, a full edit form.

class Shirt(str, Enum):
    small = "Small"; medium = "Medium"; large = "Large"; extra_large = "Extra large"

@dataclass
class Sample:
    title: str
    shirt: Annotated[Shirt, Field()] = Shirt.medium          # Enum    → <select>
    price: Annotated[Decimal, Field()] = Decimal("10.50")    # Decimal → decimal input
    quantity: Annotated[int, Field()] = 1                    # int     → number input
    expedite: Annotated[bool, Field(label="Expedite shipping")] = False
    need_by: Annotated[date | None, Field()] = None          # date    → date input
    description: Annotated[str, Field(widget="RichText")] = ""


SAMPLE = Screen(model=Sample)        # no zones, no rules — introspection is the whole form

app = Litestar(route_handlers=[
    mount_model(path="/sample", screen=SAMPLE),
    ...
])

The magic form: every widget picked by the field's type

Each field's type picks its widget and its signal; every control two-way-binds to a generated Local; unmapped types fail closed with an error naming the type. The full mapping: Fields & models.

mount_model is declui's Litestar mount. The generation itself (generate_form / generate_list / generate_server_tracker) is framework-neutral — it produces an ordinary situ component, which you can also serve on Flask via situ.mount.flask.mount_flask (Serving on Flask).

From form to application

The same model vocabulary scales up through the screen modes:

  • list — seeded rows in a client :each;
  • master-detail — a list plus a detail pane, selection entirely client-side;
  • server tracker — live server rows, @action command buttons (gated, message-bearing, optionally bulk), client search, a relationship-walking detail pane.

The capstone example generates a whole issue tracker — list, detail with an owner reference and a notes one-to-many, Close/Reopen/Assign actions gated by status — from one Issue model and an 8-line Screen. The equivalent hand-built demo carries 311 lines of components and templates.

The declui capstone: a server master-detail tracker generated from one model

What is automatic, what is yours

The generator decides: widget per type, signal site and type per field, defaults and seeding, reference option labels, the client/server split of actions (by await), the editing mode machinery when a predicate needs it, and all the fail-closed validation.

You declare: per-field intent (Field(...) — labels, widgets, predicates), layout (zones), actions, and the mount wiring (rows/choices seeding or the service/facade/context triple).

The escape hatch, stated up front

declui generates a first draft in a canonical style; regenerating a hand-tuned screen is out of scope by design. When a screen outgrows the generated surface, eject: the generated .html + .py are real source you can write to disk, own, and mount like any hand-written component (mount_component(..., overrides=...) keeps generated siblings around it). An app that ejects every screen still got its first drafts, its type→widget defaults, and its seam wiring for free.