响应式与联动
Schema 可以是稳定的普通对象,变化留给其中的函数和 Ref。SuperForm 会在响应式作用域中执行这些配置,并追踪函数实际读取的数据;依赖改变后,只更新对应状态或属性,不要求业务代码重建整份 Schema。
本章按“响应式配置 → 字段状态与模型联动 → 事件和上下文”的顺序组织。状态函数负责描述结果,computed 负责派生值,onUpdate 与组件事件负责副作用;先区分这三类职责,复杂联动会更容易维护。
三种配置形态
| 形态 | 示例 | 适用情况 |
|---|---|---|
| 静态值 | disabled: true | 生命周期内不变 |
| Ref | disabled: locked | 状态由 Schema 外部控制 |
| 函数 | disabled: ({ current }) => current.locked | 状态依赖当前模型或上下文 |
hidden、disabled、required 等状态均支持静态值和函数,其中状态值也可直接使用 Ref。
{
type: 'Textarea',
field: 'rejectReason',
label: '驳回原因',
hidden: ({ current }) => current.result !== 'reject',
required: ({ current }) => current.result === 'reject',
disabled: ({ formData }) => !formData.canReview,
}当 current.result 或 formData.canReview 改变时,相应结果会自动更新。这里无需手工调用刷新方法。
effectData 决定依赖范围
状态函数接收当前节点的 effectData:
current:当前字段所属对象;数组列中是当前行。formData:根表单数据,适合跨容器联动。value:当前字段值。index、record:数组或表格场景中的行信息。parent:上一级响应上下文。
disabled: ({ current }) => !current.country;
// 跨业务分组时读取根模型
hidden: ({ formData }) => formData.orderType !== "company";优先读取距离最近的 current,让字段组更容易复用;只有确实跨层级时再读取 formData。完整上下文见本页的事件与上下文。
dynamicAttrs:计算底层组件属性
固定属性放在 attrs,随数据变化的属性放在 dynamicAttrs:
{
type: 'Input',
field: 'shortName',
label: '简称',
attrs: {
allowClear: true,
},
dynamicAttrs: ({ current }) => ({
maxlength: current.nameType === 'short' ? 20 : 100,
placeholder: current.nameType === 'short' ? '请输入 20 字以内简称' : '请输入名称',
}),
}最终传给底层组件的属性由以下来源合并:
全局字段默认配置
→ attrs 静态配置
→ 节点事件监听器
→ dynamicAttrs 动态结果
→ 继承/计算得到的 disabled因此动态结果可以覆盖同名静态属性。dynamicAttrs 应只返回组件属性,不要在其中修改模型;它可能随依赖多次执行,副作用会造成难以追踪的更新。
computed:计算并写回字段
节点的 computed(value, effectData) 不是 Vue 模板中的只读计算,它会把返回值持续写回当前字段:
{
type: 'InputNumber',
field: 'amount',
label: '金额',
computed: (_value, { current }) => {
return Number(current.quantity || 0) * Number(current.price || 0)
},
disabled: true,
}执行顺序可理解为:
读取 quantity / price
→ 计算 amount
→ 写入当前模型的 amount
→ 输入控件与提交数据同步更新它适合派生字段、合计值和规范化结果。注意:
- 计算函数会立即执行一次。
- 返回值即实际存储值,不只是显示文本。
- 不要在函数中反向修改其依赖字段,否则可能形成循环更新。
- 只想改变只读显示时,使用
viewRender。
options 与 dataSource 的响应性
选项和数据源也可以独立响应:
const cities = ref([]);
const record = ref({ province: undefined, city: undefined });
const schema = {
dataSource: record,
subItems: [
{
type: "Select",
field: "province",
label: "省份",
options: provinceOptions,
},
{ type: "Select", field: "city", label: "城市", options: cities },
],
};options可以是数组、Ref 或函数;函数可返回数组或 Promise。dataSource可以是对象或 Ref;Ref 指向新对象时,SuperForm 切换到新模型。- 字段
value可以绑定 Ref,与模型字段进行双向同步。
选项函数的远程搜索参数和触发条件见选择输入:远程搜索,数据源切换的具体行为见Schema 与数据模型。
状态优先级与继承
容器禁用会传递给后代。父级已经禁用时,子项返回 disabled: false 也不会重新启用:
{
type: 'Card',
disabled: ({ formData }) => formData.readonly,
subItems: [
{ type: 'Input', field: 'name', disabled: false }, // 父级禁用时仍禁用
],
}禁用字段暂停其当前校验规则,但仍保留在模型和提交数据中。隐藏字段同样保留模型值。完整状态语义和选择建议见本页的字段状态与联动。
保持响应式配置可维护
- 让函数尽量只读取参数并返回结果,避免在状态函数中改数据。
- 复用的条件先提取为具名函数,例如
canEditPrice(effectData)。 - 一个字段需要触发业务请求时使用事件或
onUpdate,不要借用dynamicAttrs。 - 大量字段依赖同一个派生状态时,可在 Schema 外用 Vue
computed统一计算,再把 Ref 传入。
前面的内容解释响应式配置如何建立依赖;下面继续说明这些依赖如何落实为字段状态、模型联动和业务事件。
字段状态与联动
字段联动的关键不是“监听所有变化”,而是先判断业务结果属于哪一类:显示状态、编辑状态、组件属性、派生数据,还是副作用。SuperForm 为这些结果提供了不同入口,让 Schema 的意图保持明确。
hidden:控制是否渲染
{
type: 'Input',
field: 'companyName',
label: '企业名称',
hidden: ({ current }) => current.customerType !== 'company',
}隐藏后节点不渲染,但 companyName 仍在模型中,原值也不会自动清空。这使字段临时隐藏后可以恢复原输入。
如果业务要求隐藏时清空值,应把动作写在控制字段的 onUpdate 中:
{
type: 'Radio',
field: 'customerType',
label: '客户类型',
options: { personal: '个人', company: '企业' },
onUpdate: ({ current }) => {
if (current.customerType !== 'company') current.companyName = undefined
},
}隐藏不等于跳过校验。条件字段通常让 hidden 和 required 使用同一个判断,具体见动态必填。
disabled:控制是否允许输入
{
type: 'Input',
field: 'contractNo',
label: '合同编号',
disabled: ({ current }) => current.status !== 'draft',
}禁用字段仍显示、仍保留模型值并进入提交数据,但当前字段规则会暂停。容器的禁用状态向下继承,且父级禁用优先:
{
type: 'Card',
disabled: ({ formData }) => formData.readonly,
subItems: [/* 整组字段都会禁用 */],
}如果希望不可编辑时仍保持纯文本视觉,表格列或表单字段可使用 editable 在输入控件和只读内容之间切换。
editable:在编辑与只读之间切换
{
type: 'InputNumber',
field: 'approvedAmount',
label: '核准金额',
editable: ({ current }) => current.status === 'reviewing',
}editable: false 与 disabled: true 不同:
| 状态 | 视觉结果 | 表单值 | 校验 |
|---|---|---|---|
disabled | 仍是禁用控件 | 保留 | 暂停 |
editable: false | 切换为只读展示 | 保留 | 字段仍属于表单模型 |
hidden | 不渲染 | 保留 | 需自行配合条件规则 |
只读内容如何映射选项、范围和自定义渲染,见渲染与插槽。
required:让业务条件成为规则
{
type: 'Textarea',
field: 'reason',
label: '原因',
required: ({ current }) => current.result === 'reject',
}它同时更新必填标识和必填规则,不需要手工维护两份状态。默认提示由 label 推导,例如 label: '原因' 会生成“原因不能为空!”。完整规则展开见校验机制。
dynamicAttrs:联动 UI 参数
{
type: 'InputNumber',
field: 'discount',
label: '折扣',
dynamicAttrs: ({ current }) => ({
min: 0,
max: current.vip ? 50 : 20,
addonAfter: '%',
}),
}适合动态上下限、占位提示、选项组件的交互属性等。它只应返回底层组件属性,不负责写模型或调用接口。属性合并顺序见响应式与联动:dynamicAttrs。
computed:生成派生字段
{
type: 'InputNumber',
field: 'total',
label: '合计',
computed: (_value, { current }) => {
return Number(current.price || 0) * Number(current.quantity || 0)
},
editable: false,
}computed 的返回值会写回 total,因此能被提交、校验和其他字段继续依赖。只需格式化显示而不改变数据时,使用 viewRender。
onUpdate:执行值变化后的动作
{
type: 'Select',
field: 'province',
label: '省份',
onUpdate: async ({ current, value }) => {
current.city = undefined
cityOptions.value = await api.getCities(value)
},
}它适合:
- 清理依赖字段。
- 请求下一级选项。
- 把变化通知给业务状态。
- 执行无法表示为纯计算的副作用。
如果只需处理底层组件的特定交互参数,使用 onChange、onSearch 等事件。两者差异见事件与上下文。
选择正确的联动入口
| 目标 | 首选配置 |
|---|---|
| 是否出现 | hidden |
| 是否允许操作 | disabled |
| 输入态与只读态切换 | editable |
| 是否必填 | required |
| 动态组件属性 | dynamicAttrs |
| 计算并存储字段 | computed |
| 值变化后的业务动作 | onUpdate |
| 底层组件特定事件 | onChange 等 |
| 只修改展示结果 | viewRender |
一个完整联动示例
const isRejected = ({ current }) => current.result === "reject";
const schema = {
subItems: [
{
type: "Radio",
field: "result",
label: "审核结果",
options: { pass: "通过", reject: "驳回" },
required: true,
onUpdate: ({ current }) => {
if (current.result !== "reject") current.reason = undefined;
},
},
{
type: "Textarea",
field: "reason",
label: "驳回原因",
hidden: (data) => !isRejected(data),
required: isRejected,
dynamicAttrs: ({ current }) => ({
maxlength: current.urgent ? 200 : 500,
}),
},
],
};这个 Schema 同时表达了显示、必填、清理旧值和动态长度限制,各项职责彼此独立。可运行版本见字段联动示例。
事件与上下文
Schema 事件不是简单转发底层组件事件。SuperForm 会先注入当前字段的数据上下文,再追加组件原始参数,让同一套回调写法可以用于普通字段、嵌套对象和数组行。
两种事件写法
事件可以直接写在节点顶层:
{
type: 'Input',
field: 'keyword',
onChange(effectData, event) {
console.log(effectData.value, event)
},
onBlur(effectData, event) {},
}也可以集中写在 on 中:
{
type: 'Input',
field: 'keyword',
on: {
change(effectData, event) {},
blur(effectData, event) {},
},
}on.change 会转换为底层组件的 onChange。顶层 onXxx 更便于类型提示,on 更适合批量组合事件;两种写法不要为同一事件重复配置。
事件最终调用形式为:
schemaHandler(effectData, ...componentEventArgs);后面的参数完全来自底层组件,因此 Input 的 onChange、Select 的 onSelect 等仍应参照 Ant Design Vue 对应组件。
onUpdate 与组件事件的区别
onUpdate(effectData) 专门观察字段实际存储值:
{
type: 'Select',
field: 'province',
label: '省份',
onUpdate({ current, value }) {
current.city = undefined
loadCities(value)
},
}| 回调 | 触发依据 | 典型用途 |
|---|---|---|
onChange 等事件 | 底层组件发出事件 | 获取原始事件参数、响应具体交互 |
onUpdate | 模型字段值发生变化 | 字段联动、请求数据、清理依赖值 |
computed | 响应依赖重新计算 | 生成并写回派生值 |
onUpdate 不会作为普通 onXxx 监听器传给底层组件。它观察的是实际模型值,因此外部数据同步造成的值变化也可能触发;回调应避免无条件重复写回同一字段。
effectData 上下文
常用字段如下:
| 字段 | 含义 | 常见场景 |
|---|---|---|
formData | 根表单数据 | 跨容器联动、提交级判断 |
current | 当前字段所属对象或当前记录 | 同级字段联动 |
parent | 上一级 effectData | 需要沿嵌套上下文向上访问 |
value | 当前字段值 | 当前值判断 |
field | 当前字段在当前对象中的名称 | 通用处理函数 |
index | 数组元素下标 | InputList、ListGroup、Table |
record | 数组或表格当前记录 | 行操作、列渲染 |
isView | 当前是否处于只读展示 | 同一渲染函数适配编辑和查看 |
回调参数保持响应式。可以直接读取值,不需要手工 .value:
hidden: ({ current, value }) => current.status !== "active" || !value;current 与 formData
{
type: 'Group',
field: 'invoice',
subItems: [
{
type: 'Input',
field: 'title',
disabled: ({ current }) => current.type === 'personal',
hidden: ({ formData }) => !formData.needInvoice,
},
],
}这里 current 是 invoice,formData 是整个表单。优先用 current 表达组内依赖,能让 Group 被移动或复用时仍保持正确。
parent 不是父数据的别名
parent 指向上一级 effectData,因此上一级数据通常通过 parent.current 读取:
hidden: ({ parent }) => parent?.current?.mode !== "advanced";如果只是跨多层读取根数据,直接使用 formData 更清楚;parent 更适合组件或通用字段组确实需要理解嵌套关系的情况。
数组行上下文
在 InputList、ListGroup 和 Table 列中:
{
type: 'InputNumber',
field: 'quantity',
disabled: ({ record }) => record.locked,
onUpdate: ({ record, index, value }) => {
console.log('第几行', index, '当前记录', record, '新数量', value)
},
}current 与 record 通常都指向当前行对象;record 更能表达行级业务语义。数组结构和编辑模式见数组与表格。
远程选项函数
options 为函数时也会收到上下文:
options: ({ current }) => api.getCities({ province: current.province });Select 同时满足以下条件时会自动进入远程搜索模式:
attrs.showSearch已开启。options是函数。- 没有显式配置
onSearch。
此时框架约以 600ms 尾部节流调用:
options(effectData, keyword);若显式提供 onSearch,业务代码完全接管搜索过程,框架不再自动用关键字调用 options。返回格式、字典归一化和原始值数组规则见选择输入:远程搜索。
页面组件的扩展上下文
页面组件会在基础字段之上补充自己的上下文。例如 SuperTable 按钮还可能获得 selectedRows、selectedRowKeys、tableRef 及页面动作。不要假设所有字段位置都拥有这些值;可复用回调应只读取当前场景明确提供的数据。
相关内容:
- 字段状态与联动:如何选择状态函数、事件和计算字段。
- 渲染与插槽:渲染函数中的上下文。
- 按钮组 SuperButtons:按钮动作和上下文。