@schemx/vue
@schemx/vue
@schemx/vue 将 @schemx/core 的表单实例和 ViewSchemas 渲染为 Vue 3 组件树,包含 Group、Dependency 和 Dynamic 数组的递归渲染。它不绑定具体 UI 组件库,适合接入业务组件、设计系统或新的 UI adapter。
如果项目使用 Vant 或 Element Plus,可分别安装 @schemx/vant 或 @schemx/element-plus,直接使用已注册的表单 Renderer。
适用于使用自研组件或设计系统的 Vue 3 项目。该包提供表单组件、响应式 Hook 和 Renderer 注册机制,但不内置具体输入控件;每种 componentType 都需要由应用自行注册 Renderer。
安装与样式
pnpm add @schemx/vue vue
@schemx/core 是 @schemx/vue 的运行时依赖,会随安装自动安装;vue 是 peer dependency,业务项目需要显式安装。若业务代码直接导入 @schemx/core 或其子路径,也应在项目中显式声明该包。
ESM 入口 @schemx/vue 会通过入口模块自动加载基础样式,常规 Vite / Vue ESM 项目不需要再次导入 CSS。CommonJS 入口不会保留这条 CSS import;直接使用 CommonJS,或构建工具没有处理入口 CSS import 时,需要显式导入公开的样式子路径:
import "@schemx/vue/style.css"
Vue 包的样式只定义 Row/Col、Field、Group 和状态类的通用结构;移动端卡片、分隔线和桌面端间距由对应 UI 适配包补充。UI 适配包的手动样式应在 @schemx/vue/style.css 之后导入。
不要从 src/styles 或 dist 的内部路径导入样式;包的 exports 只公开根入口和 ./style.css。
快速开始
@schemx/vue 默认不提供具体输入控件。先注册 Renderer,再渲染表单:
<script setup lang="ts">
import { markRaw, ref } from "vue"
import Schemx, { rendererRegistry } from "@schemx/vue"
import type { SchemxField } from "@schemx/vue"
import InputRenderer from "./components/InputRenderer.vue"
type ProfileValues = {
nickname: string
}
rendererRegistry.register("input", markRaw(InputRenderer))
const formData = ref<ProfileValues>({
nickname: "",
})
const schemas: SchemxField<ProfileValues>[] = [
{
name: "nickname",
label: "昵称",
componentType: "input",
required: true,
placeholder: "请输入昵称",
},
]
</script>
<template>
<Schemx v-model="formData" :initial-values="formData" :schemas="schemas" />
</template>
这里同时传入 initialValues 是为了在 modelValue 为空时提供初始值。非空的 modelValue 会优先作为内部表单的初始快照;之后外部替换 modelValue 也会同步到内部表单。详见下一节的受控行为说明。
Schemx 组件
默认导出和命名导出 schemxForm 指向同一个可安装组件:
import Schemx, { schemxForm } from "@schemx/vue"
console.log(Schemx === schemxForm) // true
根入口当前没有名为 SchemxForm 的命名导出。可以自行给默认导入命名,但不要依赖 @schemx/vue/src/* 等深层路径。
Props
Schemx 的公开 Props 类型为 Vue 层的 SchemxFormProps<T>,由 Core 表单选项和 Vue 专属 Props 组合而成。
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
schemas |
SchemxSchemasInput<T> |
[] |
表单 Schema;可传字段数组或 createSchemas() 返回的 Schema source,运行时缺省为空数组 |
modelValue |
T |
{} |
v-model 的输入端;非空值参与初始快照,后续替换会同步到内部表单,字段变化会通过 update:modelValue 输出 |
initialValues |
T |
{} |
创建内部表单时的初始值,也是 reset() 的还原基准 |
form |
SchemxInstance<T> |
undefined |
外部表单实例;传入后组件不再创建实例,但仍会提供 Vue 上下文并同步 schemas;组件级回调不会写入该实例,v-model 仍监听实例变化 |
fieldRules |
SchemxFieldRulesMap<T> |
undefined |
按字段路径配置的字段规则兜底;字段自身 rules 或动态规则优先 |
rendererProps |
SchemxRendererPropsMap<T> |
undefined |
按 Renderer 类型设置静态默认 Props;字段 componentProps 与 Runtime 注入值优先 |
rendererRegistry |
RendererRegistry |
全局 rendererRegistry |
当前表单使用的 Renderer Registry |
defaultRendererType |
SchemxRendererKey |
undefined |
创建内部表单且未传 rendererRegistry 时的默认 Renderer 类型;Vue 全局 Registry 存在时由该 Registry 的 fallback 决定 |
presetRuleRegistry |
PresetRuleRegistry |
全局 presetRuleRegistry |
当前表单使用的预设规则 Registry |
validatorAdapters |
readonly ValidationAdapterOption[] |
[] |
当前表单使用的第三方校验 adapter;创建内部 Form 时参与配置合并并传递给 Core |
required |
RequiredConfig |
undefined |
表单级必填默认值;字段自身配置优先,普通 rules 不会推导必填或显示星号 |
readonly |
boolean |
undefined |
表单级只读默认值;字段自身配置优先 |
disabled |
boolean |
undefined |
表单级禁用默认值;字段自身配置优先 |
visible |
boolean |
true |
表单级可见性默认值;字段自身配置优先 |
labelIcon |
string | Component |
undefined |
表单级标签图标默认值;字符串传给 Icon Adapter,Component 直接渲染 |
iconComponent |
Component |
undefined |
当前 Form 使用的 Icon Adapter;优先级高于 ConfigProvider、App 和全局注册 |
labelAlign |
"left" | "center" | "right" |
undefined |
表单级标签对齐默认值;字段自身配置优先,均未配置时为 "right" |
labelPosition |
"left" | "top" | "right" |
undefined |
表单级标签位置默认值;字段自身配置优先,均未配置时为 "left" |
labelWidth |
string |
undefined |
表单级标签宽度默认值;字段自身配置优先,均未配置时为 "auto" |
contentAlign |
"left" | "center" | "right" |
undefined |
表单级内容区域对齐配置;字段自身配置优先,默认为 "left";只读字段自动为 "right" |
errorAlign |
"left" | "center" | "right" |
undefined |
表单级校验错误对齐配置;字段自身配置优先,均未配置时为 "left" |
validationTrigger |
ValidationTrigger | ValidationTrigger[] |
["blur", "change"] |
表单级校验触发方式默认值;字段自身配置优先 |
colon |
boolean |
undefined |
表单级标签冒号默认值;字段自身配置优先,均未配置时为 true |
onFinish |
(values: Readonly<T>) => void | Promise<void> |
undefined |
submit() 校验通过后的回调 Prop |
onFinishFailed |
(failure: ValidationFailure<T>) => void |
undefined |
submit() 校验失败后的回调 Prop |
onReset |
() => void |
undefined |
内部表单完整 reset() 完成后的回调;不在 resetFields() 后触发 |
onLoadingChange |
(loading: boolean) => void |
undefined |
内部表单提交开始和结束时的回调 |
onValuesChange |
(changedValues, latestSnapshot) => void |
undefined |
字段值变化后的回调 Prop |
onFieldsChange |
(changedFields, allFields) => void |
undefined |
字段路径变化后的回调 Prop |
onRuleError |
FormCallbackOptions<T>["onRuleError"] |
undefined |
规则解析异常回调;内部创建实例时当前不会透传,需在外部 form 实例上配置 |
lifecycleHooks |
FormLifecycleOptions["lifecycleHooks"] |
undefined |
Core Form 的 Runtime 生命周期钩子;当前 <Schemx> 创建内部实例时不会透传,需通过 useForm() 或 createForm() 配置。 |
schedulerOptions |
SchedulerOptions |
undefined |
Scheduler 时间片与 idle 队列配置 |
validationConcurrency |
number |
8 |
整表校验时同时运行的字段数 |
loading |
boolean |
undefined |
覆盖内置操作区显示的提交状态,不改变 Core 的真实提交状态 |
submitter / resetter |
boolean | SchemxFormActionConfig |
true |
控制内置提交 / 重置按钮;默认渲染,设为 false 隐藏,配置可设置 text 和 buttonProps |
class |
string |
"" |
添加到根 .schemx 元素的类名 |
style |
StyleValue |
{} |
绑定到根 .schemx 元素 |
row |
SchemxRowConfig |
undefined |
根级 Row 配置;Group/Dynamic 未显式配置时继承该默认值 |
col |
SchemxColConfig |
{ span: 24 } |
字段级默认 Col 配置;字段自身的 col 优先 |
上述 Schema 默认配置都会参与字段规范化:Core 负责 required、readonly、disabled、visible 和校验触发方式,Vue 适配层负责标签、内容对齐、冒号、必填标记和 schemaConfig.row 等布局展示默认值。schemaConfig.row 可作为根级、Group 和 Dynamic Row 的默认配置,Form 顶层 row 仍拥有更高优先级。Registry、默认 Renderer 和 validatorAdapters 则按表单显式配置 → 当前 App 安装配置 → Vue 模块级 Registry → Core 模块级配置 → Core 内置默认值解析。必填只能通过 required: true 或 RequiredOptions 表达;普通 rules 只负责执行校验,不会显示必填星号。传入 form 时,initialValues、Registry 和所有表单回调都由外部实例的创建者配置;组件不会用同名 Props 重建 Core Form,但会通过 Vue bridge 包装该实例,并同步 schemas 与组件声明的 schema 配置。
ConfigProvider 的 schemaConfig、row、colComponent 和 iconComponent 更新会同步到已挂载的内部 <Schemx>;Registry、校验 adapter 和默认 Renderer 类型仍在 Form 创建时解析。
Icon Adapter 只能通过 iconComponent 传入。Adapter 接收 { icon } Props,字符串的具体解释由业务或 UI 适配包决定;未配置 Adapter 时字符串回退为普通文本。
Row / Col 布局
Form 使用 row 配置根级 Row;Group 和 Dynamic Schema 使用 row 配置各自的子级 Row,Dynamic 会为每个数组 item 创建一行。Field 使用 col 配置自身的 Col:
row.gutter 支持 CSS padding shorthand:单个数字、[上下, 左右] 二元组或 [上, 右, 下, 左] 四元组。
const schemas = [
{
name: "title",
label: "标题",
componentType: "input",
col: { span: 12 },
},
{
label: "基本信息",
row: { gutter: 12 },
children: [
{
name: "description",
label: "描述",
componentType: "input",
col: { span: 8 },
},
],
},
]
旧的 layout 字段以及 SchemxLayout 类型进入弃用期。兼容期内 Field 的 col 优先于旧 layout;新代码应迁移到 row / col。
Col 组件的旧 component Prop 保留为弃用兼容入口;新代码请在 Form 或 ConfigProvider 配置 colComponent。
当未显式传入 rendererRegistry 时,Vue useForm() 会使用全局 Registry;该 Registry 默认以 input 作为 fallback。因此若要让 <Schemx :default-renderer-type="..."> 生效,应传入独立 Registry,或直接对 Vue 导出的全局 Registry 调用 setFallback()。传入外部 form 时,Renderer 配置由该实例决定。Row/Col 渲染器从 Form Context 读取 rowComponent / colComponent;底层 Row/Col 组件不再接收这两个组件 Props。
v-model、initialValues 与事件
initialValues 是内部 Store 的初始快照和 reset() 基准。modelValue 按 Vue 约定作为输入和输出:
- 创建内部实例时,非空
modelValue会整体作为内部表单初始值,优先于initialValues。 - 外部替换
modelValue后,组件会调用setFieldsValue()同步内部表单。 - 传入外部
form时,非空modelValue会在组件挂载时先写入该实例;空对象仍视为未提供。 - 内部或外部
form的字段变化都会发出update:modelValue,值为最新表单快照;同步来自modelValue的变化不会重复发出该事件。 - 外部
form上的组件级 Schema Prop 撤销后会回到 Core 默认值,不保证恢复外部实例挂载前的同名配置。 - 传入外部
form时,组件级onFinish、onFinishFailed、onValuesChange和onFieldsChange不会重新配置该实例;这些回调应在创建外部实例时配置。 - 传入外部
form时,onReset与onLoadingChange同样不会重新配置该实例;请在创建该实例时传入回调。 onFinish、onFinishFailed、onValuesChange和onFieldsChange是声明过的回调 Props,不在defineEmits的事件列表中。模板中的@finish等写法会按 Vue listener Prop 规则映射到这些 Props,但 TypeScript 用户更适合显式传回调。
内部表单模式会把 props.onFinish(values) 的返回值交给 Core,因此 submit() 会等待业务回调返回的 Promise;业务回调同步抛错或异步拒绝时,submit() 也会拒绝。传入外部 form 时组件不会重新配置这层回调,等待和错误传播完全取决于外部实例创建者;Core createForm() 同样会等待直接传入的 onFinish。
唯一通过 defineEmits 声明的组件事件如下:
| 事件 | 参数 | 触发时机 |
|---|---|---|
update:modelValue |
(value: T) |
内部表单的 onValuesChange 被调用后 |
如需程序化更新外部表单,可持有 ref 或外部 form 并显式调用 setFieldsValue();普通 v-model 已负责值的双向同步。
Slots
Schemx 把收到的所有 Slots 继续传给每个 Field 与 Group。嵌套字段名可直接作为 Slot 前缀,例如 user.nameLabel。
| Slot 名称 | Slot Props | 行为 |
|---|---|---|
{name} |
SchemxFieldSlotProps |
替换标签和控件主体;保留 wrapper、其 class/style、透传属性及 Before / Error / After 区域 |
{name}Label |
SchemxFieldSlotProps |
替换 schemx-field__label 内部内容,保留标签宽度与对齐;图标、必填标记和冒号由插槽自行处理 |
{name}Before |
SchemxFieldSlotProps |
渲染在整个字段主体前,与主体同级 |
{name}Content |
SchemxFieldContentSlotProps |
替换控件内容区域;额外提供默认 Renderer 的 columnElement |
{name}Error |
SchemxFieldErrorSlotProps |
替换 schemx-field__error 内部内容,保留错误样式与对齐;额外提供当前错误列表 errors |
{name}After |
SchemxFieldSlotProps |
渲染在错误区域后,与主体同级 |
{name}:{slotName} |
原样传给 Renderer | 去掉 {name}: 前缀后作为 Renderer 命名 Slot;同一数组路径的前缀为 user.name:,例如 user.name:prefix |
{groupKey}Header |
SchemxGroupSlotProps |
替换分组 Header 内部内容;保留默认的无障碍与折叠容器 |
{groupKey}Label |
SchemxGroupSlotProps |
替换默认分组标题;保留默认折叠箭头 |
{groupKey}Content |
SchemxGroupSlotProps |
替换分组 Body 内的子字段布局 |
submitter |
{ form, loading, disabled, submit } |
替换内置提交按钮。 |
resetter |
{ form, loading, disabled, reset } |
替换内置重置按钮。 |
字段名和 Label / Before / Content / After / Error 组合支持 camelCase 与 kebab-case 互查。子 Renderer Slot 的字段名前缀也支持这两种形式。
字段 Slot 的规范公共参数为 { schema, componentProps, value, field, form };不再把 Schema 或 Renderer Props 展开到 Slot 参数顶层。这样区域专属参数始终清晰可辨:Content 增加 columnElement,Error 增加 errors。这些公开类型均可从 @schemx/vue 导入。
字段顺序为 Before → 主体(Label + Content,或整体插槽)→ Error → After。整体插槽接管输入时,调用 componentProps.onChange(value) 和 componentProps.onBlur(value) 可复用字段回调与校验触发;只调用 field.setValue(value) 不会触发这些处理。Label 插槽已位于 <label> 内,无需再嵌套 <label>。Content 插槽位于 schemx-field__content 内,可用 <component :is="columnElement" /> 渲染默认控件。Error 插槽在无错误时也会调用,可根据 errors.length 控制内容。
<Schemx :schemas="schemas">
<template #nicknameLabel="{ schema }">
<strong>{{ schema.label }}</strong>
</template>
<template #nicknameBefore>
<small>昵称将展示在个人主页</small>
</template>
<template #nicknameAfter="{ value }">
<small>{{ String(value ?? "").length }}/20</small>
</template>
<template #nicknameError="{ errors }">
<small v-if="errors?.length">{{ errors.join(";") }}</small>
</template>
<template #nickname:prefix>
<span>@</span>
</template>
</Schemx>
组件 ref 暴露实例
组件通过 defineExpose 暴露传入或内部创建的完整 SchemxInstance<T>:
| 分类 | 成员 |
|---|---|
| 值 | getFieldValue、getFieldsValue、setFieldValue、setFieldsValue |
| 快照与初始值 | getFieldSnapshot、getFieldsSnapshot、getInitialValue、getInitialValues、setInitialValue、setInitialValues |
| touched | isFieldTouched、isFieldsTouched、setFieldTouched、setFieldsTouched、getTouchedFields |
| pending 与 loading | setFieldPending、setFieldsPending、isFieldPending、isFieldsPending、getPendingFields、isLoading |
| 错误 | getFieldErrors、getFieldsErrors、setFieldErrors、setFieldsErrors、clearFieldErrors、clearFieldsErrors、clearErrors |
| 定位、重置、校验与提交 | scrollToField(name, options?)、resetField、resetFields、reset、validateField、validate、submit |
| 响应与 Schema | effect、batch、setSchemas、updateSchemas、updateSchemaConfig、getViewSchemas、subscribeViewSchemas、waitForDependencies |
| Registry 与规则 | getRenderer、registerRenderer、hasRenderer、getPresetRule、registerPresetRule、hasPresetRule、setFieldRules、setFieldsRules、removeFieldRules、removeFieldsRules |
| 生命周期 | destroy |
scrollToField() 默认平滑滚动并将字段垂直居中;目标位于折叠 Group 时会依次展开祖先 Group。未渲染或不可见时返回 false;options 支持覆盖 behavior、block 和 inline。
<script setup lang="ts">
import { ref } from "vue"
import Schemx from "@schemx/vue"
import type { SchemxField, SchemxInstance, Values } from "@schemx/vue"
interface ProfileValues extends Values {
profile: {
nickname: string
}
}
const schemas: SchemxField<ProfileValues>[] = [
{
key: "profile",
label: "个人信息",
collapsible: true,
defaultCollapsed: true,
children: [
{
name: "profile.nickname",
label: "昵称",
componentType: "input",
},
],
},
]
const formRef = ref<SchemxInstance<ProfileValues>>()
async function scrollToNickname() {
await formRef.value?.scrollToField("profile.nickname", { block: "center" })
}
</script>
<template>
<Schemx ref="formRef" :schemas="schemas" />
<button type="button" @click="scrollToNickname">定位到分组中的昵称</button>
</template>
作为 Vue 插件安装
import { createApp } from "vue"
import Schemx from "@schemx/vue"
import App from "./App.vue"
createApp(App).use(Schemx).mount("#app")
app.use(Schemx) 会注册全局组件名 SchemxForm,并将安装选项保存为当前 Vue App 的默认配置:
app.use(Schemx, {
schemaConfig: { showRequiredMark: false },
defaultRendererType: "input",
rendererRegistry,
presetRuleRegistry,
validatorAdapters: [adapter],
})
安装配置属于当前 Vue App;不同 App 可以使用不同的 Registry、字段默认值、默认 renderer 类型和校验 adapter,适用于多应用和 SSR 隔离场景。配置优先级为表单显式配置、当前 App/Provider 配置、Vue/Core 模块级配置和 Core 内置默认值。UI 适配包导出的 Registry、布局组件和字段默认值需要通过 Form Props 或 ConfigProvider 显式传入。validatorAdapters 按该顺序合并,不同配置层的同 ID adapter 由高优先级配置覆盖,同一配置层内的重复 ID 仍需通过 { adapter, override: true } 显式覆盖。app.use() 不会调用 Core 的模块级 configureSchemx();该 API 仍可作为 Vue 与直接 createForm() 的业务基线。可安装组件还挂载了静态属性 Schemx.Field;Group 仅作为根入口命名导出,不是静态属性。
SchemxFormProps 和 Vue 层 FieldInstance 也会从 @schemx/vue 根入口导出;业务代码通常仍可直接从组件或 Hook 调用处推导类型,不需要依赖深层路径。
需要对组件树内多个表单统一设置展示默认值时,可以使用 ConfigProvider:
<script setup lang="ts">
import Schemx, { ConfigProvider } from "@schemx/vue"
</script>
<template>
<ConfigProvider :schema-config="{ readonly: true, showRequiredMark: false }">
<Schemx :schemas="schemas" />
</ConfigProvider>
</template>
ConfigProvider 不渲染额外 DOM;子级 Provider 的非空配置会覆盖父级同名配置。schemaConfig、row、rowComponent、colComponent 和 iconComponent 的变化会同步到已挂载的内部 <Schemx>,Registry、校验 adapter 和默认 Renderer 类型只在 Form 创建时解析。
Schema 写法
根入口从 @schemx/core 传递导出 SchemxField<T>。它是普通字段、分组、动态依赖子树和动态数组字段的联合类型。
普通字段
import type { SchemxField } from "@schemx/vue"
type Values = {
nickname: string
}
const schemas: SchemxField<Values>[] = [
{
name: "nickname",
label: "昵称",
componentType: "input",
initialValue: "",
placeholder: "请输入昵称",
required: true,
showRequiredMark: false,
validationTrigger: ["change", "blur"],
componentProps: {},
},
]
普通字段的真实字段如下:
| 字段 | 必填 | 说明 |
|---|---|---|
name |
是 | 字段路径,支持字符串和路径数组 |
label |
是 | 标签文本 |
componentType |
是 | Registry 中的 Renderer key |
dependencies |
否 | 基于其他字段动态覆盖展示属性的结构化依赖配置 |
componentProps |
否 | 透传给 Renderer 的专属 Props;框架注入项会覆盖同名值 |
placeholder |
否 | 占位提示 |
required、readonly、disabled、visible |
否 | 字段展示和交互状态;required 同时参与默认必填校验与星号默认展示 |
showRequiredMark |
否 | 只控制 label 前星号:未设置时等同于 Boolean(required);设为 true 可显示非必填字段星号,设为 false 可隐藏必填字段星号;禁用或只读时始终隐藏,且不会影响校验 |
initialValue |
否 | 字段挂载时的初始值和 reset() 还原值 |
preserve |
否 | Schema 移除或字段改名时是否保留字段值和状态;默认保留,设为 false 时清理旧路径 |
rules |
否 | 单条或多条校验规则,支持 Standard Schema 与内置规则名 |
labelIcon、labelAlign、labelPosition、labelWidth |
否 | 标签展示配置;labelIcon 支持字符串或 Vue Component |
contentAlign、errorAlign、colon |
否 | 内容、校验错误对齐和冒号配置 |
validationTrigger |
否 | change、blur 等校验触发时机 |
onChange、onBlur |
否 | 字段值变化和失焦时由 Vue Field 调用;Renderer 事件说明见后文 |
class、style |
否 | Vue 通过声明合并增加,运行时分别应用到字段容器的 class 和内联 style,发布根声明会自动带入该 augmentation |
key |
否 | 框架字段;业务方通常不要设置 |
showRequiredMark 是纯展示属性,不会改变 canVerified、校验触发时机或 required 规则。例如上例即使隐藏星号,required: true 仍会在 validationTrigger 指定的时机执行必填校验;反过来,仅设置 showRequiredMark: true 不会让字段变为必填。
分组
const group: SchemxField<Values> = {
key: "basic-info",
label: "基本信息",
readonly: false,
collapsible: true,
defaultCollapsed: false,
destroyOnCollapse: false,
children: [
{
name: "nickname",
label: "昵称",
componentType: "input",
},
],
}
分组字段支持 visible、readonly、disabled 和容器 dependencies。状态会递归传递给嵌套 Group、Dependency 和普通字段。它不是普通字段,没有 name、componentProps 或字段值。
折叠支持受控 collapsed、非受控 defaultCollapsed、onCollapsedChange 和 destroyOnCollapse。destroyOnCollapse=false 时仅隐藏 Body 并保留后代 Renderer 实例;默认值 true 保持原有卸载行为。折叠只影响展示,不改变字段可见性或校验状态。
通过表单实例调用 scrollToField() 时,若目标字段位于收起的 Group 中,会按外层到内层展开祖先分组,再滚动到字段。以下示例中点击按钮会展开“个人信息”并定位到昵称:
<script setup lang="ts">
import { ref } from "vue"
import Schemx from "@schemx/vue"
import type { SchemxField, SchemxInstance, Values } from "@schemx/vue"
interface ProfileValues extends Values {
profile: { nickname: string }
}
const formRef = ref<SchemxInstance<ProfileValues>>()
const schemas: SchemxField<ProfileValues>[] = [
{
key: "profile",
label: "个人信息",
collapsible: true,
defaultCollapsed: true,
children: [{ name: "profile.nickname", label: "昵称", componentType: "input" }],
},
]
</script>
<template>
<Schemx ref="formRef" :schemas="schemas" />
<button
type="button"
@click="formRef?.scrollToField('profile.nickname', { block: 'center' })"
>
定位到分组中的昵称
</button>
</template>
动态依赖子树
const dependency: SchemxField<Values> = {
to: ["nickname"],
dependencies: {
triggerFields: ["editable"],
readonly: (values) => !values.editable,
},
renderer: async (values, form, { abortSignal }) => {
if (!values.nickname || abortSignal.aborted) return []
return [
{
name: "signature",
label: "签名",
componentType: "input",
},
]
},
}
动态依赖节点还支持 visible、readonly、disabled 和容器 dependencies。to 与 renderer 负责动态子树结构,容器 dependencies 独立控制整棵子树的呈现状态。隐藏 Dependency 不会暂停 renderer,恢复可见后会展示最新子树。
renderer 可以同步或异步返回新的 SchemxField<T>[];context.abortSignal 用于取消已经过期的异步依赖计算。该 AbortSignal 属于 Core 的 Dependency renderer,和后文 useDictionary 的请求行为不同。
动态数组
Dynamic 数组字段使用必填的 key、数组路径 name 和行模板 item。item 中的普通字段和 Group 使用相对路径,Vue 会根据当前数组索引展开并递归渲染每一行;数组行增删或移动通过 Core Form 的 setFieldValue(path, updater) 完成。
import { useForm, type SchemxField } from "@schemx/vue"
type Member = {
name: string
role: "developer" | "designer"
}
type TeamValues = {
members: Member[]
}
const schemas: SchemxField<TeamValues>[] = [
{
key: "members",
name: "members",
label: "团队成员",
item: [
{
key: "member",
label: "成员信息",
children: [
{ name: "name", label: "姓名", componentType: "input" },
{ name: "role", label: "角色", componentType: "text" },
],
},
],
},
]
const form = useForm<TeamValues>({
initialValues: { members: [{ name: "张三", role: "developer" }] },
schemas,
})
form.setFieldValue("members", (members) => [
...(members ?? []),
{ name: "李四", role: "designer" },
])
Dynamic 容器的 visible、readonly、disabled 和 dependencies 会传递到每行后代字段。行内模板可以包含普通字段、Group 和 Dependency,但不能嵌套 Dynamic;行内 Dependency 的 to 使用完整表单路径,renderer 返回的子 Schema 使用当前行相对路径。Dynamic 组件是 Schemx 内部实现,不是根入口的独立导出。Dynamic 的每行子 Schema 统一由 SchemaList 回调渲染,以保持行 key、Col 和插槽透传一致。
Dictionary
SchemxDictionary<TValues, TResponse, TOption> 描述由函数加载的选项列表:TValues 是完整表单值类型,TResponse 是 api 的原始返回类型,TOption 是格式化后的选项类型。api 声明第三个参数时还会收到当前请求的 AbortSignal。
interface SchemxDictionary<T extends Values = Values, R = unknown, O = unknown> {
api: (values: T, form: SchemxInstance<T>, signal?: AbortSignal) => R | Promise<R>
formatter?: (res: Awaited<R>, form: SchemxInstance<T>) => O[] | Promise<O[]>
dependsOn?: NamePath<T>[]
shouldFetch?: (values: T) => boolean
immediate?: boolean
resetOnDepsChange?: boolean
retryCount?: number
retryInterval?: number
onError?: (error: Error, form: SchemxInstance<T>) => void
onSuccess?: (data: O[], form: SchemxInstance<T>) => void
onDepsChange?: (values: T, form: SchemxInstance<T>) => void
}
| 字段 | 默认值 | 真实行为 |
|---|---|---|
api |
必填 | 接收加载开始时的完整表单快照和实例;声明第三个参数时还会收到当前请求的 AbortSignal;支持同步或异步返回 |
formatter |
直接使用 api 结果 |
在请求成功后转换结果;支持异步,返回值应为选项数组 |
dependsOn |
undefined |
通过 useWatchFields 监听这些字段;变化时执行依赖回调、可选重置和重新加载 |
shouldFetch |
总是加载 | 每次加载前判断;返回 false 时跳过 api、清空 list 并结束加载状态 |
immediate |
true |
组件 onMounted 时是否自动调用一次 loadDict();不影响依赖变化或手动刷新 |
resetOnDepsChange |
false |
依赖变化时,如果 useDictionary 收到目标字段路径,调用 form.setFieldValue(fieldName, undefined) |
retryCount |
0 |
失败后的额外重试次数;总尝试次数为 retryCount + 1 |
retryInterval |
1000 |
两次尝试之间等待的毫秒数 |
onError |
无 | 最终失败后接收规范化的 Error 和表单实例 |
onSuccess |
无 | formatter 完成、list 写入后接收最终数组和表单实例 |
onDepsChange |
无 | 依赖变化后、shouldFetch 判断前调用 |
当前类型没有 data、list 或 options 形式的静态数组字段。同步 api 可以表达静态来源:
const staticDictionary: SchemxDictionary<ProfileValues, Option[], Option> = {
api: () => [
{ label: "公开", value: "public" },
{ label: "私密", value: "private" },
],
}
异步来源可以用 R 保留原始响应类型,并通过 formatter 转为选项数组:
type CityResponse = {
data: Array<{ id: number; name: string }>
}
const cityDictionary: SchemxDictionary<
ProfileValues,
CityResponse,
{ label: string; value: number }
> = {
api: async () => {
const response = await fetch("/api/cities")
return (await response.json()) as CityResponse
},
formatter: (response) =>
response.data.map((city) => ({
label: city.name,
value: city.id,
})),
}
依赖字段示例:
type AddressValues = {
province?: string
city?: string
}
const cityDictionary: SchemxDictionary<
AddressValues,
Array<{ id: string; name: string }>,
{ label: string; value: string }
> = {
api: (values) => fetchCities(values.province!),
formatter: (cities) => cities.map((city) => ({ label: city.name, value: city.id })),
dependsOn: ["province"],
shouldFetch: (values) => Boolean(values.province),
resetOnDepsChange: true,
immediate: false,
}
useDictionary
function useDictionary<
TValues extends Values = Values,
TName extends NamePath<TValues> = NamePath<TValues>,
TResponse = unknown,
TOption = unknown,
>(
options: SchemxDictionary<TValues, TResponse, TOption>,
fieldName?: TName
): UseDictionaryReturn<TOption>
useDictionary 必须在已经提供表单上下文的后代组件 setup() 中同步调用,例如 Schemx 的后代 Renderer。fieldName 只供 resetOnDepsChange 清空目标字段使用。通过 WithRemoteOptions 包装、且位于 Field 内的 Renderer 会自动从字段 Context 取得当前字段路径;直接调用 useDictionary() 或脱离 Field 使用 HOC 时,仍可显式传入该参数。
UseDictionaryReturn 的全部成员如下:
| 成员 | 类型 | 说明 |
|---|---|---|
list |
Ref<TOption[]> |
最终选项数组;初始值为 [] |
loading |
Ref<boolean> |
通过 shouldFetch 后开始请求时为 true,成功或失败后为 false |
error |
Ref<Error | undefined> |
新请求开始时清空;最终失败时写入规范化的 Error |
loadDict |
() => Promise<void> |
使用当前配置和当前表单值执行加载 |
refresh |
() => Promise<void> |
loadDict 的同语义包装;不会绕过 shouldFetch |
mutate |
(data: TOption[]) => void |
直接替换 list,不调用 api,也不修改 loading / error |
成功路径为 api(含重试)→ formatter → 写入 list → onSuccess。shouldFetch、api、formatter 或 onSuccess 抛错时都会进入 loadDict() 的错误路径:抛出值经 normalizeError 转为 Error,然后写入 error、清空 list、结束 loading 并调用 onError。通常这些错误会被处理,loadDict() resolve;但 onError 本身若抛错,该异常位于 catch 块内,没有第二层捕获,loadDict() / refresh() 返回的 Promise 会 reject。
依赖监听中的 onDepsChange 在调用 loadDict() 之前执行,不在其 try...catch 内。onDepsChange 自身抛错时会直接从 Watch 回调抛出,并阻止本轮重置字段及重新加载;它不会更新 useDictionary.error,也不会调用 onError。
每次 loadDict() 都会递增请求计数并中止上一次仍在进行的请求。api 声明第三个参数时会收到当前请求的 AbortSignal;重试等待也可被中止。过期请求的成功结果、异步 formatter 结果和错误都不会写入当前状态;shouldFetch 返回 false 时会清空 list 并结束 loading。底层 API 是否真正停止仍取决于它是否使用 AbortSignal,不要把请求中止等同于业务服务端已经取消处理。
自定义 Renderer
Renderer 是从 Registry 取出的普通 Vue 组件。Field 先展开 schema.componentProps,再注入或覆盖以下 Props:
| Prop / listener | 实际值与行为 |
|---|---|
value |
当前字段值 |
onUpdate:value(value) |
直接调用 field.setValue(value);不在这里触发字段级校验 |
onChange(value) |
写入字段值,调用 componentProps.onChange(value) 和 Schema 顶层 onChange(value),并按 validationTrigger 执行校验 |
onBlur() |
不写值,调用 componentProps.onBlur(value) 和 Schema 顶层 onBlur(form),并按 validationTrigger 执行校验 |
readonly |
当前 ViewSchema 已解析的只读状态,覆盖 componentProps.readonly |
disabled |
当前 ViewSchema 已解析的禁用状态,覆盖 componentProps.disabled |
placeholder |
当前 ViewSchema 的占位文本,覆盖 componentProps.placeholder |
formItemProps |
当前完整 ViewSchema,覆盖 componentProps.formItemProps |
其他 componentProps(例如 options、readonlyPlaceholder 和已经通过声明合并注册的组件专属 Props)原样透传。Vue Field 会自动注入当前 Core Form 实例到 formInstance,并用完整 ViewSchema 覆盖 formItemProps。
当前也不会向普通 Renderer 自动注入 fieldName、字段校验 error / errors 或 loading。Renderer 如需当前字段路径或响应式状态,可调用 useFieldContext();WithRemoteOptions 会自动使用该 Context 的字段路径处理 resetOnDepsChange,但不会把内部路径透传给被包装 Renderer。详见下一节。
注意:componentProps.onChange、componentProps.onBlur 与 Schema 顶层的 onChange、onBlur 都会在 Vue Field 的对应回调中调用。若只希望自定义 Renderer 行为,可把回调放在 componentProps 内;Schema 顶层回调分别收到字段值和表单实例。
<script setup lang="ts">
defineProps<{
value?: string
readonly?: boolean
disabled?: boolean
placeholder?: string
formItemProps?: { name?: string | string[] }
}>()
const emit = defineEmits<{
"update:value": [value: string]
change: [value: string]
blur: []
}>()
</script>
<template>
<input
:value="value"
:readonly="readonly"
:disabled="disabled"
:placeholder="placeholder"
@input="emit('update:value', ($event.target as HTMLInputElement).value)"
@change="emit('change', ($event.target as HTMLInputElement).value)"
@blur="emit('blur')"
/>
</template>
update:value 和 change 都能写入 Store;需要 change 校验和 componentProps.onChange 回调时,应发出 change。blur 的参数会被忽略。
全局与表单独立 Registry
全局 Registry 会作为 useForm() 的默认值:
import { markRaw } from "vue"
import { rendererRegistry } from "@schemx/vue"
import InputRenderer from "./InputRenderer.vue"
rendererRegistry.register("input", markRaw(InputRenderer))
通过 app.use(Schemx, options) 提供的 Registry 只对当前 Vue App 生效,并优先于上面的 Vue 模块级 Registry;表单 Props 或 useForm() 传入独立 Registry 时优先级最高。
也可以使用根入口从 core 传递导出的 createRendererRegistry 创建表单独立 Registry,避免修改全局状态:
<script setup lang="ts">
import { markRaw } from "vue"
import Schemx, { createRendererRegistry } from "@schemx/vue"
import type { SchemxField, Values } from "@schemx/vue"
import InputRenderer from "./InputRenderer.vue"
interface ProfileValues extends Values {
nickname: string
}
const localRegistry = createRendererRegistry("input")
localRegistry.register("input", markRaw(InputRenderer))
const schemas: SchemxField<ProfileValues>[] = [
{
name: "nickname",
label: "昵称",
componentType: "input",
},
]
</script>
<template>
<Schemx :schemas="schemas" :renderer-registry="localRegistry" />
</template>
WithRemoteOptions
SchemxWithDictionary<A, T> 是公开的类型工具:
type SchemxWithDictionary<A, T extends Values = Values> = A & {
dict?: SchemxDictionary<T> | SchemxDictionary<T>["api"]
}
它只给原 Props 类型 A 增加可选的 dict;不会自动增加 fieldName、options 或 loading。
WithRemoteOptions(WrappedComponent) 返回一个增强组件。增强组件声明并消费:
dict?: SchemxDictionary | SchemxDictionary["api"]:存在时规范化后调用useDictionary(dict, fieldName)。fieldName?: NamePath:仅作为兼容回退。HOC 位于Field内时,默认从useFieldContext().name自动取得当前字段路径;显式值只用于脱离Field的独立使用。当前实现会把该未声明属性随 attrs 一并传给被包装组件。
被包装组件实际收到所有其余 attrs、规范化后的 dict,以及 HOC 决定的 options 和 loading;函数简写会在这里转换为 { api } 对象:
- 有
dict时,Dictionary 的list和loading无条件覆盖 attrs 中的静态options、loading,即使列表仍为空。 - 没有
dict时,原 attrs 的options、loading原样保留。 - HOC 不注入
error、refresh、loadDict或mutate。 - 有
dict时内部会调用useFormContext(),所以组件必须位于已提供表单上下文的后代树中。
下面包装一个自定义 Select Renderer,并让依赖字段变化时重置当前字段:
<!-- SelectRenderer.vue -->
<script setup lang="ts">
type Option = { label: string; value: string }
defineProps<{
value?: string
options?: Option[]
loading?: boolean
disabled?: boolean
}>()
const emit = defineEmits<{
"update:value": [value: string]
change: [value: string]
}>()
</script>
<template>
<select
:value="value"
:disabled="disabled || loading"
@change="emit('change', ($event.target as HTMLSelectElement).value)"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</template>
import { markRaw } from "vue"
import { rendererRegistry, WithRemoteOptions } from "@schemx/vue"
import type {
NamePath,
SchemxDictionary,
SchemxField,
SchemxWithDictionary,
Values,
} from "@schemx/vue"
import SelectRenderer from "./SelectRenderer.vue"
type AddressValues = {
province?: string
city?: string
}
type Option = { label: string; value: string }
type City = { id: string; name: string }
// 可替换为真实请求;该最小实现让示例可以直接通过严格类型检查。
async function fetchCities(province: string): Promise<City[]> {
return province ? [{ id: "guangzhou", name: "广州" }] : []
}
const cityDictionary: SchemxDictionary<AddressValues, City[]> = {
api: (values) => fetchCities(values.province!),
formatter: (cities) => cities.map((city) => ({ label: city.name, value: city.id })),
dependsOn: ["province"],
shouldFetch: (values) => Boolean(values.province),
resetOnDepsChange: true,
}
declare module "@schemx/core" {
interface SchemxRendererDefinition<T extends Values> {
"remote-select": SchemxWithDictionary<
{
options?: Option[]
loading?: boolean
},
T
>
}
}
const RemoteSelect = WithRemoteOptions(SelectRenderer)
rendererRegistry.register("remote-select", markRaw(RemoteSelect))
const schemas: SchemxField<AddressValues>[] = [
{
name: "city",
label: "城市",
componentType: "remote-select",
componentProps: {
dict: cityDictionary,
},
},
]
若只需要静态选项,不传 dict 即可;options 会作为普通 componentProps 透传:
componentProps: {
options: [
{ label: "公开", value: "public" },
{ label: "私密", value: "private" },
],
}
Composition API
| API | 说明 |
|---|---|
useForm() |
创建由当前 Vue effect scope 管理生命周期的表单实例;自动使用 Vue 包全局 Registry,但不会自动 provide |
provideFormContext() |
在当前组件 setup() 同步阶段一次向后代提供表单实例和展示配置 |
createFormContext() |
兼容 API:仅提供表单实例;建议改用 provideFormContext() |
useFormContext() |
获取最近祖先提供的表单实例;缺少上下文时抛错 |
useField() |
创建字段级读写、校验和状态控制器 |
createFieldContext() |
向后代提供字段控制器 |
useFieldContext() |
获取当前字段控制器 |
createFormConfigContext() |
兼容 API:仅提供表单展示配置;建议改用 provideFormContext() |
useFormConfigContext() |
获取上层表单展示配置 |
createConfigProviderContext() |
创建组件树级 ConfigProvider 上下文;通常由 <ConfigProvider> 内部调用。 |
useConfigProviderContext() |
获取当前 ConfigProvider 配置;没有 Provider 时返回 undefined。 |
useConfigProviderContextRef() |
获取当前 ConfigProvider 的响应式配置引用;没有 Provider 时返回 undefined。 |
useWatch() |
按单字段、多字段或全表签名监听变化 |
useWatchField() |
监听单个字段 |
useWatchFields() |
监听多个字段 |
useWatchAll() |
监听整张表单 |
useDictionary() |
管理依赖字段的函数式选项来源 |
useStableRef() |
创建引用保持稳定的 shallowRef |
useViewSchemas() |
把 subscribeViewSchemas() 桥接为 Vue shallowRef |
useFormSelector() |
从共享表单值 Runtime 派生只读 Vue Ref |
useForm() 只负责创建和销毁实例。如需让自定义组件树中的 useField()、useDictionary() 或 Field 找到实例,应在 setup() 的同步调用栈中调用 provideFormContext({ form, schemaConfig }),一次提供表单实例和展示配置。createFormContext() 与 createFormConfigContext() 仍保留用于兼容旧代码。直接使用 <Schemx> 时,统一 Form Context 已由组件提供。
通用调用规则
依赖 Vue 生命周期或 provide / inject 的 Hook 应在 setup() 期间同步调用。直接使用 <Schemx> 时,组件已提供表单实例与展示配置;字段 Renderer 还会获得当前字段 Context。
useForm
function useForm<TValues extends Values = Values>(
options?: CreateFormOptions<TValues, NamePath<TValues>>
): SchemxInstance<TValues>
options 可选,未传 Registry 时使用 Vue 全局 rendererRegistry 和 presetRuleRegistry。函数同步返回 SchemxInstance<TValues>,其中 getFieldValue()、getFieldErrors()、isFieldTouched()、isFieldPending()、getFieldsValue() 和聚合状态读取可被 Vue effect 追踪。useForm() 不读取 Context,也不会自动 provide;当前 Vue effect scope 销毁时会释放 Runtime 并销毁 Form。非 Vue scope 场景应改用 Core createForm() 并自行销毁。
import { provideFormContext, useForm } from "@schemx/vue"
type ProfileValues = { nickname: string }
const form = useForm<ProfileValues>({ initialValues: { nickname: "" } })
// 需要让后代 Hook 读取时,同时提供表单实例和展示配置。
provideFormContext({
form,
schemaConfig: { readonly: false, validationTrigger: "blur" },
})
useForm() 除了补全 Vue 全局 Registry 外,会把 Core options 原样传给 createForm()。因此直接调用时,initialValues 会按 Core 规则形成初始快照,submit() 也会等待直接传入的 onFinish Promise。前文 modelValue 的初始化与同步行为只属于 <Schemx> 内部创建实例时的 Props 转换,不属于 useForm() 本身。defaultRendererType 仍会受全局 Renderer Registry 已被补全的影响,见前文说明。
Vue Instance 与共享 Runtime
同一 Core Form 始终复用一个 Instance 和一个内部 Runtime;响应式资源按需创建,并在存活期间由各个 Vue owner 共享。Instance 的写入、校验、提交、Schema 与 Registry 方法都委托原始 Core Form;仅常用状态读取会额外建立 Vue 依赖。getFieldsValue() 无参数时依赖全表值,传入路径时仅依赖这些字段。getFieldSnapshot()、getFieldsSnapshot()、getInitialValue() 与 getInitialValues() 保持 Core 的无依赖快照语义。
useFormSelector() 和 useField() 复用这个 Runtime;前者返回 selector 结果的只读 Ref,后者复用字段 value、errors、touched 和 pending Ref。Runtime 通过 @schemx/core/adapter 的 createFormStateAdapter() 消费 Core SnapshotSource。最后一个 Vue owner 释放或手动调用 form.destroy() 后,Runtime 会停止订阅并释放这些快照来源。
Context API
Form、Field 和 ConfigProvider 是 3 组 Context。Form 与 Field 的 Reader 要求对应 Context 存在;ConfigProvider Reader 在没有 Provider 时返回 undefined。provideFormContext() 和 create*Context() 必须在 Provider 组件的 setup() 同步阶段调用,且只对后代可见;use*Context() 仅读取最近祖先提供的值。
function provideFormContext<TValues extends Values = Values>(
context: FormContextValue<TValues>
): SchemxInstance<TValues>
function useFormContextValue<TValues extends Values = Values>(): FormContextValue<TValues>
function useFormContext<TValues extends Values = Values>(): SchemxInstance<TValues>
function createFormContext<TValues extends Values = Values>(
instance: SchemxInstance<TValues>
): SchemxInstance<TValues>
function createFieldContext<TValues extends Values = Values>(
field: FieldInstance<TValues>
): void
function useFieldContext(): FieldInstance<Values>
function createFormConfigContext(props: FormConfigContextValue): void
function useFormConfigContext(): FormConfigContextValue
function createConfigProviderContext(config: ComputedRef<SchemxConfig>): void
function useConfigProviderContext(): SchemxConfig | undefined
function useConfigProviderContextRef(): ComputedRef<SchemxConfig> | undefined
上面签名中的 FieldInstance 已从 @schemx/vue 根入口导出。createFieldContext() 当前只有 TValues extends Values = Values,没有独立的 TName 泛型;field 必须是 useField() 返回的 Vue 字段控制器,字段路径已由创建该返回值时的 name 捕获。调用方可以显式导入 FieldInstance,也可以依靠 useField() 推导参数。
| Provider 签名 | Reader 签名 | 职责、所有权与缺失行为 |
|---|---|---|
provideFormContext<T>({ form, schemaConfig }) |
useFormContextValue<T>(): FormContextValue<T> |
一次提供响应式表单实例和展示配置;Provider 只持有 Runtime,不销毁外部 Core Form。 |
provideFormContext<T>({ form, schemaConfig }) |
useFormContext<T>(): SchemxInstance<T> |
只读取统一 Context 中的表单实例;缺失时 Reader 抛出 Error。 |
createFormContext<T>(form)(兼容) |
useFormContext<T>(): SchemxInstance<T> |
仅提供表单实例的旧 API;建议迁移到 provideFormContext()。 |
createFieldContext<TValues extends Values = Values>(field): void |
useFieldContext(): FieldInstance<Values> |
提供 / 读取 useField() 返回的字段控制器。Provider 不创建字段、不接管订阅;缺失时 Reader 抛出 Error。 |
createFormConfigContext(props)(兼容) |
useFormConfigContext(): FormConfigContextValue |
仅提供 / 读取表单展示配置的旧 API;建议迁移到 provideFormContext()。 |
import { provideFormContext, useForm, type SchemxField } from "@schemx/vue"
type ProfileValues = { nickname: string }
const schemas: SchemxField<ProfileValues>[] = [
{ name: "nickname", label: "昵称", componentType: "input" },
]
const form = useForm<ProfileValues>({ schemas })
provideFormContext({
form,
schemaConfig: {
readonly: false,
validationTrigger: "blur",
},
})
在后代组件的 setup() 中只读取所需 Context:
import { useFieldContext, useFormContextValue } from "@schemx/vue"
type ProfileValues = { nickname: string }
const { form, schemaConfig } = useFormContextValue<ProfileValues>()
const field = useFieldContext()
useField
function useField<TValues extends Values = Values>(
name: NamePath<TValues>
): FieldInstance<TValues>
Hook 从 useFormContext() 取得 Instance,为 name 创建 Core 字段控制器,并复用共享 Runtime 中的字段状态。它保留 Core SchemxFieldInstance 的全部成员,并增加 value: Ref<FieldValue<...> | undefined>、errors: ComputedRef<readonly string[]>、dirty: ComputedRef<boolean> 与 pending: ComputedRef<boolean>;dirty 读取字段 touched Ref。getValue()、getErrors()、isTouched() 和 isPending() 都读取对应 Vue Ref,getValues() 则通过 Instance 读取全表值。只有 value 可通过 .value 写入,其余三个 computed 状态只读。Core 方法的完整签名见 Core Schema source、字段与监听。
useField() 不缓存字段控制器对象;每次调用只创建轻量的 Core 控制器包装,并复用当前 Runtime 中按字段 SnapshotSource 缓存的字段 Ref。Runtime 由 useForm()、Form Context 和 useFormSelector() 等 Vue owner 的引用计数持有,最后一个 owner 释放或手动销毁后停止订阅。FieldInstance 可从 @schemx/vue 根入口导入;不需要依赖深层路径。
import { createFieldContext, useField } from "@schemx/vue"
type ProfileValues = { nickname: string }
const nickname = useField<ProfileValues>("nickname")
createFieldContext(nickname)
nickname.setValue("Schemx")
await nickname.validate()
console.log(nickname.value.value, nickname.errors.value)
createFieldContext(field) 将这一返回值提供给后代并返回 void;useFieldContext() 无参数,返回最近祖先提供的同一控制器。它们不会替代表单 Context。
FormContextValue 与 FormConfigContextValue 的边界
FormContextValue<T> 同时包含 form 和展示配置,FormConfigContextValue 只包含展示配置。两者都不包含 schemas、initialValues、回调、class、style、Registry 或 defaultRendererType。组件 Props 中的平铺 Schema 字段会在 <Schemx> 内部聚合为 schemaConfig,再传给 Form 实例和字段展示 Context。
useWatch、useWatchField、useWatchFields 与 useWatchAll
function useWatch<T extends Values>(
callback: WatchAllCallback<T>,
options?: CreateWatchOptions
): () => void
function useWatch<T extends Values>(
name: NamePath<T>,
callback: WatchFieldCallback<T>,
options?: CreateWatchOptions
): () => void
function useWatch<T extends Values>(
names: NamePath<T>[],
callback: WatchFieldsCallback<T>,
options?: CreateWatchOptions
): () => void
function useWatchField<T extends Values = Values>(
name: NamePath<T>,
callback: WatchFieldCallback<T>,
options?: CreateWatchOptions
): () => void
function useWatchFields<T extends Values>(
names: NamePath<T>[],
callback: WatchFieldsCallback<T>,
options?: CreateWatchOptions
): () => void
function useWatchAll<T extends Values = Values>(
callback: WatchAllCallback<T>,
options?: CreateWatchOptions
): () => void
useWatch 按参数形状分发,3 个语义化函数分别对应单字段、多字段和全表重载。它们都从 useFormContext() 读取表单,返回可手动调用的取消函数,并在组件卸载时自动取消。options.immediate 和 options.inequality 默认均为 false;后者使用深比较跳过相等值。
| 模式 | callback 第 1 个参数 | callback 第 2 个参数 |
|---|---|---|
| 单字段 | 变化后的全表快照。 | { value, prevValue };immediate 首次的 prevValue 为 undefined。 |
| 多字段 | 变化后的全表快照。 | { changedPaths, changedValues, prevValues };immediate 时路径为全部传入路径、prevValues 为 {}。 |
| 全表 | 变化后的全表快照。 | { changedPaths, changedValues, prevValues };immediate 时 changedPaths 为 []。 |
useWatchAll() 与 useWatch(callback) 会在 effect 中读取可追踪的全表值,因此会在字段变化时执行;immediate: true 还会在创建监听时同步执行 1 次。需要减少监听范围时可使用 useWatchField() 或 useWatchFields()。
import { useWatch, useWatchAll, useWatchField, useWatchFields } from "@schemx/vue"
type ProfileValues = { firstName: string; lastName: string }
useWatch<ProfileValues>("firstName", (_snapshot, { value, prevValue }) => {
console.log(prevValue, value)
})
useWatchField<ProfileValues>(
"firstName",
(snapshot, { value, prevValue }) => console.log(prevValue, value, snapshot),
{ immediate: true, inequality: true }
)
const stop = useWatchFields<ProfileValues>(
["firstName", "lastName"],
(_snapshot, payload) => {
console.log(payload.changedPaths, payload.changedValues)
}
)
useWatchAll<ProfileValues>(
(snapshot, payload) => {
console.log(payload.changedPaths, snapshot)
},
{ immediate: true }
)
stop()
useDictionary 摘要
签名为 useDictionary<TValues, TName, TResponse, TOption>(options, fieldName?): UseDictionaryReturn<TOption>。它通过 useFormContext() 取得表单,在 onMounted 时按 immediate 决定是否加载,并通过 useWatchFields() 自动清理依赖订阅。完整参数、返回值、竞态边界与示例见前文 useDictionary,这里不重复维护第 2 份定义。
useStableRef
function useStableRef<T extends Record<string, any>>(
factory: () => T
): Readonly<ShallowRef<T>>
Hook 在 Vue watchEffect 中执行 factory 并追踪 Vue 依赖。只有工厂结果与当前值浅比较不等时才替换 Ref 引用。它不使用 Schemx Context,watchEffect 由当前 Vue scope 管理。
import { computed } from "vue"
import { useStableRef } from "@schemx/vue"
const disabled = computed(() => false)
const stableProps = useStableRef(() => ({ disabled: disabled.value }))
useViewSchemas
function useViewSchemas<T extends Values = Values>(
form: SchemxInstance<T>
): ShallowRef<readonly SchemxViewSchema<T>[]>
Hook 不读取 Context,必须显式传入表单实例。它以 form.getViewSchemas() 初始化 shallowRef,通过 form.subscribeViewSchemas() 更新;当前 Vue effect scope 销毁时自动 unsubscribe。
import { useForm, useViewSchemas } from "@schemx/vue"
const form = useForm({
schemas: [{ name: "nickname", label: "昵称", componentType: "input" }],
})
const viewSchemas = useViewSchemas(form)
console.log(viewSchemas.value)
Field 与 Group
Field
Field 的必填 Prop 是 schema,还支持用于包装器的 class 和 style:
| Prop | 类型 | 说明 |
|---|---|---|
schema |
SchemxViewFieldSchema<T> |
Core 已解析完成的字段 ViewSchema,不是原始 SchemxField<T> |
class |
ClassValue |
追加到字段外层 wrapper 的 class。 |
style |
StyleValue |
追加到字段外层 wrapper 的内联 style。 |
Field 只处理普通字段 ViewSchema:它创建字段控制器、提供字段上下文、查找 Renderer,并处理标签、内容、错误、校验触发和可见性。Schemx 会在根级 ViewSchema 循环中直接区分并渲染 Field 或 Group。
它支持与 Schemx 一致的动态 Slots:{name}、{name}Label、{name}Before、{name}Content、{name}After、{name}Error 和 {name}:{slotName}。完整 Slot Props 见 Schemx 组件的 Slots。
Field 不能脱离上下文单独工作:普通字段至少需要祖先同步提供表单实例和展示配置,通常直接放在 <Schemx> 内部。若自定义 adapter 直接使用它,应调用 provideFormContext({ form, schemaConfig })。传给它的 Schema 应来自 form.getViewSchemas() 或 useViewSchemas(form),不要把尚未编译的 dependency Schema 直接传入。
Group
Group 的必填 Prop 是 schema,还支持外层样式和递归渲染回调:
| Prop | 类型 | 说明 |
|---|---|---|
schema |
SchemxViewGroupSchema<T> |
包含已解析 children 的分组 ViewSchema |
class |
ClassValue |
追加到 Group 外层 wrapper 的 class。 |
style |
StyleValue |
追加到 Group 外层 wrapper 的内联 style。 |
renderChildren |
(schemas, rowConfig?) => VNodeChild |
可选;<Schemx> 由 SchemaList 提供,直接挂载 Group 时省略则使用兼容递归渲染。 |
它会把收到的全部 Slots 原样传给子级 Field 或嵌套 Group,并支持 {groupKey}Header、{groupKey}Label、{groupKey}Content 三个分组自身 Slot。Header 替换 Header 内部内容,Label 替换标题,Content 替换 Body 内的默认子字段布局;三者都接收 schema、collapsed、collapsible、disabled、readonly 和 toggle。collapsible 控制标题是否可点击和通过 Enter / Space 切换;disabled 状态禁止折叠交互,readonly 状态仍允许浏览和折叠。组件支持受控与非受控折叠;从受控切换为非受控时会延续最后一次受控值。destroyOnCollapse 控制子级是否卸载,ARIA 关联 ID 优先使用 Core Node ID,直接挂载组件时回退到 Vue 实例 ID,避免规范化后相同 key 发生冲突。
label 为空时不渲染标题。Vue 源码的 types/index.ts 通过声明合并为分组 Schema 增加 class 和 style,发布声明入口会加载该增强;运行时将二者绑定到 .schemx-group-wrapper 外层容器。子级字段仍要求表单实例和展示配置上下文。
Field 和 Group 是构建自定义 Vue adapter 或重排 ViewSchemas 时的底层组件。一般业务表单优先使用 <Schemx>;只有在需要自定义整体布局、分区或容器时才直接组合这两个组件。由 <Schemx> 渲染时,SchemaList 会传入 renderChildren 来统一处理子级布局;直接挂载 Group 时也可省略该回调,组件会递归渲染子级作为兼容默认行为。需要自定义子级布局时可传入自己的回调。
class 和 style 支持多来源合并。Schemx 根节点使用内部 schemx class 与组件 Props;Field 外层容器按“内部 class → 父级传入 class/style → Schema 的 class/style”合并;Group 外层容器按同样顺序合并。class 会全部保留,style 后面的来源覆盖前面的同名属性。Field Schema 样式作用于 .schemx-field-wrapper,Group Schema 样式作用于 .schemx-group-wrapper。
Registry
Vue 包保留 2 个模块级 fallback Registry;通过 app.use() 使用适配包时,会为每个 Vue App
创建独立的配置 Registry 和适配包 Scope。
| 导出 | 真实类型 | 初始内容 | 与 useForm() 的关系 |
|---|---|---|---|
rendererRegistry |
RendererRegistry |
空 Registry,默认 renderer key 预设为 "input";适配包直接挂载时使用 fallback。 |
未传 options.rendererRegistry 且没有 App Scope 时使用该实例。 |
presetRuleRegistry |
PresetRuleRegistry |
模块初始化时包含内置 required,并可继续注册业务预设规则。 |
未传 options.presetRuleRegistry 时使用该单例。 |
Renderer Registry 包含 register、registerAll、get、resolve、has、unregister、keys、setFallback、getFallback、clear 和 size;PresetRuleRegistry 包含 register、registerAll、get、resolve、has、unregister、keys、clear 和 size。完整签名见 Core 的 校验 与 Renderer Registry 章节。
import { markRaw } from "vue"
import { rendererRegistry, presetRuleRegistry, type StandardSchemaV1 } from "@schemx/vue"
import InputRenderer from "./InputRenderer.vue"
rendererRegistry.register("input", markRaw(InputRenderer))
const phoneRule: StandardSchemaV1<string> = {
"~standard": {
version: 1,
vendor: "app",
validate(value) {
return typeof value === "string" && /^1\d{10}$/.test(value)
? { value }
: { issues: [{ message: "手机号格式错误" }] }
},
},
}
presetRuleRegistry.register("phone", phoneRule)
全局 Registry 由所有表单共享。表单独立 Registry 则由 Core 工厂新建,并通过 useForm() 选项或 <Schemx> Props 传入:
import { createRendererRegistry, createPresetRuleRegistry, useForm } from "@schemx/vue"
const renderers = createRendererRegistry("input")
const validators = createPresetRuleRegistry()
const form = useForm({ rendererRegistry: renderers, presetRuleRegistry: validators })
createRendererRegistry() 和 createPresetRuleRegistry() 是 Core 传递导出,不是 Vue 自有 API。独立 PresetRuleRegistry 创建时包含内置 required,也可继续注册调用方的预设规则。
类型参考
Vue 根入口自有以下公开类型:
| 类型 | 定义与用途 |
|---|---|
FormContextValue<TValues> |
Form Context,包含表单实例和展示配置。见 Composition API。 |
FormConfigContextValue |
表单展示配置 Context。 |
SchemxDictionary<TValues, TResponse, TOption> |
函数式选项源配置;TResponse 从 api 传递到 formatter,TOption 是格式化后的选项类型。见 Dictionary。 |
SchemxInstallOptions |
基于 Core SchemxConfig 的 App 安装配置;Vue 通过 SchemxConfigDefinition 扩展 row / rowComponent / colComponent。 |
SchemxWithDictionary<A, TValues> |
A & { dict?: SchemxDictionary<TValues> | SchemxDictionary<TValues>["api"] },只增加可选 dict。见 WithRemoteOptions。 |
UseDictionaryReturn<TOption> |
{ list, loading, error, loadDict, refresh, mutate },list 和 mutate 使用 TOption。见 useDictionary。 |
UseDictOptionsReturn<TOption> |
UseDictionaryReturn<TOption> 的弃用别名。 |
SchemxFormProps<TValues> |
Schemx 组件 Props 类型,由 Core 表单选项和 Vue 专属 modelValue、form、class、style 等属性组合而成。 |
ScrollToFieldOptions |
scrollToField() 的滚动方式和对齐选项。 |
VueSchemxInstance<TValues> |
SchemxInstance<TValues> 的弃用别名。 |
FieldInstance<TValues> |
Vue Ref / Computed 桥接后的字段控制器类型,由 useField() 返回。 |
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.2.0 | ^3.0.0 | MIT | |
| 1.1.1 | ^3.0.0 | MIT | |
| 1.1.0 | ^3.0.0 | MIT | |
| 1.0.1 | ^3.0.0 | MIT | |
| 1.0.0 | ^3.0.0 | MIT | |
| 0.2.3 | ^3.0.0 | MIT | |
| 0.2.2 | ^3.0.0 | MIT | |
| 0.2.1 | ^3.0.0 | MIT | |
| 0.2.0 | ^3.0.0 | MIT | |
| 0.1.22 | ^3.0.0 | MIT | |
| 0.1.21 | ^3.0.0 | MIT | |
| 0.1.20 | ^3.0.0 | MIT |
Pre-releases (10)
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0-next.6 | ^3.0.0 | MIT | |
| 1.0.0-next.5 | ^3.0.0 | MIT | |
| 1.0.0-next.4 | ^3.0.0 | MIT | |
| 1.0.0-next.3 | ^3.0.0 | MIT | |
| 1.0.0-next.2 | ^3.0.0 | MIT | |
| 1.0.0-next.1 | ^3.0.0 | MIT | |
| 1.0.0-next.0 | ^3.0.0 | MIT | |
| 0.2.4-beta.20260716174917.f6b485a | ^3.0.0 | MIT | |
| 0.1.22-alpha.20260701230808.331d3b2 | ^3.0.0 | MIT | |
| 0.1.21-beta.20260617171923.7799bce | ^3.0.0 | MIT |