Skip to content

Fields & models

Declaring intent: Field(...)

Field is a transparent Annotated carrier — the same discipline as situ's site markers, so title: Annotated[str, Field(label="Title")] is still str to every type checker.

from situ.declui import Field

@dataclass
class Post:
    title:   Annotated[str,  Field(label="Title", label_field=True)]
    user:    Annotated[str,  Field(editable="editing and value != 'admin'")]
    comment: Annotated[str,  Field(widget="RichText", visible="editing or not is_private")]
    token:   Annotated[str,  Field(hidden=True)]
Option Effect
label= the caption (default: the field name, capitalized)
label_field=True this field is the object's display label — used by references, lists, one-to-many rows
hidden=True never rendered (primary keys, internal flags)
secret=True a password input
widget= override the type's default widget — "RichText", "Popup", "Combobox"
search=True on a server tracker, generate a client search box filtering rows on this str field — zero network
visible= / editable= / valid= predicate strings

Reserved options

required=, filter=, link=, and in_= are accepted so models type-check and currently have no effect — they are reserved for planned slices. In particular required=True performs no validation today (a declui form has no <form>/submit step to enforce it on).

Type → widget

The DRY floor: a bare typed field gets a working control. Anything unmapped is a CompileError naming the type — silent degradation to a text box would hide the problem.

Model type Generated signal Control
str Local[str] <input type="text"> (type="password" if secret)
int Local[int] <input type="number"> — truly numeric in predicates
float Local[float] <input type="number" step="any">
Decimal Local[str] <input inputmode="decimal">stays a string: a JS float would corrupt money
bool Local[bool] a checkbox
date (or date \| None) Local[str] <input type="date"> (ISO string — compares correctly, including against today())
Enum Local[str] <select> over the members, value = each member's .value
another model class Local[str] a reference: <select> over choices, option value = the target's key, label = its label_field
list[SubModel] Local[list] a one-to-many: a read-only nested list of the children's labels

Nullable references get a — none — option; required ones default to the first choice.

Widget overrides

  • widget="RichText" → a <textarea>.
  • widget="Popup" → the native <select> (an alias, for Enum or reference fields — MetaUI's vocabulary).
  • widget="Combobox" → the kit's searchable single-select over the same signal. declui auto-injects the kit runtime (widgets.js + ui.css) into the page head only when a screen uses a kit widget.

Model sources

read_model(cls) reads five model families through one interface — the generated component is byte-identical whichever you use:

@dataclass
class Product:
    name: Annotated[str, Field(label_field=True)]
    price: Annotated[Decimal, Field()] = Decimal("0")

Plain stdlib — no extra dependency.

class Product(msgspec.Struct):          # or @attrs.define, or pydantic.BaseModel
    name: Annotated[str, Field(label_field=True)]
    price: Annotated[Decimal, Field()] = Decimal("0")

Same Annotated[T, Field(...)] convention; install situ[model-adapters]. Each library is imported only if your model actually uses it.

class Product(Base):
    __tablename__ = "product"
    id: Mapped[int] = mapped_column(primary_key=True, info={"declui": Field(hidden=True)})
    name: Mapped[str] = mapped_column(info={"declui": Field(label_field=True)})
    price: Mapped[Decimal]

A structural adapter: mapped columns are read from the mapper (col.type.python_type → the Python type), and declui intent rides mapped_column(info={"declui": Field(...)}). Generating a form performs no query. relationship() attributes are not columns and are out of scope for this adapter; a MappedAsDataclass model is detected as SQLAlchemy first. Known edge: a native-string sa.Enum("a", "b") (no Python Enum class) renders as a text input.

Defaults and seeding

Field defaults become the generated signals' initial values, coerced to the wire: an Enum default → its .value, Decimal → its string form, date | None = None"", a default_factory is resolved where it yields a static literal (an attrs Factory(takes_self=True) degrades gracefully to no default). The same coercions apply to seeded rows.