Skip to content

响应式与联动

Schema 可以是稳定的普通对象,变化留给其中的函数和 Ref。SuperForm 会在响应式作用域中执行这些配置,并追踪函数实际读取的数据;依赖改变后,只更新对应状态或属性,不要求业务代码重建整份 Schema。

本章按“响应式配置 → 字段状态与模型联动 → 事件和上下文”的顺序组织。状态函数负责描述结果,computed 负责派生值,onUpdate 与组件事件负责副作用;先区分这三类职责,复杂联动会更容易维护。

三种配置形态

形态示例适用情况
静态值disabled: true生命周期内不变
Refdisabled: locked状态由 Schema 外部控制
函数disabled: ({ current }) => current.locked状态依赖当前模型或上下文

hiddendisabledrequired 等状态均支持静态值和函数,其中状态值也可直接使用 Ref。

ts
{
  type: 'Textarea',
  field: 'rejectReason',
  label: '驳回原因',
  hidden: ({ current }) => current.result !== 'reject',
  required: ({ current }) => current.result === 'reject',
  disabled: ({ formData }) => !formData.canReview,
}

current.resultformData.canReview 改变时,相应结果会自动更新。这里无需手工调用刷新方法。

effectData 决定依赖范围

状态函数接收当前节点的 effectData

  • current:当前字段所属对象;数组列中是当前行。
  • formData:根表单数据,适合跨容器联动。
  • value:当前字段值。
  • indexrecord:数组或表格场景中的行信息。
  • parent:上一级响应上下文。
ts
disabled: ({ current }) => !current.country;

// 跨业务分组时读取根模型
hidden: ({ formData }) => formData.orderType !== "company";

优先读取距离最近的 current,让字段组更容易复用;只有确实跨层级时再读取 formData。完整上下文见本页的事件与上下文

dynamicAttrs:计算底层组件属性

固定属性放在 attrs,随数据变化的属性放在 dynamicAttrs

ts
{
  type: 'Input',
  field: 'shortName',
  label: '简称',
  attrs: {
    allowClear: true,
  },
  dynamicAttrs: ({ current }) => ({
    maxlength: current.nameType === 'short' ? 20 : 100,
    placeholder: current.nameType === 'short' ? '请输入 20 字以内简称' : '请输入名称',
  }),
}

最终传给底层组件的属性由以下来源合并:

text
全局字段默认配置
  → attrs 静态配置
  → 节点事件监听器
  → dynamicAttrs 动态结果
  → 继承/计算得到的 disabled

因此动态结果可以覆盖同名静态属性。dynamicAttrs 应只返回组件属性,不要在其中修改模型;它可能随依赖多次执行,副作用会造成难以追踪的更新。

computed:计算并写回字段

节点的 computed(value, effectData) 不是 Vue 模板中的只读计算,它会把返回值持续写回当前字段:

ts
{
  type: 'InputNumber',
  field: 'amount',
  label: '金额',
  computed: (_value, { current }) => {
    return Number(current.quantity || 0) * Number(current.price || 0)
  },
  disabled: true,
}

执行顺序可理解为:

text
读取 quantity / price
  → 计算 amount
  → 写入当前模型的 amount
  → 输入控件与提交数据同步更新

它适合派生字段、合计值和规范化结果。注意:

  • 计算函数会立即执行一次。
  • 返回值即实际存储值,不只是显示文本。
  • 不要在函数中反向修改其依赖字段,否则可能形成循环更新。
  • 只想改变只读显示时,使用 viewRender

options 与 dataSource 的响应性

选项和数据源也可以独立响应:

ts
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 也不会重新启用:

ts
{
  type: 'Card',
  disabled: ({ formData }) => formData.readonly,
  subItems: [
    { type: 'Input', field: 'name', disabled: false }, // 父级禁用时仍禁用
  ],
}

禁用字段暂停其当前校验规则,但仍保留在模型和提交数据中。隐藏字段同样保留模型值。完整状态语义和选择建议见本页的字段状态与联动

保持响应式配置可维护

  • 让函数尽量只读取参数并返回结果,避免在状态函数中改数据。
  • 复用的条件先提取为具名函数,例如 canEditPrice(effectData)
  • 一个字段需要触发业务请求时使用事件或 onUpdate,不要借用 dynamicAttrs
  • 大量字段依赖同一个派生状态时,可在 Schema 外用 Vue computed 统一计算,再把 Ref 传入。

前面的内容解释响应式配置如何建立依赖;下面继续说明这些依赖如何落实为字段状态、模型联动和业务事件。

字段状态与联动

字段联动的关键不是“监听所有变化”,而是先判断业务结果属于哪一类:显示状态、编辑状态、组件属性、派生数据,还是副作用。SuperForm 为这些结果提供了不同入口,让 Schema 的意图保持明确。

hidden:控制是否渲染

ts
{
  type: 'Input',
  field: 'companyName',
  label: '企业名称',
  hidden: ({ current }) => current.customerType !== 'company',
}

隐藏后节点不渲染,但 companyName 仍在模型中,原值也不会自动清空。这使字段临时隐藏后可以恢复原输入。

如果业务要求隐藏时清空值,应把动作写在控制字段的 onUpdate 中:

ts
{
  type: 'Radio',
  field: 'customerType',
  label: '客户类型',
  options: { personal: '个人', company: '企业' },
  onUpdate: ({ current }) => {
    if (current.customerType !== 'company') current.companyName = undefined
  },
}

隐藏不等于跳过校验。条件字段通常让 hiddenrequired 使用同一个判断,具体见动态必填

disabled:控制是否允许输入

ts
{
  type: 'Input',
  field: 'contractNo',
  label: '合同编号',
  disabled: ({ current }) => current.status !== 'draft',
}

禁用字段仍显示、仍保留模型值并进入提交数据,但当前字段规则会暂停。容器的禁用状态向下继承,且父级禁用优先:

ts
{
  type: 'Card',
  disabled: ({ formData }) => formData.readonly,
  subItems: [/* 整组字段都会禁用 */],
}

如果希望不可编辑时仍保持纯文本视觉,表格列或表单字段可使用 editable 在输入控件和只读内容之间切换。

editable:在编辑与只读之间切换

ts
{
  type: 'InputNumber',
  field: 'approvedAmount',
  label: '核准金额',
  editable: ({ current }) => current.status === 'reviewing',
}

editable: falsedisabled: true 不同:

状态视觉结果表单值校验
disabled仍是禁用控件保留暂停
editable: false切换为只读展示保留字段仍属于表单模型
hidden不渲染保留需自行配合条件规则

只读内容如何映射选项、范围和自定义渲染,见渲染与插槽

required:让业务条件成为规则

ts
{
  type: 'Textarea',
  field: 'reason',
  label: '原因',
  required: ({ current }) => current.result === 'reject',
}

它同时更新必填标识和必填规则,不需要手工维护两份状态。默认提示由 label 推导,例如 label: '原因' 会生成“原因不能为空!”。完整规则展开见校验机制

dynamicAttrs:联动 UI 参数

ts
{
  type: 'InputNumber',
  field: 'discount',
  label: '折扣',
  dynamicAttrs: ({ current }) => ({
    min: 0,
    max: current.vip ? 50 : 20,
    addonAfter: '%',
  }),
}

适合动态上下限、占位提示、选项组件的交互属性等。它只应返回底层组件属性,不负责写模型或调用接口。属性合并顺序见响应式与联动:dynamicAttrs

computed:生成派生字段

ts
{
  type: 'InputNumber',
  field: 'total',
  label: '合计',
  computed: (_value, { current }) => {
    return Number(current.price || 0) * Number(current.quantity || 0)
  },
  editable: false,
}

computed 的返回值会写回 total,因此能被提交、校验和其他字段继续依赖。只需格式化显示而不改变数据时,使用 viewRender

onUpdate:执行值变化后的动作

ts
{
  type: 'Select',
  field: 'province',
  label: '省份',
  onUpdate: async ({ current, value }) => {
    current.city = undefined
    cityOptions.value = await api.getCities(value)
  },
}

它适合:

  • 清理依赖字段。
  • 请求下一级选项。
  • 把变化通知给业务状态。
  • 执行无法表示为纯计算的副作用。

如果只需处理底层组件的特定交互参数,使用 onChangeonSearch 等事件。两者差异见事件与上下文

选择正确的联动入口

目标首选配置
是否出现hidden
是否允许操作disabled
输入态与只读态切换editable
是否必填required
动态组件属性dynamicAttrs
计算并存储字段computed
值变化后的业务动作onUpdate
底层组件特定事件onChange
只修改展示结果viewRender

一个完整联动示例

ts
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 会先注入当前字段的数据上下文,再追加组件原始参数,让同一套回调写法可以用于普通字段、嵌套对象和数组行。

两种事件写法

事件可以直接写在节点顶层:

ts
{
  type: 'Input',
  field: 'keyword',
  onChange(effectData, event) {
    console.log(effectData.value, event)
  },
  onBlur(effectData, event) {},
}

也可以集中写在 on 中:

ts
{
  type: 'Input',
  field: 'keyword',
  on: {
    change(effectData, event) {},
    blur(effectData, event) {},
  },
}

on.change 会转换为底层组件的 onChange。顶层 onXxx 更便于类型提示,on 更适合批量组合事件;两种写法不要为同一事件重复配置。

事件最终调用形式为:

ts
schemaHandler(effectData, ...componentEventArgs);

后面的参数完全来自底层组件,因此 Input 的 onChange、Select 的 onSelect 等仍应参照 Ant Design Vue 对应组件。

onUpdate 与组件事件的区别

onUpdate(effectData) 专门观察字段实际存储值:

ts
{
  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

ts
hidden: ({ current, value }) => current.status !== "active" || !value;

current 与 formData

ts
{
  type: 'Group',
  field: 'invoice',
  subItems: [
    {
      type: 'Input',
      field: 'title',
      disabled: ({ current }) => current.type === 'personal',
      hidden: ({ formData }) => !formData.needInvoice,
    },
  ],
}

这里 currentinvoiceformData 是整个表单。优先用 current 表达组内依赖,能让 Group 被移动或复用时仍保持正确。

parent 不是父数据的别名

parent 指向上一级 effectData,因此上一级数据通常通过 parent.current 读取:

ts
hidden: ({ parent }) => parent?.current?.mode !== "advanced";

如果只是跨多层读取根数据,直接使用 formData 更清楚;parent 更适合组件或通用字段组确实需要理解嵌套关系的情况。

数组行上下文

在 InputList、ListGroup 和 Table 列中:

ts
{
  type: 'InputNumber',
  field: 'quantity',
  disabled: ({ record }) => record.locked,
  onUpdate: ({ record, index, value }) => {
    console.log('第几行', index, '当前记录', record, '新数量', value)
  },
}

currentrecord 通常都指向当前行对象;record 更能表达行级业务语义。数组结构和编辑模式见数组与表格

远程选项函数

options 为函数时也会收到上下文:

ts
options: ({ current }) => api.getCities({ province: current.province });

Select 同时满足以下条件时会自动进入远程搜索模式:

  • attrs.showSearch 已开启。
  • options 是函数。
  • 没有显式配置 onSearch

此时框架约以 600ms 尾部节流调用:

ts
options(effectData, keyword);

若显式提供 onSearch,业务代码完全接管搜索过程,框架不再自动用关键字调用 options。返回格式、字典归一化和原始值数组规则见选择输入:远程搜索

页面组件的扩展上下文

页面组件会在基础字段之上补充自己的上下文。例如 SuperTable 按钮还可能获得 selectedRowsselectedRowKeystableRef 及页面动作。不要假设所有字段位置都拥有这些值;可复用回调应只读取当前场景明确提供的数据。

相关内容:

基于 MIT 许可发布