07 · 表单与受控状态¶
表单的核心:
值在 Model 里;控件只展示并回报意图。
最小表单¶
type Model struct {
Name string
}
type NameChanged struct{ Name string }
func Update(m *Model, msg Msg) ui.Cmd[Msg] {
switch msg := msg.(type) {
case NameChanged:
m.Name = msg.Name
}
return nil
}
func View(_ *ui.Context, m Model, send ui.Send[Msg]) ui.Widget {
return ui.Column(
ui.Input("name", m.Name).
Hint("Name").
OnChange(func(text string) {
send(NameChanged{Name: text})
}),
ui.Text("Hello, "+m.Name),
).Gap(12)
}
示例:examples/form。
常见字段模式¶
Input / TextArea¶
ui.Input("email", m.Email).
Hint("you@example.com").
OnChange(func(s string) { send(EmailChanged{s}) })
ui.TextArea("bio", m.Bio).
OnChange(func(s string) { send(BioChanged{s}) })
InputGroup 前后缀操作¶
静态内容使用 Prefix / Suffix;可点击的前后缀使用 PrefixAction /
SuffixAction。InputGroupAction 会提供统一的 24dp 操作区域、手型光标和
无障碍名称:
ui.InputGroup(ui.Input("website", m.Website)).
SuffixAction(
ui.InputGroupAction("clear-website", "清理网址", ui.Icon(lucide.X).Size(16)).
OnClick(func() { send(ClearWebsite{}) }),
)
操作区域默认不会抢占输入框焦点;需要在操作后继续聚焦编辑器时,显式调用
.FocusOnActionPress(true)。自定义插槽间距仍可通过 PrefixPadding /
SuffixPadding 覆盖。输入编辑区属于 PartContent,需要单独调整光标或样式时,
使用 ui.Part(ui.PartContent, ...) 调整光标、文字或排版,不要覆盖整个组合框的根样式。
Checkbox / Switch¶
ui.Checkbox("agree", m.Agree).
Label("我同意条款").
OnChange(func(v bool) { send(AgreeChanged{v}) })
ui.Switch("dark", m.Dark).
OnChange(func(v bool) { send(DarkChanged{v}) })
RadioGroup¶
选项与选中值都来自 Model(或常量列表 + Model 选中 key)。
Select¶
ui.Select("state", m.State, items).
Label("State").
Placeholder("Select one").
OnChange(func(key string) { send(SetState(key)) })
多选:
ui.SelectMultiple("countries", m.Countries, items).
OnSelectionChange(func(keys []string) {
send(SetCountries(keys))
})
校验示例:
ui.Select("required", m.Choice, items).
Required(true).
Invalid(m.Choice == "").
ErrorMessage("请选择一项").
OnChange(...)
示例:examples/selects、examples/checkboxes、examples/switches、examples/inputs。
Open 状态契约(重要)¶
适用于 Select、Dropdown、ContextMenu、Popover、Modal 等可开合控件:
未调用 Open(...) → 非受控:控件内部维护 open
Open(bool) → 受控:open 必须在 Model 中,并处理对应的状态回调
DefaultOpen(...) → 仅给非受控提供初始种子
非受控(默认)¶
适合简单页面,不需要从外部关闭面板:
ui.Select("city", m.City, items).
OnChange(func(key string) { send(SetCity(key)) })
// 不调用 Open(...)
受控¶
需要与其它 UI 联动、或从外部关闭时:
type Model struct {
City string
Open bool
}
ui.Select("city", m.City, items).
Open(m.Open).
OnOpenChange(func(open bool) { send(SetOpen(open)) }).
OnChange(func(key string) { send(SetCity(key)) })
注意: 调用了 Open(...) 却不处理对应的状态回调、不更新 Model,面板会卡住。
Dropdown 使用 OnOpenChangeEvent,通过 event.Open 读取新状态;Select、
ContextMenu、Popover 和 Modal 继续使用 OnOpenChange。
DefaultOpen¶
校验与错误展示¶
模式:
- Model 持有值与错误信息(或派生:
invalid := m.Email == "")。 - 控件
.Invalid(true)/.ErrorMessage("...")。 - 提交时在 Update 里统一校验,写入错误字段。
ui.Input("email", m.Email).
Invalid(m.EmailError != "").
// 具体 ErrorMessage API 以该控件 go doc 为准
OnChange(...)
Label / Description 关联¶
优先用控件自带的 Label / Description 方法;布局容器也会预注册 Label 与字段的关联,语义不依赖视觉顺序。
自定义复合布局时,尽量先布局关联的 Label,再布局控件,以便同帧语义正确。
提交¶
ui.Button("submit", ui.Text("提交")).OnClick(func() {
send(Submit{})
})
func Update(m *Model, msg Msg) ui.Cmd[Msg] {
switch msg.(type) {
case Submit:
if err := validate(m); err != nil {
m.FormError = err.Error()
return nil
}
return submitCmd(snapshot(m)) // 见第 08 章
}
return nil
}