生态产品多个设计器现已发布候选版本

查看生态总览
跳转到内容

校验与联动 ​

校验与联动都是写在模板里的规则:字段与容器的 validation 是校验规则,linkage 是联动规则。需要计算的地方写成受限表达式,表达式只能读表单数据与宿主给的变量,不能执行任何代码。

校验规则 ​

kind不通过的情况规则里的字段
required值为空:null、只含空白的字符串、空数组,或元素全为空的数组;false 与 0 不算空—
accepted值不是 true,用于「必须勾选同意」,只适用于布尔值字段—
length字符串的字数或数组的元素个数不在 [min, max] 里;字数按用户看到的字计,一个表情算一个min、max,至少给一个
range数值不在 [min, max] 里min、max,至少给一个
pattern整串不匹配正则pattern(不带两边的斜杠)、flags(只允许 i、m、s、u)
expression表达式的结果是 false;true 与 null 都算通过expression,$value 是字段自己的值

每条规则都有 id,在所在规则列表里唯一;message 可省,写了就是不通过时给用户看的提示,不写时用渲染器按问题码给的缺省文案,见问题码。每个种类适用哪些规则见种类与属性,写了不适用的规则时模板自检报 rule.notApplicable。

校验的约定:

  • 值为空时只看 required 与 accepted,其余规则跳过,「选填但填了就要合规」不必另写条件。
  • 隐藏或禁用的字段不校验,连同被隐藏、禁用的容器里的全部字段;只读的字段照常校验。
  • 列表容器(子表单、子表)自己的规则针对整份列表:required 表示至少一行,length 按行数计;行里的字段各按各的规则校验。
ts
{
  type: 'field',
  id: 'f-invoices',
  kind: 'number',
  key: 'invoices',
  basic: { label: '发票张数' },
  data: { precision: 0 },
  appearance: {},
  validation: [
    { id: 'r-invoices-required', kind: 'required' },
    { id: 'r-invoices-max', kind: 'expression', expression: '$value <= 20', message: '一次最多报销 20 张发票' },
  ],
  linkage: [],
},
{
  type: 'field',
  id: 'f-agree',
  kind: 'checkbox',
  key: 'agree',
  basic: { label: '我确认以上费用真实发生' },
  data: {},
  appearance: {},
  validation: [{ id: 'r-agree', kind: 'accepted', message: '提交前请先确认' }],
  linkage: [],
},

什么时候校验 ​

  • 一个字段第一次校验的时机由渲染器的 validateOn 决定:缺省 blur 是离开字段时,change 是改值时,submit 是提交时。校验过的字段此后随改值重算。
  • 提交时校验全部字段。没通过时派发 invalid,焦点移到第一个出问题的字段,表单上方列出错误摘要;通过时派发 submit。
  • 宿主也可以随时调用 validate() 校验全部字段、validateField(path) 校验一个字段,两者都返回 { valid, issues },不派发提交事件。

联动规则 ​

联动规则按条件改变节点的状态,或给字段赋值:

action条件成立时适用于
show / hide显示 / 隐藏字段、容器、静态元素
enable / disable可用 / 禁用字段、容器
require / optional必填 / 不必填字段
assign把 value 表达式的结果赋给字段字段

when 是条件,结果为 true 时成立;不写表示总是成立。条件不成立时节点回到模板里的静态设定:basic.hidden、basic.disabled、basic.readOnly,以及有没有 required 规则。

按类型显示、按条件必填:

ts
{
  type: 'field',
  id: 'f-city',
  kind: 'text',
  key: 'city',
  basic: { label: '出差城市' },
  data: {},
  appearance: {},
  validation: [],
  linkage: [
    { id: 'l-city-hide', action: 'hide', when: 'type != \'travel\'' },
    { id: 'l-city-require', action: 'require', when: 'type == \'travel\'' },
  ],
},

联动的约定:

  • 每个节点从静态设定出发,按文档顺序依次执行条件成立的规则,同一状态有多条规则成立时排在后面的生效。
  • 赋值立即生效,后面的表达式读到的是新值;赋值引起的连锁变化会重算到稳定为止,仍不稳定时渲染器派发 linkage-error。
  • 隐藏与禁用向下叠加:容器隐藏了,里面的节点都隐藏。
  • 被隐藏的字段保留它的值,重新显示时还在;提交时交出的数据不含隐藏字段的值。
  • require 让没有 required 规则的字段变成必填,提示用缺省文案;optional 让有 required 规则的字段暂时不必填。

表达式 ​

表达式是 XiHan.UI 的受限表达式:有四则运算、比较、&&、||、!、条件运算 ?:,以及逻辑、数学、聚合、文本与日期函数,不能调用宿主代码以外的任何东西。数字按十进制计算,0.1 + 0.2 == 0.3 成立。

表达式里能用的名字:

名字是什么
顶层的数据键表单数据顶层的值,如 type、total;列表容器的键是行的数组,如 items
$value字段或列表容器自己的值
$row列表里的节点所在的那一行,如 $row.qty
$index列表里的节点所在的行号,从 0 起
$ 开头的宿主变量渲染器 variables 属性给的值,如 $limit

a.b 在 a 是数组时逐行取字段,聚合函数会把数组展开,所以 SUM(items.amount) 就是明细各行小计之和。值为 null 时运算大多得到 null,条件是 null 时不成立。

行里的小计与表单的合计都用赋值算出:

ts
{
  type: 'container',
  id: 'c-items',
  kind: 'subtable',
  key: 'items',
  basic: { title: '明细' },
  data: { initialRows: 1 },
  appearance: {},
  validation: [{ id: 'r-items', kind: 'required', message: '至少填写一条明细' }],
  linkage: [],
  regions: [{
    id: 'c-items-row',
    props: {},
    children: [
      { type: 'field', id: 'f-item', kind: 'text', key: 'name', basic: { label: '名目' }, data: {}, appearance: { columnMinWidth: 120 }, validation: [{ id: 'r-item', kind: 'required' }], linkage: [] },
      { type: 'field', id: 'f-qty', kind: 'number', key: 'qty', basic: { label: '数量' }, data: { precision: 0 }, appearance: { columnMinWidth: 104 }, validation: [{ id: 'r-qty', kind: 'range', min: 1 }], linkage: [] },
      { type: 'field', id: 'f-price', kind: 'number', key: 'price', basic: { label: '单价' }, data: { precision: 2 }, appearance: { columnMinWidth: 144 }, validation: [], linkage: [] },
      {
        type: 'field',
        id: 'f-amount',
        kind: 'number',
        key: 'amount',
        basic: { label: '小计', readOnly: true },
        data: { precision: 2 },
        appearance: { columnMinWidth: 144 },
        validation: [],
        linkage: [{ id: 'l-amount', action: 'assign', value: '$row.qty * $row.price' }],
      },
    ],
  }],
},
ts
{
  type: 'field',
  id: 'f-total',
  kind: 'number',
  key: 'total',
  basic: { label: '合计(元)', readOnly: true },
  data: { precision: 2 },
  appearance: {},
  validation: [],
  linkage: [{ id: 'l-total', action: 'assign', value: 'SUM(items.amount)' }],
},
{
  type: 'field',
  id: 'f-reason',
  kind: 'textarea',
  key: 'reason',
  basic: { label: '事由', hint: '合计超过限额时必填' },
  data: {},
  appearance: {},
  validation: [{ id: 'r-reason', kind: 'length', max: 200 }],
  linkage: [{ id: 'l-reason', action: 'require', when: 'total > $limit' }],
},

合计超过宿主给的限额时,事由变成必填。限额经渲染器的 variables 传入,名字以 $ 开头:

ts
export const variables = { $limit: 5000 }
ts
const renderer = document.createElement('xh-form-renderer')
renderer.schema = schema
renderer.values = values
renderer.variables = variables

模板自检会检查每条表达式:写错语法、用了不认识的名字或函数,都报 expression.invalid。宿主变量要告诉自检:validateDocument(schema, { expressionEngine, identifiers: ['$limit'] })。

示例 ​

切到「差旅」,出差城市出现并变成必填;改数量或单价,小计与合计跟着变;合计超过 5000 时事由变成必填。直接提交,下方列出没通过的规则。

Released under The MIT License