Forms
Fields, a `ui.form` around them, and the validation on the STATE — not on the view. That last point is what makes the difference between “the form refuses” and “the data cannot be wrong”.The skeleton
`ui.form` is a container: its children are written inside a `with`. `on_submit=` receives the form's values.def register(name: str, email: str) -> None:
Account().create(name=name, email=email)
with ui.form(on_submit=register):
with ui.form_field(label="Name", required=True):
ui.input(name="name")
with ui.form_field(label="Email",
hint="A work address, preferably"):
ui.input(name="email", type="email")
ui.button("Create", type="submit", color="primary")
`ui.form_field` carries the label above, the field in the middle, the hint or the error underneath. It also sets the accessibility links — a screen reader announces the error with the field, without anyone writing it.Validation lives on the STATE
A `@validator` attaches to a typed state field. It runs on EVERY assignment — so from the form as much as from an import, a handler, a script.from bretzel.state import SessionState, field, validator
class Signup(SessionState):
email: str = field(default="")
age: int = field(default=0)
@validator("email")
def _valid_email(self, value: str) -> str:
"""A FIELD validator: it receives the value and
returns the one we keep — so it can normalise
too."""
value = value.strip().lower()
if "@" not in value:
raise ValueError("Invalid address.")
return value
@validator
def _consistent(self) -> None:
"""An INSTANCE validator: it runs after any
mutation, and sees every field."""
if self.age < 18 and self.email.endswith(".pro"):
raise FormError("Business account: 18 minimum.")
| Form | When it runs | What it receives |
|---|---|---|
| @validator("field") | on every assignment to THIS field | `(self, value)` — and returns the value to keep |
| @validator | after any mutation of the instance | `(self)` — it sees every field, and raises to refuse |
Showing the error
`ui.form_field(error=…)` shows the message under the field AND sets `aria-invalid` — the two go together, otherwise the error exists on screen and not for a screen reader.signup = Signup()
with ui.form_field(label="Email",
error=signup.errors.get("email")):
ui.input(name="email", value=signup.email)
Two ways of refusing
`FormError` is the raise meant to TRAVEL BACK to the form — that is what distinguishes it from a bare `ValueError`, which says “this value is impossible” and comes back as a server error.