UI Form Controls
First-party form controls as headless/themed pairs: Input, Textarea, Select, Checkbox, Switch, RadioGroup, and the FormField wrapper that composes label, control, and accessible error region over the forms module's Field binding.
UI Form Controls
"""Standalone docs demo: a complete form built from ui form controls."""
from typing import Any
from webcompy.app import WebComPyApp
from webcompy.components import ComponentContext, define_component
from webcompy.elements import html
from webcompy.forms import email, min_length, required, use_field, use_form
from webcompy.signal import use_computed, use_state
from webcompy.ui import Checkbox, FormField, Input, RadioGroup, Select, Switch, Textarea
@define_component("ui-form-controls-demo-app")
def UiFormControlsDemo(_: ComponentContext[None]) -> Any:
name_field = use_field(
use_state(lambda: ""), validators=[required(), min_length(3, "At least 3 characters")], name="name"
)
email_field = use_field(use_state(lambda: ""), validators=[required(), email()], name="email")
country_field = use_field(use_state(lambda: ""), validators=[required()], name="country")
notes_field = use_field(use_state(lambda: ""), name="notes")
updates_field = use_field(use_state(lambda: True), name="updates")
marketing_field = use_field(use_state(lambda: False), name="marketing")
plan_field = use_field(use_state(lambda: "free"), validators=[required()], name="plan")
agree_field = use_field(
use_state(lambda: False), validators=[lambda v: None if v else "Please accept the terms"], name="agree"
)
form = use_form(
name=name_field,
email=email_field,
country=country_field,
plan=plan_field,
agree=agree_field,
)
submitted = use_state(lambda: "")
def _on_submit(values: dict[str, Any]) -> None:
submitted.value = f"Welcome, {values['name']} ({values['plan']} plan)!"
def _on_reset(_ev: Any) -> None:
form.reset()
submitted.value = ""
return html.DIV(
{"class": "ufc-demo"},
html.H1({}, "UI Form Controls Demo"),
html.P({}, "Themed form controls with Field binding, validation on blur, and accessible error wiring."),
html.FORM(
{"@submit": form.submit(_on_submit)},
FormField(
{"field": name_field, "label": "Name"},
slots={"default": lambda: Input({"field": name_field, "placeholder": "Your name"})},
),
FormField(
{"field": email_field, "label": "Email"},
slots={
"default": lambda: Input(
{"field": email_field, "input_type": "email", "placeholder": "[email protected]"}
)
},
),
FormField(
{"field": country_field, "label": "Country"},
slots={
"default": lambda: Select(
{
"field": country_field,
"options": [
{"value": "", "label": "Choose your country…"},
{"value": "jp", "label": "Japan"},
{"value": "us", "label": "United States"},
{"value": "de", "label": "Germany"},
],
}
)
},
),
FormField(
{"field": notes_field, "label": "Notes"},
slots={"default": lambda: Textarea({"field": notes_field, "rows": 3, "placeholder": "Anything else?"})},
),
FormField(
{"field": plan_field},
slots={
"default": lambda: RadioGroup(
{
"field": plan_field,
"legend": "Plan",
"options": [
{"value": "free", "label": "Free"},
{"value": "pro", "label": "Pro"},
{"value": "team", "label": "Team"},
],
}
)
},
),
html.DIV(
{"class": "ufc-demo-toggles"},
FormField(
{"field": agree_field, "label": "I accept the terms"},
slots={"default": lambda: Checkbox({"field": agree_field})},
),
Switch({"field": updates_field, "label": "Product updates"}),
Switch({"field": marketing_field, "label": "Marketing emails"}),
),
html.DIV(
{"class": "ufc-demo-actions"},
html.BUTTON({"type": "submit", "id": "ufc-demo-submit"}, "Submit"),
html.BUTTON({"type": "button", "id": "ufc-demo-reset", "@click": _on_reset}, "Reset"),
),
),
html.P({"id": "ufc-demo-status"}, use_computed(lambda: submitted.value)),
)
UiFormControlsDemo.scoped_style = {
".ufc-demo": {
"font-family": "sans-serif",
"padding": "1rem",
"max-width": "28rem",
},
".ufc-demo form": {
"display": "flex",
"flex-direction": "column",
"gap": "0.75rem",
},
".ufc-demo-toggles": {
"display": "flex",
"flex-wrap": "wrap",
"gap": "1rem",
"align-items": "center",
},
".ufc-demo-actions": {
"display": "flex",
"gap": "0.5rem",
"margin-top": "0.5rem",
},
".ufc-demo-actions button": {
"padding": "0.5rem 1rem",
"cursor": "pointer",
},
"#ufc-demo-status": {
"margin-top": "0.75rem",
"font-weight": "600",
},
}
app = WebComPyApp(root_component=UiFormControlsDemo)
app.run()Binding contract
Every control accepts either a forms-module Field (bound through the framework :bind mechanism, so value sync, dirty on write-back, and touched on blur come for free) or a raw value plus an on_change callback. The two modes are mutually exclusive. Select binds a string-valued field to native option values; Switch exposes the role="switch" pattern with aria-checked over a native checkbox.
FormField wiring
FormField provides hydration-stable association ids through the component DI scope: the slotted control receives the id referenced by the caption's label for, and while the field is touched and invalid it carries aria-invalid plus an aria-describedby link to the error region. Errors never flash on load because the display is gated on touched. Group controls like RadioGroup self-label with a legend, so the wrapper omits its label prop for them.
State vocabulary
Bound controls expose data-state="valid" and flip to data-state="invalid" in the touched-invalid state, matching the headless contract's state attributes. The themed layer styles borders, focus-visible rings, disabled, and invalid states from semantic design tokens in the primitives stylesheet.