Skip to content

Forms

wybthon.forms

forms

Form state, validation helpers, and accessibility attribute utilities.

This module gives you a small but complete toolkit for building controlled form components on top of Wybthon's reactive primitives:

Example
form = form_state({"name": "", "email": ""})
rules = {"name": [required()], "email": [required(), email()]}

def save(fields):
    print({k: f.value() for k, f in fields.items()})

form(
    input_(**bind_text(form["name"], validators=rules["name"])),
    span(form["name"].error, **error_message_attrs(id="name-error")),
    button("Save", type="submit"),
    on_submit=on_submit_validated(rules, save, form),
)
See Also

Type Aliases:

Name Description
AsyncValidator

A validator takes a value and returns an error message, or None when valid.

Classes:

Name Description
Field

Reactive state for one form field: value, error, and touched.

FormState

Fields plus aggregate dirty, validating, and submission state.

Functions:

Name Description
required

Validate that a value is present and non-empty.

min_length

Validate that the stringified value length is at least n.

max_length

Validate that the stringified value length is at most n.

email

Validate a basic email address format with a lightweight regex.

validate

Return the first validation error, or None when all validators pass.

form_state

Create fields and aggregate form state from initial values.

bind_text

Bind a text input to a field with validation on every input event.

bind_checkbox

Bind a checkbox input to a boolean field (checked + on_change).

bind_select

Bind a <select> element to a field (value + on_change).

bind_number

Bind a numeric input, preserving an empty value as None.

bind_multiselect

Bind all selected option values without per-option bridge reads.

on_submit

Create a submit handler that prevents default and forwards to handler.

validate_field

Validate a single field and update its error and touched signals.

validate_form

Validate every field in a form against a rules map.

on_submit_validated

Submit handler that validates the whole form before calling handler.

rules_from_schema

Build a validators map from a small declarative schema.

a11y_control_attrs

Reactive ARIA attributes for a control bound to a field.

error_message_attrs

Attributes for an accessible error-message container (a polite live region).

AsyncValidator

AsyncValidator = Callable[[Any], str | None | Awaitable[str | None]]

A validator takes a value and returns an error message, or None when valid.

Field

Field(initial: T)

Reactive state for one form field: value, error, and touched.

Attributes:

Name Type Description
value Signal[T]

Accessor for the current input value.

set_value T

Setter for the value.

error Accessor[str | None]

Accessor for the latest validation error, or None.

set_error Setter[str | None]

Setter for the error.

touched Accessor[bool]

Accessor that's True once the user has interacted with the field, so error display can wait until they have.

set_touched Setter[bool]

Setter for the touched flag.

Methods:

Name Description
validate

Validate the current value, mark the field touched, and store the error.

set_value

Stage a value and invalidate any validation for an earlier edit.

reset

Reset value, errors, and interaction state; optionally set a new baseline.

validate_async

Validate the latest edit; stale responses never overwrite newer state.

validate

validate(validators: list[Validator]) -> str | None

Validate the current value, mark the field touched, and store the error.

set_value

set_value(value: T | Callable[[T], T]) -> T

Stage a value and invalidate any validation for an earlier edit.

reset

reset(value: Any = _MISSING) -> None

Reset value, errors, and interaction state; optionally set a new baseline.

validate_async async

validate_async(validators: list[AsyncValidator]) -> str | None

Validate the latest edit; stale responses never overwrite newer state.

FormState

FormState(initial: Mapping[str, Any])

Bases: dict[str, Field[Any]]

Fields plus aggregate dirty, validating, and submission state.

Methods:

Name Description
data

Read the current values as a detached mapping.

reset

Reset all fields, optionally replacing their initial values.

submit

Validate and submit a value snapshot, exposing pending and failure state.

data

data() -> dict[str, Any]

Read the current values as a detached mapping.

reset

reset(values: Mapping[str, Any] | None = None) -> None

Reset all fields, optionally replacing their initial values.

submit async

submit(handler: Callable[[dict[str, Any]], Any], *, rules: Mapping[str, list[AsyncValidator]] | None = None) -> Any

Validate and submit a value snapshot, exposing pending and failure state.

required

required(message: str = 'This field is required') -> Validator

Validate that a value is present and non-empty.

Returns a validator that returns message for None or blank strings (after strip()), otherwise None.

min_length

min_length(n: int, message: str | None = None) -> Validator

Validate that the stringified value length is at least n.

max_length

max_length(n: int, message: str | None = None) -> Validator

Validate that the stringified value length is at most n.

email

email(message: str = 'Invalid email address') -> Validator

Validate a basic email address format with a lightweight regex.

Accepts None and empty strings as valid so it can be combined with required, which handles the missing case explicitly.

validate

validate(value: Any, validators: list[Validator]) -> str | None

Return the first validation error, or None when all validators pass.

form_state

form_state(initial: Mapping[str, Any]) -> FormState

Create fields and aggregate form state from initial values.

bind_text

bind_text(field: Field[Any], *, validators: list[Validator] | None = None, parse: Callable[[str], Any] | None = None, format: Callable[[Any], str] | None = None) -> dict[str, Any]

Bind a text input to a field with validation on every input event.

Returns:

Type Description
dict[str, Any]

Props to spread onto a text input (value + on_input). The

dict[str, Any]

value entry is the field's accessor, so programmatic writes

dict[str, Any]

(field.set_value(...)) update the DOM too.

bind_checkbox

bind_checkbox(field: Field[bool]) -> dict[str, Any]

Bind a checkbox input to a boolean field (checked + on_change).

bind_select

bind_select(field: Field[Any]) -> dict[str, Any]

Bind a <select> element to a field (value + on_change).

bind_number

bind_number(field: Field[float | None], *, validators: list[Validator] | None = None) -> dict[str, Any]

Bind a numeric input, preserving an empty value as None.

bind_multiselect

bind_multiselect(field: Field[list[str]]) -> dict[str, Any]

Bind all selected option values without per-option bridge reads.

on_submit

on_submit(handler: Callable[[dict[str, Field[Any]]], Any], form: dict[str, Field[Any]]) -> Callable[[Any], Any]

Create a submit handler that prevents default and forwards to handler.

validate_field

validate_field(field: Field[Any], validators: list[Validator] | None = None) -> str | None

Validate a single field and update its error and touched signals.

validate_form

validate_form(form: dict[str, Field[Any]], rules: dict[str, list[Validator]]) -> tuple[bool, dict[str, str | None]]

Validate every field in a form against a rules map.

Marks each listed field touched and stores its error as a side effect.

Returns:

Type Description
bool

A (is_valid, errors) tuple where errors maps each field

dict[str, str | None]

name to its current error message (or None).

on_submit_validated

on_submit_validated(rules: dict[str, list[Validator]], handler: Callable[[dict[str, Field[Any]]], Any], form: dict[str, Field[Any]]) -> Callable[[Any], Any]

Submit handler that validates the whole form before calling handler.

rules_from_schema

rules_from_schema(schema: dict[str, dict[str, Any]]) -> dict[str, list[Validator]]

Build a validators map from a small declarative schema.

Supported per-field keys:

  • required: bool or str (a string is used as the message).
  • min_length: int, with optional min_length_message.
  • max_length: int, with optional max_length_message.
  • email: bool or str (a string is used as the message).
Example
rules_from_schema({
    "name": {"required": True, "min_length": 2},
    "email": {"email": True},
})

a11y_control_attrs

a11y_control_attrs(field: Field[Any], *, described_by_id: str | None = None) -> dict[str, Any]

Reactive ARIA attributes for a control bound to a field.

  • aria-invalid is "true" while the field has an error.
  • aria-describedby references described_by_id while an error is present, so screen readers announce the message.

Returns:

Type Description
dict[str, Any]

Props to spread onto the control (reactive values).

error_message_attrs

error_message_attrs(*, id: str) -> dict[str, Any]

Attributes for an accessible error-message container (a polite live region).

What's in this module

A small toolkit for controlled forms on top of signals: per-field state, binding helpers that return prop dicts to spread onto inputs, composable validators, submit wrappers, and reactive ARIA attributes. Import it from wybthon.forms; these names aren't re-exported from wybthon.

Name Description
Field Reactive state for one field: value/set_value, error/set_error, touched/set_touched, and .validate(validators).
form_state {name: initial} to a FormState.
FormState A {name: Field} dict with aggregate dirty, validating, submitting, and submit_error state, plus data(), reset(), and await submit(handler, rules=...).
bind_text {"value": accessor, "on_input": handler} for text inputs, validating on every input event.
bind_number Numeric input binding that keeps an empty value as None.
bind_checkbox {"checked": accessor, "on_change": handler} for a boolean field.
bind_select {"value": accessor, "on_change": handler} for <select>.
bind_multiselect Binds every selected option of a <select multiple> to a list field.
on_submit Submit handler that prevents default and calls handler(form).
on_submit_validated Same, but validates the whole form against rules first.
Validator, AsyncValidator Type aliases: (value) -> str | None, and the same returning an awaitable.
required, min_length, max_length, email Validator factories with optional custom messages.
validate, validate_field, validate_form Run validators on a value, a field, or a whole form.
rules_from_schema Build a rules map from {"name": {"required": True, "min_length": 2}, ...}.
a11y_control_attrs Reactive aria_invalid and aria_describedby for a control.
error_message_attrs id, role="alert", and aria_live="polite" for the message container.
from wybthon import button, component, form, input_, label, span
from wybthon.forms import (
    a11y_control_attrs,
    bind_checkbox,
    bind_text,
    email,
    error_message_attrs,
    form_state,
    on_submit_validated,
    required,
)


@component
def Signup():
    fields = form_state({"name": "", "email": "", "agree": False})
    rules = {"name": [required()], "email": [required(), email()]}

    def save(f):
        print({k: field.value.peek() for k, field in f.items()})

    return form(
        label("Name", html_for="name"),
        input_(
            id="name",
            **bind_text(fields["name"], validators=rules["name"]),
            **a11y_control_attrs(fields["name"], described_by_id="name-error"),
        ),
        span(fields["name"].error, **error_message_attrs(id="name-error")),
        label(input_(type="checkbox", **bind_checkbox(fields["agree"])), " I agree"),
        button("Save", type="submit"),
        on_submit=on_submit_validated(rules, save, fields),
    )
  • A field's error accessor is None while valid, so embedding it as a child renders nothing until there's a message.
  • touched becomes True on the first input, so you can delay showing errors: lambda: fields["name"].error() if fields["name"].touched() else None.
  • email() accepts empty values; combine it with required().
  • In CPython tests, drive a form with wybthon.testing: fire.input(screen.get_by_label_text("Name"), "Ada"), then fire.submit(form_node).

See also