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 layout.Column(
input.New(m.Name).
Hint("Name").
OnChange(func(text string) {
send(NameChanged{Name: text})
}),
text.New("Hello, "+m.Name),
).Gap(12)
}
受控输入的可运行示例见仓库 examples/inputs,在仓库根目录运行 go run ./examples/inputs。
常见字段模式¶
Input / TextArea¶
input.New(m.Email).
Hint("you@example.com").
OnChange(func(s string) { send(EmailChanged{s}) })
input.NewTextArea(m.Bio).
OnChange(func(s string) { send(BioChanged{s}) })
右键编辑菜单¶
Input / TextArea 自带右键编辑菜单(剪切 / 复制 / 粘贴 / 全选),菜单项会根据 只读、密码与选区状态自动启用或禁用:密码字段不提供剪切和复制,只读字段不提供 剪切和粘贴。快捷键显示跟随平台(Windows / Linux 为 Ctrl,macOS 为 Command), 菜单文案跟随界面语言(中文 / 英文)。
input.Group / GroupTextArea 的字段与 combobox.New 的输入框共用这套
内置菜单,同样支持 .DefaultContextMenu(false) 与 .ContextMenu(...);
text.Selectable 自带右键菜单(复制 / 全选)。菜单打开期间选区高亮保持可见,
不会因为菜单抢走焦点而消失。
不需要内置菜单时用 .DefaultContextMenu(false) 关闭;要换成自定义菜单(比如
追加业务动作)时用 .ContextMenu(...),自定义菜单会整体替换内置菜单:
input.New(m.Email).DefaultContextMenu(false)
input.New(m.Email).
ContextMenu(menu.New("email-menu", []menu.Item{
{Key: "copy", Label: "复制地址"},
}))
input.Group 前后缀操作¶
静态内容使用 Prefix / Suffix;可点击的前后缀使用 PrefixAction /
SuffixAction。input.GroupAction 会提供统一的 24dp 操作区域、手型光标和
无障碍名称:
input.Group(input.New(m.Website)).
SuffixAction(
input.GroupAction("clear-website", "清理网址").Content(icon.New(lucide.X).Size(16)).
OnClick(func() { send(ClearWebsite{}) }),
)
操作区域默认不会抢占输入框焦点;需要在操作后继续聚焦编辑器时,显式调用
.FocusOnActionPress(true)。自定义插槽间距仍可通过 PrefixPadding /
SuffixPadding 覆盖。输入编辑区属于 PartContent,需要单独调整光标或样式时,
使用 style.New().Part(style.PartContent, ...) 调整光标、文字或排版,不要覆盖整个组合框的根样式。
Checkbox / Switch¶
toggle.NewCheckbox("我同意条款").Key("agree").Checked(m.Agree).
OnChange(func(v bool) { send(AgreeChanged{v}) })
toggle.NewSwitch("深色模式").Key("dark").Checked(m.Dark).
OnChange(func(v bool) { send(DarkChanged{v}) })
RadioGroup¶
选项与选中值都来自 Model(或常量列表 + Model 选中 key)。
Select¶
selector.New(items).Key("state").SelectedKey(m.State).
Label("State").
Placeholder("Select one").
OnChange(func(key string) { send(SetState(key)) })
校验示例:
selector.New(items).Key("required").SelectedKey(m.Choice).
Required(true).
Invalid(m.Choice == "").
ErrorMessage("请选择一项").
OnChange(...)
输入框组合见仓库 examples/inputs;本章其他字段代码为用法片段。
Open 状态契约(重要)¶
适用于 Select、Dropdown、ContextMenu、Popover、Modal 等可开合控件:
未调用 Open(...) → 非受控:控件内部维护 open
Open(bool) → 受控:open 必须在 Model 中,并处理对应的状态回调
DefaultOpen(...) → 仅给非受控提供初始种子
非受控(默认)¶
适合简单页面,不需要从外部关闭面板:
selector.New(items).Key("city").SelectedKey(m.City).
OnChange(func(key string) { send(SetCity(key)) })
// 不调用 Open(...)
受控¶
需要与其它 UI 联动、或从外部关闭时:
type Model struct {
City string
Open bool
}
selector.New(items).Key("city").SelectedKey(m.City).
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 里统一校验,写入错误字段。
input.New(m.Email).
Invalid(m.EmailError != "").
// 具体 ErrorMessage API 以该控件 go doc 为准
OnChange(...)
Label / Description 关联¶
优先用控件自带的 Label / Description 方法;布局容器也会预注册 Label 与字段的关联,语义不依赖视觉顺序。
自定义复合布局时,尽量先布局关联的 Label,再布局控件,以便同帧语义正确。
提交¶
button.New("提交").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
}