07 - Forms and controlled state¶
Form controls display a value from the model and report user intent through a callback. The model remains the source of truth.
Minimal form¶
type Model struct {
Name string
}
type NameChanged struct{ Value string }
func View(_ *ui.Context, m Model, send ui.Send[Msg]) ui.Widget {
return ui.Column(
ui.Input("name", m.Name).
Hint("Name").
OnChange(func(value string) {
send(NameChanged{Value: value})
}),
ui.Text("Hello, "+m.Name),
).Gap(12)
}
The complete runnable form is in examples/form.
Common fields¶
ui.Input("email", m.Email).
Hint("you@example.com").
OnChange(func(value string) { send(EmailChanged{Value: value}) })
ui.TextArea("bio", m.Bio).
OnChange(func(value string) { send(BioChanged{Value: value}) })
ui.Checkbox("agree", m.Agree).
Label("I agree to the terms").
OnChange(func(value bool) { send(AgreeChanged{Value: value}) })
InputGroup actions¶
Use Prefix and Suffix for static content. Use PrefixAction and
SuffixAction for clickable affixes. InputGroupAction provides a stable
24dp target, pointer cursor, and accessible label:
ui.InputGroup(ui.Input("website", m.Website)).
SuffixAction(
ui.InputGroupAction("clear-website", "Clear website", ui.Icon(lucide.X).Size(16)).
OnClick(func() { send(ClearWebsite{}) }),
)
Action slots do not focus the editor by default. Call
.FocusOnActionPress(true) when the editor should regain focus after an
action. Override the default slot spacing with PrefixPadding or
SuffixPadding when a custom layout needs it. The editor face is
PartContent; use ui.Part(ui.PartContent, ...) to customize its cursor,
text, or typography without changing the outer group shell.
Selection controls use model keys:
ui.Select("state", m.State, items).
Label("State").
Placeholder("Select one").
OnChange(func(key string) { send(SetState(key)) })
Use Invalid and ErrorMessage for validation feedback. Validate and update
the error fields in Update, usually when the user submits.
Open state¶
Select, dropdown, context-menu, popover, and modal controls support two modes:
No Open(...) -> uncontrolled; the widget owns open state
Open(bool) -> controlled; the model owns open state
DefaultOpen(bool) -> initial value for uncontrolled state
Controlled usage must update the model from the component's open-state callback:
ui.Select("city", m.City, items).
Open(m.CityOpen).
OnOpenChange(func(open bool) { send(SetCityOpen(open)) }).
OnChange(func(key string) { send(SetCity(key)) })
Calling Open(...) without handling the callback leaves the panel stuck at the
value supplied by the model.
Dropdown uses OnOpenChangeEvent and reads the new state from event.Open.
Select, ContextMenu, Popover, and Modal continue to use OnOpenChange.
Labels and descriptions¶
Prefer a control's Label and Description methods. For custom compositions,
associate a Label with the field key so keyboard focus and accessibility
semantics remain correct.
Continue with Commands and subscriptions.