Skip to content

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 rating past 50 and bio unlocks, live.
  • Tick suspended and 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 after or is the error message shown. An empty, untouched field never nags — the error is gated on value != '' (declui has no required-validation, so empty is valid by definition; valid is 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 a Decimal field 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 reference date/datetime fields (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.