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.