Predicates¶
Conditional presentation — this field is editable only while…, visible unless…, valid when… — is the spine of model-driven UI. In declui a predicate is a string in a small, whitelisted expression grammar, written on the field that owns it. Where you declare it decides where it runs.
The vocabulary¶
A predicate may reference:
| Name | Meaning |
|---|---|
value |
this field's own value |
| any sibling field name | the form owns every field as a signal, so cross-field conditions are ordinary |
editing |
the view/edit mode flag (see below) |
len min max abs round today |
the whitelisted function set |
That's all. The compiler parses the string, walks the AST against a node whitelist (boolean ops, comparisons, not, whitelisted calls, names, literals — nothing else), validates every identifier, and fails closed on anything outside the grammar. Two independent adversarial security reviews of this compiler found zero holes; model-supplied text (labels, options, messages) is additionally entity-encoded against template injection.
Where each predicate lands¶
| Declared on | Compiles to | Runs |
|---|---|---|
Field(visible="…") |
a :show binder |
in the browser, re-evaluated on any dependency change |
Field(editable="…") |
:attr="disabled: not (…)" |
in the browser |
Field(valid="…") |
an error message :show, gated on the field being non-empty |
in the browser |
Screen(object_editable="…") |
AND-ed into every field's disabled binder | in the browser |
@action(visible="…") |
a Jinja {% if %} around the row's button |
on the server, per row at render |
Field predicates are client binders — the cross-field reactivity that classic metadata-driven frameworks bought with a server round trip per keystroke is a compiled expression here, zero network. The action gate is the deliberate exception: it governs a server command over server-held rows, so it renders (and is enforced) where the rows live.
Examples¶
@dataclass
class User:
rating: Annotated[int, Field(widget="RatingBar")] = 0
bio: Annotated[str, Field(widget="RichText",
editable="rating > 50")] # cross-field
password: Annotated[str, Field(secret=True,
valid="len(value) > 5 or 'Too short'")]
suspended: Annotated[bool, Field()] = False
USER = Screen(model=User, object_editable="not suspended")
- Drag
ratingpast 50 andbiounlocks, live. - Tick
suspendedand every field locks — one screen-wide rule. The field a whole-object rule reads is automatically exempted (otherwise you could never un-suspend). valid=uses the truthy-or-message idiom: the expression is valid when truthy; the string afteroris the error message shown. An empty, untouched field never nags — the error is gated onvalue != ''(declui has no required-validation, so empty is valid by definition;validis a format check).
The editing mode¶
If any predicate references editing, the generated form gains an editing: Local[bool] = True signal and an Edit/Done toggle button. visible="editing or value != ''" reproduces the classic "hide the empty field in view mode" pattern.
Known limits (each fails closed)¶
- Relational comparison (
</>) on aDecimalfield is rejected — Decimals are string-valued signals and the compare would be lexical. Equality is fine. - An
@action(visible=…)gate may not call functions, and may not referencedate/datetimefields (the server row holds a date object, while the client grammar assumes an ISO string). - A predicate referencing a field not rendered on that screen errors, naming the field.