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

查看生态总览
跳转到内容

扩展 ​

新的字段种类、渲染方式、属性编辑器、模板规则、命令与面板都写成扩展:一个普通对象,经元素的 extensions 属性传入,只对这一个实例生效,同一页面的其它实例不受影响。内置的种类、渲染与命令也写成扩展,排在最前面,与传入的走同一条路。

三层 ​

层类型在内置之上加的字段谁用
coreFormCoreExtensionid、kinds、documentProperties、controls、rules、issueTextvalidateDocument、createFormModel
运行时FormRuntimeExtensionfieldRenderers、containerRenderers、staticRenderers<xh-form-renderer>、createFormController
设计器FormDesignerExtensioneditors、commands、panels、paletteSources、settings、tourSteps、about、kindIcons、setup<xh-form-designer>

各层是超集:设计器层的扩展可以原样交给渲染器与 validateDocument,它们只读自己那一层的字段。业务页面只用渲染器时,扩展只写到运行时层,不会带进设计器的代码。

模板里用了扩展登记的种类,渲染器、设计器与 validateDocument 都要给同一组扩展,否则模板自检报 node.unknownKind。

一个例子 ​

下面的扩展新增「身份证号」字段,渲染沿用单行文本,规则提醒加上格式校验,工具栏多一条复制 JSON:

ts
export const idCard: FormDesignerExtension = {
  id: 'acme/id-card',
  kinds: [idCardKind],
  // 渲染器按种类名找渲染方式:身份证号就用单行文本的
  fieldRenderers: { 'id-card': FORM_FIELD_RENDERERS.text! },
  rules: [(document) => {
    const issues: FormSchemaIssue[] = []
    patternMissing(document.children, ['children'], issues)
    return issues
  }],
  issueText: issue => (issue.code === 'id-card.pattern-missing' ? '身份证号字段建议加上正则校验' : undefined),
  commands: [{
    id: 'acme/id-card.copy-json',
    label: '复制 JSON',
    placement: ['toolbar'],
    run: ({ designer }) => void navigator.clipboard.writeText(JSON.stringify(designer.schema, null, 2)),
  }],
}

种类 ​

kinds 里的每一项是一个种类定义。字段的定义:

ts
/** 身份证号:值是字符串,放在物料的「输入」分类里,属性沿用单行文本的基础与外观;显示名按语言自带。 */
const idCardKind: FieldKindDefinition = {
  type: 'field',
  kind: 'id-card',
  label: {
    'en-US': 'ID card number',
    'zh-CN': '身份证号',
    'zh-TW': '身分證號',
    'ja-JP': '身分証番号',
    'ko-KR': '신분증 번호',
    'de-DE': 'Ausweisnummer',
    'es-ES': 'Número de identidad',
    'fr-FR': 'Numéro d’identité',
    'pt-BR': 'Número de identidade',
    'ru-RU': 'Номер удостоверения личности',
  },
  category: 'input',
  element: 'xh-text-field',
  properties: { basic: text.properties.basic, data: [], appearance: text.properties.appearance },
  validations: ['required', 'pattern', 'expression'],
  valueKind: () => 'string',
  emptyValue: () => '',
}
字段说明
typefield、container 或 static
kind种类名,不能与内置的或别的扩展的重名
label可选,显示名,按语言各给一个,见名字
category物料面板里的分类,内置的有 input、choice、datetime、advanced、layout、data、static
element缺省渲染用的 XiHan.UI 元素标签
properties三组属性描述,属性面板按它们给出编辑方式,模板自检按它们检查取值;写法见种类与属性里内置种类的属性表
validations字段与容器适用的校验规则种类
valueKind / emptyValue字段的值形状与空值,可随 data 变化

容器另有 scope(list 时带数据键、值是行的数组,none 只管排版)、regionProperties、regions(区域个数的上下限与新建时的区域)、accepts(区域能放任意节点还是只放字段)与可选的 check(种类自己的结构检查)。

渲染 ​

渲染器按种类名找渲染方式。fieldRenderers 的值是一个函数:收到字段的上下文,建出控件元素,返回 { element, update?, destroy? };标签、提示与错误文案由渲染器外面的字段外壳给出,控件不必自带。上下文里有字段节点、数据路径、当前值、状态、错误文案、选项、文案,以及 setValue 与 commit,见渲染器参考。

内置的字段渲染表 FORM_FIELD_RENDERERS 也导出了,新种类的控件与某个内置种类一样时可以直接拿来用,例子里身份证号就用了单行文本的。渲染方式按种类名登记,不能与内置的重名,内置种类的渲染不能被替换。

模板里的种类没有渲染方式时,渲染器不渲染整张表单,派发 schema-error(XH_FORM_RT_NO_RENDERER)。

规则与问题说明 ​

rules 里的每一项是 (document, context) => 问题[],与内置检查一起跑,结果进 validateDocument 的返回值与设计器的问题列表。问题码必须以扩展 id 的末段加点开头,例子的 id 是 acme/id-card,问题码就是 id-card.…;问题可以写 severity: 'warning'。

ts
/** 身份证号字段没有正则校验时给一条警告;问题码以扩展 id 的末段 `id-card.` 开头。 */
function patternMissing(nodes: readonly FormNode[], path: JsonPath, out: FormSchemaIssue[]): void {
  nodes.forEach((node, index) => {
    const at = [...path, index]
    if (node.type === 'field' && node.kind === 'id-card' && !node.validation.some(rule => rule.kind === 'pattern'))
      out.push({ code: 'id-card.pattern-missing', path: at, target: { kind: 'node', id: node.id }, severity: 'warning' })
    if (node.type === 'container')
      node.regions.forEach((region, r) => patternMissing(region.children, [...at, 'regions', r, 'children'], out))
  })
}

issueText 给自己的问题码说明文字,设计器的问题列表显示它。

命令 ​

commands 里的命令与内置命令一样:{ id, label, shortLabel?, icon?, placement?, shortcut?, enabled?, checked?, tone?, run }。id 用扩展 id 加点加名字;placement 写 toolbar、menubar:file(菜单栏的「文件」菜单,另有 menubar:edit、menubar:view、menubar:help)、menu:node 或 selection:node(选中一个节点时浮在它右下角的选中工具条)等时出现在对应位置的缺省排列末尾(菜单栏里排在那张菜单收尾的一组之前);扩展还能用 menubarMenus 往菜单栏上加自己的菜单 { id, label, order? }(order 写在 30–80 之间,排在「视图」与「帮助」之间),命令写 menubar:<菜单 id> 落进去;宿主用 commandLayout 排列时按 id 引用;shortcut 是快捷键:带 Mod 的在整个设计器里认(焦点在文字输入里时除外),单键只在焦点在画布里时认,Mod 在 macOS 上是 ⌘、其余平台是 Ctrl;icon 取 @xihan-ui/icons 的图标,工具栏缺省只显示图标,没给图标的命令显示文字(shortLabel,没给就是 label);破坏性命令的 tone 写 danger。label、enabled 与 run 收到的上下文里有设计器实例 designer、设计器文案 translations,以及四款设计器共用的命令文案 commands(如 commands.undo)。

名字 ​

新种类在物料、画布、属性面板、状态栏与问题列表里的名字,写在种类的 label 上,按语言各给一个,键是 BCP 47 语言标记,与 XiHan.UI 内建语言包的语言一一对应(en-US、zh-CN……)。设计器按全局配置的 locale(与 XiHan.UI 的语言包合并后就有)取名:先找语言标记完全相同的,再找同一语种的(没写 zh-TW 时用 zh-CN),再用 en-US;都没有时显示种类的键。宿主不用再往文案里补这些名字;真要换个说法,给设计器元素的 translations 写 kind,它压在 label 之上。上面种类里的身份证号就给了十种语言的名字。

分类名取设计器文案的 category,属性名取 property,扩展新加的分类与属性的名字仍由宿主补进文案。

ts
const designer = document.createElement('xh-form-designer')
designer.extensions = [idCard]
designer.schema = schema

物料的「输入」分类末尾多了身份证号;点它放进画布,问题列表给出一条警告;工具栏末尾多了「复制 JSON」。

种类图标 ​

kindIcons 给本扩展登记的种类配图标,键是种类名,值取 @xihan-ui/icons 的图标;物料面板的条目与页面结构的行都用它。只能给自己 kinds 里的种类,给了别的种类时给 extensions 赋值就抛 TypeError。不给的种类只显示名字。内置种类的图标是包入口导出的 FORM_KIND_ICONS。

面板 ​

panels 在设计器外框里加自己的面板:region 给出区名(即面板 id)、缺省停的栏(start、end、bottom)与标题,给了 title 就画成卡片,面板头写标题,还可以给 icon、subtitle 与 help;defaultHidden 的面板缺省不显示。卡片和内置面板一样能挪动、浮出、收起与隐藏,写 movable、floatable、minimizable、closable 为 false 就不出对应的操作;每块面板自动有一条开关命令 panel:<区名>。区名与内置的区或别的扩展面板重名时,给 extensions 赋值就抛 DesignerExtensionError。面板元素用设计器零件时写 headless,标题交给卡片画。create({ designer }) 建出面板元素,可选的 update 在设计器状态变化后调用、destroy 在撤掉时调用。面板里的条目可以拖到画布上:拖动载荷写成 { kind } 是新建一个节点,写成 { clipboard: { nodes } } 是按粘贴的规则插入一段节点。面板与外框的约定见视图组件文档的 designer-shell。

物料来源 ​

paletteSources 往物料面板里加自己的几组条目,如一组常用的字段片段:每个来源是 { id, order?, groups(context), insert(item, context, at?) }。

  • groups 每次刷新现取,组接在内置分组后面,几个来源按 order 排(不写按 100)。组 id 与条目 id 在整个物料面板里不能重名,与内置的种类名也不能重。
  • 条目被点按(或回车)时调 insert(item, context);拖到画布上放下时调 insert(item, context, at),at 是放下处的 FormDropTarget({ parent, index? },parent 是区域 id,顶层为 null)。插入怎么做由来源定,比如按粘贴的规则插入一组节点:designer.paste(clipboard, { at }),不给 at 时插到选中节点之后。
  • 来源 id 与别的扩展的重名时,给 extensions 赋值就抛 DesignerExtensionError。

设置分区 ​

settings 给设置对话框加自己的分区:id、label、可选的 order 与条目 items,条目用属性描述的写法,写了 default 的是缺省值。用户的取值以「分区 id.条目 id」为键,命令、面板与扩展从上下文的 settings 读,没写的已经取了缺省值。分区 id 不能是内置的 general、language、layout、toolbar、help、shortcuts、about,也不能与别的扩展重名,见设置。

引导步骤 ​

tourSteps 往新手引导里加步骤,每一步是 { id, target?, title, description, placement?, order?, when? }:

  • id 用扩展 id 加点加名字。不能与表单的(form. 开头)或别的扩展的重名,也不能用 designer-shell 留着的 shell. 前缀,否则给 extensions 赋值就抛 DesignerExtensionError。
  • target 是一块面板 { panel, part? }、外框的一个区 { region }、工具栏上的一条命令 { command }、菜单栏上的一张菜单 { menubar: '<菜单 id>' }(如 file、view,或扩展登记的菜单)或任意选择器 { selector };不给就居中显示。目标面板隐藏、命令不在工具栏上、菜单栏上没有这张菜单时这一步跳过;窄屏时菜单栏收成「菜单」钮,菜单的目标落在那颗钮上。
  • title、description 可以写成收上下文的函数,按此刻的语言取文字。
  • order 定先后:designer-shell 的通用步骤是 0–60,收尾的设置一步是 900,表单的是 100–120,扩展从 200 起;同值按登记顺序。when 返回 false 时跳过这一步,如只读时不讲编辑。

引导怎么开始、怎么记下走完,见新手引导。

关于 ​

about 给设置对话框「关于」页加几行,如附加包的版本与授权:[{ label, value }],按扩展顺序接在表单自己那几行后面。

属性编辑器与编辑方式 ​

  • controls 登记新的编辑方式:键是属性描述里的 control.kind,值的 check(value) 返回问题码或 undefined,模板自检按它检查取值。文本、数字、开关、下拉、选项表 options、选项树 treeOptions 等是 designer-kit 的内建编辑方式。
  • editors 给属性面板换上自己的编辑器,可以按编辑方式、属性路径或「种类 + 路径」查找;路径带分组前缀,如 basic.label、data.options,数据键是 key;种类在节点上是种类名,区域是 region,表单设置是 settings。编辑器的写法见视图组件文档的 designer-kit。
  • documentProperties 给表单设置加属性,没有选中节点时属性面板编辑它们。

扩展数据 ​

扩展自己的数据放在文档或节点的 extensions 里,键是扩展 id,值是一个 JSON 对象,与业务属性分开。设计器的编辑操作 setExtensionData 写它,复制粘贴时原样带着走:

ts
designer.execute([{ type: 'setExtensionData', nodeId: 'f-name', id: 'acme/id-card', data: { masked: true } }])

冲突与装卸 ​

  • 种类名、渲染方式、编辑方式、命令 id、快捷键与面板区名在扩展之间不能重名。给 extensions 赋值时当场查重,冲突抛 DesignerExtensionError,什么都不变。
  • setup(context) 在设计器连上页面时调用,返回的函数在断开或换掉扩展时调用,适合挂监听。

Released under The MIT License