DirectSurface UIDirectSurface UI
开始使用
文档/业务指南

表单开发

import {  FormBindingBag,  FormSession,  RenderFieldPresenter,  RenderFormPanel,  RenderNumberInput,  RenderTextBox,} from 'ds-ui'

表单是业务系统中最常见的页面形态。DirectSurface UI 推荐用 RenderFormPanel 管理字段排列,用 RenderFieldPresenter 管理标签、必填、单位、校验状态和提示信息。

如果你正在搭完整业务页面,先看 常用业务场景组合。本页重点说明表单本身如何组织、绑定和校验。

表单结构

RenderFormPanel
  RenderFieldPresenter
    RenderTextBox / RenderComboBox / RenderDatePicker

RenderFormPanel 负责字段网格,RenderFieldPresenter 负责单个字段的标签和状态,具体输入组件只负责值编辑。FormSession 位于页面或 controller 层,用来保存草稿、校验结果和提交状态,不参与布局和绘制。

组件选择

字段类型 推荐组件 说明
单行文本 TextField 公共类名是 RenderTextBox。适合姓名、编码、关键字。
多行文本 TextArea 适合备注、批注、说明。
数字 NumberInput 适合年龄、数量、金额、比例。
日期时间 DatePicker 适合日期、时间、日期时间字段。
单选枚举 Dropdown 公共类名是 RenderComboBox。值必须来自候选项。
多选枚举 MultiSelectDropdown 适合标签、诊断集合、权限集合。
树选择 DropTreeEdit 适合科室、分类、层级字典。
表格选择 LookupEdit 适合需要多列候选信息的选择。

最小示例

const form = new RenderFormPanel({  columns: 2,  labelWidth: 88,  adaptive: true,  columnGap: 16,  rowGap: 8,}) form.addField(new RenderFieldPresenter({  label: '姓名',  required: true,  child: new RenderTextBox({ placeholder: '请输入姓名' }),})) form.addField(new RenderFieldPresenter({  label: '年龄',  unit: '岁',  child: new RenderNumberInput({ value: 0 }),}))
LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

字段值管理

Render 组件只负责交互和值变化通知,不会偷偷创建业务表单模型。简单页面可以直接维护一个 draft;需要取消、关联字段回填、异步校验或防重复提交时,使用页面级 FormSession 和显式字段绑定:

interface PatientDraft {  name: string  age: number} const session = new FormSession<PatientDraft>({  name: '',  age: 0,})const bindings = new FormBindingBag(session)const nameInput = new RenderTextBox({})const formPanel = new RenderFormPanel({ columns: 2 })const nameField = new RenderFieldPresenter({  label: '姓名',  required: true,  child: nameInput,}) bindings.bindField('name', nameInput, {  presenter: nameField,  rules: [value => value.trim() ? undefined : '姓名不能为空'],})formPanel.addField(nameField)

推荐规则:

  • 输入组件的 value 是 UI 当前值。
  • FormSession 的 draft 是提交前的业务草稿,bindField() 显式声明输入组件对应哪个字段。
  • 服务层 DTO 在提交时由 session.snapshot()runSubmit() 传入的 draft 转换出来。
  • 不要把后端原始对象直接作为多个输入组件共享的可变对象,避免取消编辑时难以回滚。
  • 一次业务操作需要更新多个字段时使用 setValues(),外部只收到一次完整变化。

FormBindingBag 不会扫描 Render Tree,也不会根据组件嵌套猜字段。页面仍然平铺地决定哪个输入对应哪个字段;同一个组件原有的 onChange 也可以继续处理页面反馈等独立逻辑。值同步、失焦校验、错误投影和提交前编辑收尾见编辑器契约与表单字段绑定,编辑快照、dirty、touched、恢复和事务见编辑会话

校验状态

字段校验应该落在字段展示层,而不是只改变输入组件边框。

const nameField = new RenderFieldPresenter({  label: '姓名',  required: true,  status: 'error',  message: '姓名不能为空',  child: new RenderTextBox({ placeholder: '请输入姓名' }),})

这样标签、提示、输入控件可以形成一个完整的校验区域。

校验可以分两类:

类型 时机 展示
即时校验 输入、失焦、选择值变化 更新 RenderFieldPresenterstatusmessage
提交校验 点击保存或提交命令 汇总所有字段错误,并把第一个错误滚动到可视区域。

FormSession 可以注册 change、blur、submit 三种触发时机的同步或异步规则,并把不同来源的错误分开保存。使用 bindField() 时,绑定会把当前字段的第一条错误投影给 RenderFieldPresenter;手动接线时仍可直接订阅 form.errors。具体写法见表单校验与提交

异步校验和提交都绑定在会话生命周期上。页面关闭时调用 session.dispose(),当前校验和提交信号会被取消,旧结果不会覆盖新值。

表单提交

表单提交建议分四层:

  • UI 层负责具体输入和编辑器暂存状态。
  • 显式字段绑定负责把正式值同步到 FormSession
  • 页面层通过 FormSession 执行校验、管理提交状态并组装请求对象。
  • 服务层提交数据并返回结果。

按钮、菜单、快捷键应尽量绑定页面命令,不要在每个按钮里重复写权限和保存逻辑。

interface RecordDraft {  title: string} const session = new FormSession<RecordDraft>({ title: '' })const persist = async (  draft: Readonly<RecordDraft>,  _signal: AbortSignal,): Promise<RecordDraft> => ({ ...draft }) const saveCommand = {  id: 'form.save',  title: '保存',  canExecute: () => !session.isSubmitting,  execute: async () => {    const result = await session.runSubmit(persist)    if (result.status === 'submitted') {      session.acceptChanges(result.value)    }  },}

runSubmit() 会先执行 submit 校验,并阻止同一会话重复提交。它不会替业务决定何时建立新基线;保存成功后由页面显式调用 acceptChanges()。命令状态变化后调用 commandManager.invalidate()。详见 命令与权限

响应式规则

  • 桌面宽度下可以使用 2 到 4 列。
  • 窄宽度下使用 adaptive: true 降低列数。
  • 字段阅读顺序必须稳定,不能因为响应式导致业务语义混乱。
  • 超长标签优先省略号加 tooltip,超长值根据业务决定省略或换行。

使用建议

  • 简单字段用 RenderFormPanel
  • 大量动态字段可以先分组,再在组内使用 FormPanel。
  • 表格内编辑不要嵌完整表单,应使用 GridView 的单元格编辑器。
  • FormSessionFormBindingBag 应和当前页面、弹窗或文档页同生共死,不要放进应用级单例。
  • 表单页关闭时先 dispose binding bag,再 dispose 会话,清理字段接线、未完成的异步校验、提交和页面级上下文。

典型组合

查询表单

查询表单通常短而密集,适合放在页面顶部:

PageHeader
RenderFormPanel 查询条件
CommandToolbar 查询 / 重置 / 导出
GridView

查询条件只作为页面 draft。点击查询后再请求数据,不建议每个字段变化都立即刷新大表格。

编辑表单

编辑表单通常需要分组和滚动:

ScrollViewer
  Section 基本信息
    RenderFormPanel
  Section 联系方式
    RenderFormPanel
  Section 备注
    TextArea
Footer CommandToolbar

保存、取消、提交、审核等操作应放在固定操作区或页面命令栏,避免用户滚动到表单底部才看到关键动作。

弹窗表单

弹窗表单适合短字段和临时编辑:

Modal / Popover
  RenderFormPanel
  Confirm / Cancel

弹窗每次打开时根据当前值创建独立 FormSession。取消时 reset 或直接 dispose,确定后把 snapshot() 交给打开源页面合并;不要让弹窗直接编辑源页面对象。

常见错误

只改输入框边框表示错误

这样标签、必填标识、错误文案和输入框状态会割裂。应把错误状态放到 RenderFieldPresenter

在表格单元格里嵌完整表单

表格编辑应使用 GridView 的编辑能力或单元格 popup 编辑器。完整表单适合页面、抽屉或弹窗。

把页面草稿放到应用级上下文

页面草稿应放在页面对象、页面 controller 或页面级 App Context 中。放到应用级上下文会导致 tab 间串数据和关闭后内存不释放。

相关文档