@schemx/vue 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1059 -74
- package/dist/analyze.html +1 -1
- package/dist/components/FormGroup/index.d.ts.map +1 -1
- package/dist/hocs/withRemoteOptions.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/provideFormConfigContext.d.ts +9 -1
- package/dist/hooks/provideFormConfigContext.d.ts.map +1 -1
- package/dist/hooks/useField.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +44 -24
- package/dist/index.mjs.map +1 -1
- package/dist/types/field.d.ts +2 -0
- package/dist/types/field.d.ts.map +1 -1
- package/dist/types/form.d.ts +3 -2
- package/dist/types/form.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,53 +1,44 @@
|
|
|
1
1
|
# @schemx/vue
|
|
2
2
|
|
|
3
|
-
`@schemx/vue` 将 `@schemx/core` 的表单实例和 ViewSchemas 渲染为 Vue 组件树。它不绑定具体 UI 组件库,适合接入业务组件、设计系统或新的 UI adapter。
|
|
3
|
+
`@schemx/vue` 将 `@schemx/core` 的表单实例和 ViewSchemas 渲染为 Vue 3 组件树。它不绑定具体 UI 组件库,适合接入业务组件、设计系统或新的 UI adapter。
|
|
4
4
|
|
|
5
|
-
如果项目使用 Vant,推荐直接安装 [`@schemx/vant`](../vant)
|
|
5
|
+
如果项目使用 Vant,推荐直接安装 [`@schemx/vant`](../vant)。该包已经注册常用的移动端表单 Renderer。
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- 提供 Vue 3 表单组件和 Composition API。
|
|
10
|
-
- 通过 `rendererRegistry` 注册自定义 renderer。
|
|
11
|
-
- 支持 `v-model`、`initialValues`、表单级事件和实例方法。
|
|
12
|
-
- 复用 `@schemx/core` 的字段依赖、校验、运行时 schema node 和 ViewSchemas。
|
|
13
|
-
- 直接导出 `@schemx/core` 的公开类型与工具。
|
|
14
|
-
|
|
15
|
-
## 安装
|
|
7
|
+
## 安装与样式
|
|
16
8
|
|
|
17
9
|
```bash
|
|
18
10
|
pnpm add @schemx/vue @schemx/core vue
|
|
19
11
|
```
|
|
20
12
|
|
|
21
|
-
`@schemx/core` 是 `@schemx/vue` 的 peer
|
|
22
|
-
|
|
23
|
-
## 快速开始
|
|
13
|
+
`@schemx/core` 和 `vue` 是 `@schemx/vue` 的 peer dependencies,业务项目需要显式安装。
|
|
24
14
|
|
|
25
|
-
`@schemx/vue`
|
|
15
|
+
ESM 入口 `@schemx/vue` 会通过入口模块自动加载基础样式,常规 Vite / Vue ESM 项目不需要再次导入 CSS。CommonJS 入口不会保留这条 CSS import;直接使用 CommonJS,或构建工具没有处理入口 CSS import 时,需要显式导入公开的样式子路径:
|
|
26
16
|
|
|
27
17
|
```ts
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
import { rendererRegistry } from "@schemx/vue"
|
|
18
|
+
import "@schemx/vue/style.css"
|
|
19
|
+
```
|
|
31
20
|
|
|
32
|
-
|
|
21
|
+
不要从 `src/styles` 或 `dist` 的内部路径导入样式;包的 `exports` 只公开根入口和 `./style.css`。
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
```
|
|
23
|
+
## 快速开始
|
|
36
24
|
|
|
37
|
-
|
|
25
|
+
`@schemx/vue` 默认不提供具体输入控件。先注册 Renderer,再渲染表单:
|
|
38
26
|
|
|
39
27
|
```vue
|
|
40
28
|
<script setup lang="ts">
|
|
41
|
-
import { ref } from "vue"
|
|
42
|
-
|
|
43
|
-
import Schemx from "@schemx/vue"
|
|
29
|
+
import { markRaw, ref } from "vue"
|
|
44
30
|
|
|
31
|
+
import Schemx, { rendererRegistry } from "@schemx/vue"
|
|
45
32
|
import type { SchemxField } from "@schemx/vue"
|
|
46
33
|
|
|
34
|
+
import InputRenderer from "./components/InputRenderer.vue"
|
|
35
|
+
|
|
47
36
|
type ProfileValues = {
|
|
48
37
|
nickname: string
|
|
49
38
|
}
|
|
50
39
|
|
|
40
|
+
rendererRegistry.register("input", markRaw(InputRenderer))
|
|
41
|
+
|
|
51
42
|
const formData = ref<ProfileValues>({
|
|
52
43
|
nickname: "",
|
|
53
44
|
})
|
|
@@ -64,15 +55,396 @@ rendererRegistry.register("input", markRaw(InputRenderer))
|
|
|
64
55
|
</script>
|
|
65
56
|
|
|
66
57
|
<template>
|
|
67
|
-
<Schemx v-model="formData" :schemas="schemas" />
|
|
58
|
+
<Schemx v-model="formData" :initial-values="formData" :schemas="schemas" />
|
|
59
|
+
</template>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
这里同时传入 `initialValues` 是有意为之:当前版本会在字段变化后发出 `update:modelValue`,但不会用传入的 `modelValue` 初始化内部表单,也不会监听外部 `modelValue` 的后续变化。详见下一节的受控行为说明。
|
|
63
|
+
|
|
64
|
+
## Schemx 组件
|
|
65
|
+
|
|
66
|
+
默认导出和命名导出 `schemxForm` 指向同一个可安装组件:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import Schemx, { schemxForm } from "@schemx/vue"
|
|
70
|
+
|
|
71
|
+
console.log(Schemx === schemxForm) // true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
根入口当前没有名为 `SchemxForm` 的命名导出。可以自行给默认导入命名,但不要依赖 `@schemx/vue/src/*` 等深层路径。
|
|
75
|
+
|
|
76
|
+
### Props
|
|
77
|
+
|
|
78
|
+
`Schemx` 的公开 Props 由 core 的 `SchemxProps<T>` 加上 Vue 层的 `class` 和 `style` 组成。
|
|
79
|
+
|
|
80
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
81
|
+
| --------------------- | ------------------------------------------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `schemas` | `SchemxField<T>[]` | `[]` | 表单 Schema。类型层要求传入,运行时缺省为空数组 |
|
|
83
|
+
| `modelValue` | `T` | `{}` | `v-model` 的输入端;当前实现不使用它初始化或反向同步内部实例,见下文 |
|
|
84
|
+
| `initialValues` | `T` | `{}` | 创建内部表单时的初始值,也是 `reset()` 的还原基准 |
|
|
85
|
+
| `form` | `SchemxInstance<T>` | `undefined` | 外部表单实例;传入后组件不再创建实例,但仍会提供 Vue 上下文并同步 `schemas`;组件 Props 回调和 `v-model` 输出不会接入该实例 |
|
|
86
|
+
| `rendererRegistry` | `RendererRegistryType` | 全局 `rendererRegistry` | 当前表单使用的 Renderer Registry |
|
|
87
|
+
| `defaultRendererType` | `string` | `undefined` | 类型上用于设置默认 Renderer;当前 Vue 创建路径总会传入 Registry,因此该 Prop 实际不会设置 Registry 的默认类型,见下文 |
|
|
88
|
+
| `validatorRegistry` | `ValidatorsRegistryType` | 全局 `validatorRegistry` | 当前表单使用的校验规则 Registry |
|
|
89
|
+
| `required` | `boolean` | `undefined` | 类型中存在,但当前不会成为字段默认值;字段按自身配置或校验规则推导 |
|
|
90
|
+
| `readonly` | `boolean` | `undefined` | core 实际读取的表单级字段默认值之一;字段自身配置优先 |
|
|
91
|
+
| `disabled` | `boolean` | `undefined` | core 实际读取的表单级字段默认值之一;字段自身配置优先 |
|
|
92
|
+
| `visible` | `boolean` | `undefined` | 类型中存在,但当前不会成为字段默认值;字段未配置时由 core 固定解析为 `true` |
|
|
93
|
+
| `labelIcon` | `string` | `undefined` | 类型中存在,但当前不会成为字段默认值;Vue `FormItem` 目前也不渲染标签图标 |
|
|
94
|
+
| `labelAlign` | `"left" \| "center" \| "right"` | `undefined` | 类型中存在,但 core 会把未配置字段解析为 `"left"`,遮蔽 `FormItem` 的表单上下文回退 |
|
|
95
|
+
| `labelPosition` | `"left" \| "top" \| "right"` | `undefined` | 类型中存在,但 core 会把未配置字段解析为 `"left"`,遮蔽表单上下文回退 |
|
|
96
|
+
| `labelWidth` | `string` | `undefined` | 类型中存在,但 core 会把未配置字段解析为 `"auto"`,遮蔽表单上下文回退 |
|
|
97
|
+
| `contentAlign` | `"left" \| "center" \| "right"` | `undefined` | core 实际读取的表单级字段默认值之一;用于解析 Renderer 的 `align`,字段自身配置优先 |
|
|
98
|
+
| `validationTrigger` | `ValidationTrigger \| ValidationTrigger[]` | `undefined` | core 实际读取的表单级字段默认值之一;字段自身配置优先,均未配置时最终默认 `"blur"` |
|
|
99
|
+
| `colon` | `boolean` | `undefined` | 类型中存在,但 core 会把未配置字段解析为 `true`,遮蔽表单上下文回退 |
|
|
100
|
+
| `onFinish` | `(values: Readonly<T>) => void \| Promise<void>` | `undefined` | `submit()` 校验通过后的回调 Prop |
|
|
101
|
+
| `onFinishFailed` | `(error: ValidateError<T>) => void` | `undefined` | `submit()` 校验失败后的回调 Prop |
|
|
102
|
+
| `onValuesChange` | `(changedValues, latestSnapshot) => void` | `undefined` | 字段值变化后的回调 Prop |
|
|
103
|
+
| `onFieldsChange` | `(changedFields, allFields) => void` | `undefined` | 字段路径变化后的回调 Prop |
|
|
104
|
+
| `class` | `string` | `""` | 添加到根 `.schemx` 元素的类名 |
|
|
105
|
+
| `style` | `CSS.Properties` | `{}` | 类型已声明,但当前根元素没有绑定该值;不要依赖它产生内联样式 |
|
|
106
|
+
|
|
107
|
+
当前只有 `readonly`、`disabled`、`contentAlign` 和 `validationTrigger` 真正参与 core 的表单级默认值合并,优先级为字段配置 → 表单 Prop → core 固定默认值。`required`、`visible`、`labelIcon`、`labelAlign`、`labelPosition`、`labelWidth` 和 `colon` 虽然存在于 `SchemxProps` 和 Vue Props 类型中,但 core 不从表单 `defaultProps` 读取它们;其中标签布局类字段还会被 core 写入字段 ViewSchema 的固定默认值,导致 `FormItem` 中同名表单上下文回退无法生效。传入 `form` 时,`initialValues`、Registry 和所有表单回调都由外部实例的创建者配置;组件不会用同名 Props 重建或包装外部实例。
|
|
108
|
+
|
|
109
|
+
当前 `useForm()` 总会把显式传入的 `rendererRegistry` 或 Vue 包的全局 `rendererRegistry` 传给 core。core 只有在没有 Registry 时才用 `defaultRendererType` 创建新 Registry,因此 `<Schemx :default-renderer-type="...">` 和 Vue `useForm({ defaultRendererType: ... })` 中的该值当前实际被忽略。需要默认回退类型时,请创建已经设置默认类型的独立 Registry,例如 `createRendererRegistry("input")`,再通过 `rendererRegistry` 传入;外部 `form` 则应在创建实例时完成配置。
|
|
110
|
+
|
|
111
|
+
### `v-model`、`initialValues` 与事件
|
|
112
|
+
|
|
113
|
+
`initialValues` 是内部 Store 的初始快照和 `reset()` 基准。`modelValue` 按 Vue 约定对应 `v-model`,但当前组件实现是单向输出而非完整受控模式:
|
|
114
|
+
|
|
115
|
+
- 创建内部实例时只传递 `initialValues`,不会把 `modelValue` 合并进去。
|
|
116
|
+
- 外部替换 `modelValue` 后,组件不会调用 `setFieldsValue()`。
|
|
117
|
+
- 组件创建内部实例时,字段变化会发出 `update:modelValue`,值为最新表单快照。
|
|
118
|
+
- 传入外部 `form` 时不会安装组件内部的 `onValuesChange` 包装;字段变化不会发出 `update:modelValue`,所以 `v-model` 输出也不生效。外部实例的 `onValuesChange` 等回调必须由创建者配置。
|
|
119
|
+
- `onFinish`、`onFinishFailed`、`onValuesChange` 和 `onFieldsChange` 是声明过的回调 Props,不在 `defineEmits` 的事件列表中。模板中的 `@finish` 等写法会按 Vue listener Prop 规则映射到这些 Props,但 TypeScript 用户更适合显式传回调。
|
|
120
|
+
|
|
121
|
+
内部表单模式还存在一个 `onFinish` 等待边界:组件传给 core 的包装函数会调用 `props.onFinish(values)`,但没有 `return` 或 `await` 其返回值。因此 `submit()` 只等待这层立即完成的包装 Promise,不等待业务 `onFinish` 返回的异步任务;该任务后续 reject 也不会沿 `submit()` 传播。业务回调若在返回 Promise 前同步抛错,包装函数会转为 rejected Promise,core 的 `submit()` 仍会收到该拒绝。传入外部 `form` 时组件不会安装这层包装,等待和错误传播完全取决于外部实例创建者配置的回调;core `createForm()` 本身会 `await` 直接传给它的 `onFinish`。
|
|
122
|
+
|
|
123
|
+
唯一通过 `defineEmits` 声明的组件事件如下:
|
|
124
|
+
|
|
125
|
+
| 事件 | 参数 | 触发时机 |
|
|
126
|
+
| ------------------- | ------------ | ------------------------------------ |
|
|
127
|
+
| `update:modelValue` | `(value: T)` | 内部表单的 `onValuesChange` 被调用后 |
|
|
128
|
+
|
|
129
|
+
如需真正的外部受控同步,当前应持有 `ref` 或外部 `form`,并显式调用 `setFieldsValue()`。
|
|
130
|
+
|
|
131
|
+
### Slots
|
|
132
|
+
|
|
133
|
+
`Schemx` 把收到的所有 Slots 继续传给每个 `FormItem`。字符串字段名直接参与 Slot 命名。数组路径目前存在两种不同的字符串化规则:整体字段 Slot 和 Renderer 子 Slot 会先经过 `normalizeNameKey()`,用 `.` 连接;标签、内容和错误 Slot 则直接把数组插入模板字符串,使用 JavaScript 默认的逗号连接。这是当前实现差异,并非统一的命名约定。
|
|
134
|
+
|
|
135
|
+
| Slot 名称 | Slot Props | 行为 |
|
|
136
|
+
| ------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `{name}` | 当前字段的 `SchemxViewSchema` | 完全接管整个字段,不渲染默认 FormItem 结构;数组路径 `['user', 'name']` 对应 `user.name` |
|
|
138
|
+
| `{name}Label` | 当前字段的 `SchemxViewSchema` | 替换标签区域;同一数组路径当前对应 `user,nameLabel` |
|
|
139
|
+
| `{name}Content` | 当前字段 Schema 字段及 `columnElement` | 替换控件内容区域;`columnElement` 是已经创建的 Renderer VNode;同一数组路径当前对应 `user,nameContent` |
|
|
140
|
+
| `{name}Error` | 当前字段 Schema 字段及 `errors` | 替换错误区域;同一数组路径当前对应 `user,nameError` |
|
|
141
|
+
| `{name}:{slotName}` | 原样传给 Renderer | 去掉 `{name}:` 前缀后作为 Renderer 命名 Slot;同一数组路径的前缀为 `user.name:`,例如 `user.name:prefix` |
|
|
142
|
+
|
|
143
|
+
字段名和 `Label` / `Content` / `Error` 组合支持 camelCase 与 kebab-case 互查。子 Renderer Slot 的字段名前缀也支持这两种形式。
|
|
144
|
+
|
|
145
|
+
```vue
|
|
146
|
+
<Schemx :schemas="schemas">
|
|
147
|
+
<template #nicknameLabel="schema">
|
|
148
|
+
<strong>{{ schema.label }}</strong>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<template #nicknameError="{ errors }">
|
|
152
|
+
<small v-if="errors?.length">{{ errors.join(";") }}</small>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<template #nickname:prefix>
|
|
156
|
+
<span>@</span>
|
|
157
|
+
</template>
|
|
158
|
+
</Schemx>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 组件 ref 暴露实例
|
|
162
|
+
|
|
163
|
+
组件通过 `defineExpose` 暴露传入或内部创建的完整 `SchemxInstance<T>`。该接口当前共有 40 个成员:
|
|
164
|
+
|
|
165
|
+
| 分类 | 成员 |
|
|
166
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
167
|
+
| 值与快照(9) | `getFieldValue`、`getFieldsValue`、`setFieldValue`、`setFieldsValue`、`getFieldSnapshot`、`getFieldsSnapshot`、`getInitialValue`、`getInitialValues`、`setInitialValues` |
|
|
168
|
+
| touched 与 pending(6) | `isFieldTouched`、`setFieldTouched`、`getTouchedFields`、`setFieldPending`、`isFieldPending`、`getPendingFields` |
|
|
169
|
+
| 重置、校验与提交(7) | `resetFields`、`reset`、`validateField`、`validate`、`getFieldError`、`setFieldError`、`submit` |
|
|
170
|
+
| 响应与 Schema(9) | `effect`、`batch`、`setSchemas`、`updateSchemas`、`updateFieldSchema`、`updateDefaultProps`、`getViewSchemas`、`subscribeViewSchemas`、`waitForDependencies` |
|
|
171
|
+
| Registry(8) | `getRenderer`、`registerRenderer`、`hasRenderer`、`getValidator`、`registerValidator`、`hasValidator`、`registerRules`、`unregisterRules` |
|
|
172
|
+
| 生命周期(1) | `destroy` |
|
|
173
|
+
|
|
174
|
+
```vue
|
|
175
|
+
<script setup lang="ts">
|
|
176
|
+
import { ref } from "vue"
|
|
177
|
+
|
|
178
|
+
import Schemx from "@schemx/vue"
|
|
179
|
+
import type { SchemxField, SchemxInstance, Values } from "@schemx/vue"
|
|
180
|
+
|
|
181
|
+
interface ProfileValues extends Values {
|
|
182
|
+
nickname: string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const schemas: SchemxField<ProfileValues>[] = [
|
|
186
|
+
{
|
|
187
|
+
name: "nickname",
|
|
188
|
+
label: "昵称",
|
|
189
|
+
componentType: "input",
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
const formRef = ref<SchemxInstance<ProfileValues>>()
|
|
194
|
+
|
|
195
|
+
async function submit() {
|
|
196
|
+
await formRef.value?.submit()
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<template>
|
|
201
|
+
<Schemx ref="formRef" :schemas="schemas" />
|
|
202
|
+
<button type="button" @click="submit">提交</button>
|
|
68
203
|
</template>
|
|
69
204
|
```
|
|
70
205
|
|
|
71
|
-
|
|
206
|
+
### 作为 Vue 插件安装
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { createApp } from "vue"
|
|
210
|
+
|
|
211
|
+
import Schemx from "@schemx/vue"
|
|
212
|
+
|
|
213
|
+
import App from "./App.vue"
|
|
214
|
+
|
|
215
|
+
createApp(App).use(Schemx).mount("#app")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`app.use(Schemx)` 只注册全局组件名 `SchemxForm`。`SchemxInstallOptions` 当前是空接口,传入的 options 不会产生运行时效果。可安装组件还挂载了静态属性 `Schemx.FormItem`;`FormGroup` 仅作为根入口命名导出,不是静态属性。
|
|
219
|
+
|
|
220
|
+
`SchemxFormProps` 和 Vue 层 `FieldInstance` 存在于内部类型目录,但当前没有从根入口导出。不要推荐或依赖深层导入;组件 Props 请以默认组件推导和根入口公开的 `SchemxProps` 为准。
|
|
221
|
+
|
|
222
|
+
## Schema 写法
|
|
223
|
+
|
|
224
|
+
根入口从 `@schemx/core` 传递导出 `SchemxField<T>`。它是普通字段、分组和动态依赖子树的联合类型。
|
|
225
|
+
|
|
226
|
+
### 普通字段
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import type { SchemxField } from "@schemx/vue"
|
|
230
|
+
|
|
231
|
+
type Values = {
|
|
232
|
+
nickname: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const schemas: SchemxField<Values>[] = [
|
|
236
|
+
{
|
|
237
|
+
name: "nickname",
|
|
238
|
+
label: "昵称",
|
|
239
|
+
componentType: "input",
|
|
240
|
+
initialValue: "",
|
|
241
|
+
placeholder: "请输入昵称",
|
|
242
|
+
required: true,
|
|
243
|
+
rules: "required",
|
|
244
|
+
validationTrigger: ["change", "blur"],
|
|
245
|
+
componentProps: {
|
|
246
|
+
align: "left",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
普通字段的真实字段如下:
|
|
253
|
+
|
|
254
|
+
| 字段 | 必填 | 说明 |
|
|
255
|
+
| -------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
256
|
+
| `name` | 是 | 字段路径,支持字符串和路径数组 |
|
|
257
|
+
| `label` | 是 | 标签文本 |
|
|
258
|
+
| `componentType` | 是 | Registry 中的 Renderer key |
|
|
259
|
+
| `dependencies` | 否 | 基于其他字段动态覆盖展示属性的结构化依赖配置 |
|
|
260
|
+
| `componentProps` | 否 | 透传给 Renderer 的专属 Props;框架注入项会覆盖同名值 |
|
|
261
|
+
| `placeholder` | 否 | 占位提示 |
|
|
262
|
+
| `required`、`readonly`、`disabled`、`visible` | 否 | 字段展示和交互状态 |
|
|
263
|
+
| `initialValue` | 否 | 字段挂载时的初始值和 `reset()` 还原值 |
|
|
264
|
+
| `rules` | 否 | 单条或多条校验规则,支持 Standard Schema 与内置规则名 |
|
|
265
|
+
| `labelIcon`、`labelAlign`、`labelPosition`、`labelWidth` | 否 | 标签展示配置 |
|
|
266
|
+
| `contentAlign`、`colon` | 否 | 内容对齐和冒号配置 |
|
|
267
|
+
| `validationTrigger` | 否 | `change`、`blur` 等校验触发时机 |
|
|
268
|
+
| `onChange`、`onBlur` | 否 | 类型中存在的顶层回调;当前 Vue `FormItem` 不调用它们,Renderer 事件说明见后文 |
|
|
269
|
+
| `class`、`style` | 否 | Vue 源码通过声明合并增加,运行时分别应用到字段容器的 class 和内联 style;当前发布根声明未带入该 augmentation,普通消费者直接填写时可能无法通过类型检查 |
|
|
270
|
+
| `key` | 否 | 框架字段;业务方通常不要设置 |
|
|
271
|
+
|
|
272
|
+
### 分组
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const group: SchemxField<Values> = {
|
|
276
|
+
componentType: "group",
|
|
277
|
+
label: "基本信息",
|
|
278
|
+
collapsible: true,
|
|
279
|
+
defaultCollapsed: false,
|
|
280
|
+
children: [
|
|
281
|
+
{
|
|
282
|
+
name: "nickname",
|
|
283
|
+
label: "昵称",
|
|
284
|
+
componentType: "input",
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
分组字段包含 `componentType: "group"`、`label`、`children`、可选的 `collapsible`、`defaultCollapsed` 和框架用 `key`。它不是普通字段,没有 `name`、`componentProps` 或字段值。
|
|
291
|
+
|
|
292
|
+
### 动态依赖子树
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const dependency: SchemxField<Values> = {
|
|
296
|
+
componentType: "dependency",
|
|
297
|
+
to: ["nickname"],
|
|
298
|
+
renderer: async (values, form, { abortSignal }) => {
|
|
299
|
+
if (!values.nickname || abortSignal.aborted) return []
|
|
300
|
+
|
|
301
|
+
return [
|
|
302
|
+
{
|
|
303
|
+
name: "signature",
|
|
304
|
+
label: "签名",
|
|
305
|
+
componentType: "input",
|
|
306
|
+
},
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
动态依赖节点包含 `componentType: "dependency"`、依赖路径数组 `to`、`renderer(values, form, context)` 和可选 `key`。`renderer` 可以同步或异步返回新的 `SchemxField<T>[]`;`context.abortSignal` 用于取消已经过期的异步依赖计算。该 `AbortSignal` 属于 core 的 dependency renderer,和后文 `useDictionary` 的请求行为不同。
|
|
313
|
+
|
|
314
|
+
## Dictionary
|
|
315
|
+
|
|
316
|
+
`SchemxDictionary<T, R>` 描述由函数加载的选项列表:`T` 是完整表单值类型,`R` 是 `api` 的原始返回类型,并会传给 `formatter`。
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
interface SchemxDictionary<T extends Values = Values, R = any> {
|
|
320
|
+
api: (values: T, form: SchemxInstance<T>) => R | Promise<R>
|
|
321
|
+
formatter?: (res: Awaited<R>, form: SchemxInstance<T>) => any[] | Promise<any[]>
|
|
322
|
+
dependsOn?: NamePath<T>[]
|
|
323
|
+
shouldFetch?: (values: T) => boolean
|
|
324
|
+
immediate?: boolean
|
|
325
|
+
resetOnDepsChange?: boolean
|
|
326
|
+
retryCount?: number
|
|
327
|
+
retryInterval?: number
|
|
328
|
+
onError?: (error: Error, form: SchemxInstance<T>) => void
|
|
329
|
+
onSuccess?: (data: any[], form: SchemxInstance<T>) => void
|
|
330
|
+
onDepsChange?: (values: T, form: SchemxInstance<T>) => void
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
| 字段 | 默认值 | 真实行为 |
|
|
335
|
+
| ------------------- | ------------------- | ------------------------------------------------------------------------------------- |
|
|
336
|
+
| `api` | 必填 | 接收加载开始时的完整表单快照和实例;支持同步或异步返回 |
|
|
337
|
+
| `formatter` | 直接使用 `api` 结果 | 在请求成功后转换结果;支持异步,返回值应为数组 |
|
|
338
|
+
| `dependsOn` | `undefined` | 通过 `useWatchFields` 监听这些字段;变化时执行依赖回调、可选重置和重新加载 |
|
|
339
|
+
| `shouldFetch` | 总是加载 | 每次加载前判断;返回 `false` 时跳过 `api`、清空 `list` 并结束加载状态 |
|
|
340
|
+
| `immediate` | `true` | 组件 `onMounted` 时是否自动调用一次 `loadDict()`;不影响依赖变化或手动刷新 |
|
|
341
|
+
| `resetOnDepsChange` | `false` | 依赖变化时,如果 `useDictionary` 收到目标字段路径,调用 `form.setFieldValue(fieldName, undefined)` |
|
|
342
|
+
| `retryCount` | `0` | 失败后的额外重试次数;总尝试次数为 `retryCount + 1` |
|
|
343
|
+
| `retryInterval` | `1000` | 两次尝试之间等待的毫秒数 |
|
|
344
|
+
| `onError` | 无 | 最终失败后接收规范化的 `Error` 和表单实例 |
|
|
345
|
+
| `onSuccess` | 无 | `formatter` 完成、`list` 写入后接收最终数组和表单实例 |
|
|
346
|
+
| `onDepsChange` | 无 | 依赖变化后、`shouldFetch` 判断前调用 |
|
|
347
|
+
|
|
348
|
+
当前类型没有 `data`、`list` 或 `options` 形式的静态数组字段。同步 `api` 可以表达静态来源:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const staticDictionary: SchemxDictionary<ProfileValues, Option[]> = {
|
|
352
|
+
api: () => [
|
|
353
|
+
{ label: "公开", value: "public" },
|
|
354
|
+
{ label: "私密", value: "private" },
|
|
355
|
+
],
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
异步来源可以用 `R` 保留原始响应类型,并通过 `formatter` 转为选项数组:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
type CityResponse = {
|
|
363
|
+
data: Array<{ id: number; name: string }>
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const cityDictionary: SchemxDictionary<ProfileValues, CityResponse> = {
|
|
367
|
+
api: async () => {
|
|
368
|
+
const response = await fetch("/api/cities")
|
|
369
|
+
return (await response.json()) as CityResponse
|
|
370
|
+
},
|
|
371
|
+
formatter: (response) =>
|
|
372
|
+
response.data.map((city) => ({
|
|
373
|
+
label: city.name,
|
|
374
|
+
value: city.id,
|
|
375
|
+
})),
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
依赖字段示例:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
type AddressValues = {
|
|
383
|
+
province?: string
|
|
384
|
+
city?: string
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const cityDictionary: SchemxDictionary<
|
|
388
|
+
AddressValues,
|
|
389
|
+
Array<{ id: string; name: string }>
|
|
390
|
+
> = {
|
|
391
|
+
api: (values) => fetchCities(values.province!),
|
|
392
|
+
formatter: (cities) => cities.map((city) => ({ label: city.name, value: city.id })),
|
|
393
|
+
dependsOn: ["province"],
|
|
394
|
+
shouldFetch: (values) => Boolean(values.province),
|
|
395
|
+
resetOnDepsChange: true,
|
|
396
|
+
immediate: false,
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `useDictionary`
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
function useDictionary<
|
|
404
|
+
TValues extends Values = Values,
|
|
405
|
+
TName extends NamePath<TValues> = NamePath<TValues>,
|
|
406
|
+
>(options: SchemxDictionary<TValues>, fieldName?: TName): UseDictionaryReturn
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`useDictionary` 必须在已经提供表单上下文的后代组件 `setup()` 中同步调用,例如 `Schemx` 的后代 Renderer。`fieldName` 只供 `resetOnDepsChange` 清空目标字段使用。通过 `WithRemoteOptions` 包装、且位于 `FormItem` 内的 Renderer 会自动从字段 Context 取得当前字段路径;直接调用 `useDictionary()` 或脱离 `FormItem` 使用 HOC 时,仍可显式传入该参数。
|
|
410
|
+
|
|
411
|
+
`UseDictionaryReturn` 的全部成员如下:
|
|
412
|
+
|
|
413
|
+
| 成员 | 类型 | 说明 |
|
|
414
|
+
| ---------- | ------------------------- | ---------------------------------------------------------------- |
|
|
415
|
+
| `list` | `Ref<any[]>` | 最终选项数组;初始值为 `[]` |
|
|
416
|
+
| `loading` | `Ref<boolean>` | 通过 `shouldFetch` 后开始请求时为 `true`,成功或失败后为 `false` |
|
|
417
|
+
| `error` | `Ref<Error \| undefined>` | 新请求开始时清空;最终失败时写入规范化的 `Error` |
|
|
418
|
+
| `loadDict` | `() => Promise<void>` | 使用当前配置和当前表单值执行加载 |
|
|
419
|
+
| `refresh` | `() => Promise<void>` | `loadDict` 的同语义包装;不会绕过 `shouldFetch` |
|
|
420
|
+
| `mutate` | `(data: any[]) => void` | 直接替换 `list`,不调用 `api`,也不修改 `loading` / `error` |
|
|
421
|
+
|
|
422
|
+
成功路径为 `api`(含重试)→ `formatter` → 写入 `list` → `onSuccess`。`shouldFetch`、`api`、`formatter` 或 `onSuccess` 抛错时都会进入 `loadDict()` 的错误路径:抛出值经 `normalizeError` 转为 `Error`,然后写入 `error`、清空 `list`、结束 `loading` 并调用 `onError`。通常这些错误会被处理,`loadDict()` resolve;但 `onError` 本身若抛错,该异常位于 `catch` 块内,没有第二层捕获,`loadDict()` / `refresh()` 返回的 Promise 会 reject。
|
|
423
|
+
|
|
424
|
+
依赖监听中的 `onDepsChange` 在调用 `loadDict()` 之前执行,不在其 `try...catch` 内。`onDepsChange` 自身抛错时会直接从 Watch 回调抛出,并阻止本轮重置字段及重新加载;它不会更新 `useDictionary.error`,也不会调用 `onError`。
|
|
425
|
+
|
|
426
|
+
并发加载使用递增计数避免过期的成功响应或异步 `formatter` 结果写入,但不会创建 `AbortController`,底层请求仍会继续执行,`api` 也不会收到 `AbortSignal`。当前错误分支没有过期请求检查;较早请求的迟到错误仍可能覆盖较新结果。另一个边界是:`shouldFetch` 返回 `false` 时不会递增请求计数,已经在途的旧请求仍可能随后写回。需要严格取消语义时,请在业务 `api` 外层管理请求,并避免把 Dictionary 的竞态控制等同于网络中止。
|
|
72
427
|
|
|
73
428
|
## 自定义 Renderer
|
|
74
429
|
|
|
75
|
-
|
|
430
|
+
Renderer 是从 Registry 取出的普通 Vue 组件。`FormItem` 先展开 `schema.componentProps`,再注入或覆盖以下 Props:
|
|
431
|
+
|
|
432
|
+
| Prop / listener | 实际值与行为 |
|
|
433
|
+
| ----------------------- | ------------------------------------------------------------------------------------------- |
|
|
434
|
+
| `value` | 当前字段值 |
|
|
435
|
+
| `onUpdate:value(value)` | 直接调用 `field.setValue(value)`;不在这里触发字段级校验 |
|
|
436
|
+
| `onChange(value)` | 写入字段值,调用 `schema.componentProps.onChange(value)`,并按 `validationTrigger` 执行校验 |
|
|
437
|
+
| `onBlur()` | 不写值,按 `validationTrigger` 执行校验 |
|
|
438
|
+
| `readonly` | 当前 ViewSchema 已解析的只读状态,覆盖 `componentProps.readonly` |
|
|
439
|
+
| `disabled` | 当前 ViewSchema 已解析的禁用状态,覆盖 `componentProps.disabled` |
|
|
440
|
+
| `placeholder` | 当前 ViewSchema 的占位文本,覆盖 `componentProps.placeholder` |
|
|
441
|
+
| `formItemProps` | 当前完整 ViewSchema,覆盖 `componentProps.formItemProps` |
|
|
442
|
+
|
|
443
|
+
其他 `componentProps`(例如 `options`、`readonlyPlaceholder`、`align` 和已经通过声明合并注册的组件专属 Props)原样透传。虽然 core 的 `SchemxBaseComponentProps` 声明了 `formInstance`,当前 Vue `FormItem` 不会自动注入它。
|
|
444
|
+
|
|
445
|
+
当前也不会向普通 Renderer 自动注入 `fieldName`、字段校验 `error` / `errors` 或 `loading`。Renderer 如需当前字段路径或响应式状态,可调用 `useFieldContext()`;`WithRemoteOptions` 会自动使用该 Context 的字段路径处理 `resetOnDepsChange`,但不会把内部路径透传给被包装 Renderer。详见下一节。
|
|
446
|
+
|
|
447
|
+
注意:`componentProps.onChange`、`componentProps.onBlur` 会分别在框架注入的同名回调中调用。Schema 顶层的 `onChange`、`onBlur` 仍没有在 Vue `FormItem` 中接线;需要使用回调时应放在 `componentProps` 内。
|
|
76
448
|
|
|
77
449
|
```vue
|
|
78
450
|
<script setup lang="ts">
|
|
@@ -81,12 +453,13 @@ renderer 会接收到 `value`、`onUpdate:value`、`onChange`、`onBlur`、`read
|
|
|
81
453
|
readonly?: boolean
|
|
82
454
|
disabled?: boolean
|
|
83
455
|
placeholder?: string
|
|
456
|
+
formItemProps?: { name?: string | string[] }
|
|
84
457
|
}>()
|
|
85
458
|
|
|
86
459
|
const emit = defineEmits<{
|
|
87
460
|
"update:value": [value: string]
|
|
88
461
|
change: [value: string]
|
|
89
|
-
blur: [
|
|
462
|
+
blur: []
|
|
90
463
|
}>()
|
|
91
464
|
</script>
|
|
92
465
|
|
|
@@ -98,63 +471,675 @@ renderer 会接收到 `value`、`onUpdate:value`、`onChange`、`onBlur`、`read
|
|
|
98
471
|
:placeholder="placeholder"
|
|
99
472
|
@input="emit('update:value', ($event.target as HTMLInputElement).value)"
|
|
100
473
|
@change="emit('change', ($event.target as HTMLInputElement).value)"
|
|
101
|
-
@blur="emit('blur'
|
|
474
|
+
@blur="emit('blur')"
|
|
102
475
|
/>
|
|
103
476
|
</template>
|
|
104
477
|
```
|
|
105
478
|
|
|
106
|
-
|
|
479
|
+
`update:value` 和 `change` 都能写入 Store;需要 `change` 校验和 `componentProps.onChange` 回调时,应发出 `change`。`blur` 的参数会被忽略。
|
|
480
|
+
|
|
481
|
+
### 全局与表单独立 Registry
|
|
482
|
+
|
|
483
|
+
全局 Registry 会作为 `useForm()` 的默认值:
|
|
107
484
|
|
|
108
485
|
```ts
|
|
109
|
-
import {
|
|
486
|
+
import { markRaw } from "vue"
|
|
487
|
+
|
|
488
|
+
import { rendererRegistry } from "@schemx/vue"
|
|
110
489
|
|
|
111
|
-
|
|
490
|
+
import InputRenderer from "./InputRenderer.vue"
|
|
112
491
|
|
|
113
|
-
rendererRegistry.register("input", InputRenderer)
|
|
492
|
+
rendererRegistry.register("input", markRaw(InputRenderer))
|
|
114
493
|
```
|
|
115
494
|
|
|
495
|
+
也可以使用根入口从 core 传递导出的 `createRendererRegistry` 创建表单独立 Registry,避免修改全局状态:
|
|
496
|
+
|
|
116
497
|
```vue
|
|
117
|
-
<
|
|
498
|
+
<script setup lang="ts">
|
|
499
|
+
import { markRaw } from "vue"
|
|
500
|
+
|
|
501
|
+
import Schemx, { createRendererRegistry } from "@schemx/vue"
|
|
502
|
+
import type { SchemxField, Values } from "@schemx/vue"
|
|
503
|
+
|
|
504
|
+
import InputRenderer from "./InputRenderer.vue"
|
|
505
|
+
|
|
506
|
+
interface ProfileValues extends Values {
|
|
507
|
+
nickname: string
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const localRegistry = createRendererRegistry("input")
|
|
511
|
+
localRegistry.register("input", markRaw(InputRenderer))
|
|
512
|
+
|
|
513
|
+
const schemas: SchemxField<ProfileValues>[] = [
|
|
514
|
+
{
|
|
515
|
+
name: "nickname",
|
|
516
|
+
label: "昵称",
|
|
517
|
+
componentType: "input",
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
</script>
|
|
521
|
+
|
|
522
|
+
<template>
|
|
523
|
+
<Schemx :schemas="schemas" :renderer-registry="localRegistry" />
|
|
524
|
+
</template>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## WithRemoteOptions
|
|
528
|
+
|
|
529
|
+
`SchemxWithDictionary<A, T>` 是公开的类型工具:
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
type SchemxWithDictionary<A, T extends Values = Values> = A & {
|
|
533
|
+
dict?: SchemxDictionary<T>
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
它只给原 Props 类型 `A` 增加可选的 `dict`;不会自动增加 `fieldName`、`options` 或 `loading`。
|
|
538
|
+
|
|
539
|
+
`WithRemoteOptions(WrappedComponent)` 返回一个增强组件。增强组件声明并消费:
|
|
540
|
+
|
|
541
|
+
- `dict?: SchemxDictionary`:存在时调用 `useDictionary(dict, fieldName)`。
|
|
542
|
+
- `fieldName?: NamePath`:仅作为兼容回退。HOC 位于 `FormItem` 内时,默认从 `useFieldContext().name` 自动取得当前字段路径;显式值只用于脱离 `FormItem` 的独立使用,且不会继续传给被包装组件。
|
|
543
|
+
|
|
544
|
+
被包装组件实际收到所有其余 attrs、原始 `dict`,以及 HOC 决定的 `options` 和 `loading`:
|
|
545
|
+
|
|
546
|
+
- 有 `dict` 时,Dictionary 的 `list` 和 `loading` 无条件覆盖 attrs 中的静态 `options`、`loading`,即使列表仍为空。
|
|
547
|
+
- 没有 `dict` 时,原 attrs 的 `options`、`loading` 原样保留。
|
|
548
|
+
- HOC 不注入 `error`、`refresh`、`loadDict` 或 `mutate`。
|
|
549
|
+
- 有 `dict` 时内部会调用 `useFormContext()`,所以组件必须位于已提供表单上下文的后代树中。
|
|
550
|
+
|
|
551
|
+
下面包装一个自定义 Select Renderer,并让依赖字段变化时重置当前字段:
|
|
552
|
+
|
|
553
|
+
```vue
|
|
554
|
+
<!-- SelectRenderer.vue -->
|
|
555
|
+
<script setup lang="ts">
|
|
556
|
+
type Option = { label: string; value: string }
|
|
557
|
+
|
|
558
|
+
defineProps<{
|
|
559
|
+
value?: string
|
|
560
|
+
options?: Option[]
|
|
561
|
+
loading?: boolean
|
|
562
|
+
disabled?: boolean
|
|
563
|
+
}>()
|
|
564
|
+
|
|
565
|
+
const emit = defineEmits<{
|
|
566
|
+
"update:value": [value: string]
|
|
567
|
+
change: [value: string]
|
|
568
|
+
}>()
|
|
569
|
+
</script>
|
|
570
|
+
|
|
571
|
+
<template>
|
|
572
|
+
<select
|
|
573
|
+
:value="value"
|
|
574
|
+
:disabled="disabled || loading"
|
|
575
|
+
@change="emit('change', ($event.target as HTMLSelectElement).value)"
|
|
576
|
+
>
|
|
577
|
+
<option v-for="option in options" :key="option.value" :value="option.value">
|
|
578
|
+
{{ option.label }}
|
|
579
|
+
</option>
|
|
580
|
+
</select>
|
|
581
|
+
</template>
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { markRaw } from "vue"
|
|
586
|
+
|
|
587
|
+
import { rendererRegistry, WithRemoteOptions } from "@schemx/vue"
|
|
588
|
+
import type {
|
|
589
|
+
NamePath,
|
|
590
|
+
SchemxDictionary,
|
|
591
|
+
SchemxField,
|
|
592
|
+
SchemxWithDictionary,
|
|
593
|
+
Values,
|
|
594
|
+
} from "@schemx/vue"
|
|
595
|
+
|
|
596
|
+
import SelectRenderer from "./SelectRenderer.vue"
|
|
597
|
+
|
|
598
|
+
type AddressValues = {
|
|
599
|
+
province?: string
|
|
600
|
+
city?: string
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
type Option = { label: string; value: string }
|
|
604
|
+
type City = { id: string; name: string }
|
|
605
|
+
|
|
606
|
+
// 可替换为真实请求;该最小实现让示例可以直接通过严格类型检查。
|
|
607
|
+
async function fetchCities(province: string): Promise<City[]> {
|
|
608
|
+
return province ? [{ id: "guangzhou", name: "广州" }] : []
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const cityDictionary: SchemxDictionary<AddressValues, City[]> = {
|
|
612
|
+
api: (values) => fetchCities(values.province!),
|
|
613
|
+
formatter: (cities) => cities.map((city) => ({ label: city.name, value: city.id })),
|
|
614
|
+
dependsOn: ["province"],
|
|
615
|
+
shouldFetch: (values) => Boolean(values.province),
|
|
616
|
+
resetOnDepsChange: true,
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
declare module "@schemx/core" {
|
|
620
|
+
interface SchemxRendererDefinition<T extends Values> {
|
|
621
|
+
"remote-select": SchemxWithDictionary<
|
|
622
|
+
{
|
|
623
|
+
options?: Option[]
|
|
624
|
+
loading?: boolean
|
|
625
|
+
},
|
|
626
|
+
T
|
|
627
|
+
>
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const RemoteSelect = WithRemoteOptions(SelectRenderer)
|
|
632
|
+
rendererRegistry.register("remote-select", markRaw(RemoteSelect))
|
|
633
|
+
|
|
634
|
+
const schemas: SchemxField<AddressValues>[] = [
|
|
635
|
+
{
|
|
636
|
+
name: "city",
|
|
637
|
+
label: "城市",
|
|
638
|
+
componentType: "remote-select",
|
|
639
|
+
componentProps: {
|
|
640
|
+
dict: cityDictionary,
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
]
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
若只需要静态选项,不传 `dict` 即可;`options` 会作为普通 `componentProps` 透传:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
componentProps: {
|
|
650
|
+
options: [
|
|
651
|
+
{ label: "公开", value: "public" },
|
|
652
|
+
{ label: "私密", value: "private" },
|
|
653
|
+
],
|
|
654
|
+
}
|
|
118
655
|
```
|
|
119
656
|
|
|
120
657
|
## Composition API
|
|
121
658
|
|
|
122
|
-
| API | 说明
|
|
123
|
-
| --------------------------- |
|
|
124
|
-
| `useForm()` | 创建由当前 Vue effect scope
|
|
125
|
-
| `createFormContext()` |
|
|
126
|
-
| `useFormContext()` |
|
|
127
|
-
| `useField()` | 创建字段级读写、校验和状态控制器
|
|
128
|
-
| `createFieldContext()` |
|
|
129
|
-
| `useFieldContext()` | 获取当前字段控制器
|
|
130
|
-
| `createFormConfigContext()` |
|
|
131
|
-
| `useFormConfigContext()` |
|
|
132
|
-
| `useWatch()` | 按单字段、多字段或全表签名监听变化
|
|
133
|
-
| `useWatchField()` | 监听单个字段
|
|
134
|
-
| `useWatchFields()` | 监听多个字段
|
|
135
|
-
| `useWatchAll()` | 监听整张表单
|
|
136
|
-
| `useDictionary()` |
|
|
137
|
-
| `useEffect()` | 创建字段依赖追踪 effect
|
|
138
|
-
| `useStableRef()` | 创建引用保持稳定的 `shallowRef`
|
|
139
|
-
| `useViewSchemas()` | 把 `subscribeViewSchemas()` 桥接为 Vue `shallowRef`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
659
|
+
| API | 说明 |
|
|
660
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
661
|
+
| `useForm()` | 创建由当前 Vue effect scope 管理生命周期的表单实例;自动使用 Vue 包全局 Registry,但不会自动 `provide` |
|
|
662
|
+
| `createFormContext()` | 在当前组件 `setup()` 同步阶段向后代提供表单实例;不接管实例所有权 |
|
|
663
|
+
| `useFormContext()` | 获取最近祖先提供的表单实例;缺少上下文时抛错 |
|
|
664
|
+
| `useField()` | 创建字段级读写、校验和状态控制器 |
|
|
665
|
+
| `createFieldContext()` | 向后代提供字段控制器 |
|
|
666
|
+
| `useFieldContext()` | 获取当前字段控制器 |
|
|
667
|
+
| `createFormConfigContext()` | 向后代提供表单展示配置 |
|
|
668
|
+
| `useFormConfigContext()` | 获取上层表单展示配置 |
|
|
669
|
+
| `useWatch()` | 按单字段、多字段或全表签名监听变化 |
|
|
670
|
+
| `useWatchField()` | 监听单个字段 |
|
|
671
|
+
| `useWatchFields()` | 监听多个字段 |
|
|
672
|
+
| `useWatchAll()` | 监听整张表单 |
|
|
673
|
+
| `useDictionary()` | 管理依赖字段的函数式选项来源 |
|
|
674
|
+
| `useEffect()` | 创建字段依赖追踪 effect,并在组件 `onUnmounted` 时清理 |
|
|
675
|
+
| `useStableRef()` | 创建引用保持稳定的 `shallowRef` |
|
|
676
|
+
| `useViewSchemas()` | 把 `subscribeViewSchemas()` 桥接为 Vue `shallowRef` |
|
|
677
|
+
|
|
678
|
+
`useForm()` 只负责创建和销毁实例。如需让自定义组件树中的 `useField()`、`useDictionary()` 或 `FormItem` 找到实例,Provider 必须在 `setup()` 的同步调用栈中调用 `createFormContext(form)`;展示层还应调用 `createFormConfigContext(props)`。直接使用 `<Schemx>` 时,这两种上下文已经由组件提供。
|
|
679
|
+
|
|
680
|
+
### 通用调用规则
|
|
681
|
+
|
|
682
|
+
依赖 Vue 生命周期或 provide / inject 的 Hook 应在 `setup()` 期间同步调用。直接使用 `<Schemx>` 时,组件已提供表单实例与展示配置;字段 Renderer 还会获得当前字段 Context。
|
|
683
|
+
|
|
684
|
+
### `useForm`
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
function useForm<TValues extends Values = Values>(
|
|
688
|
+
options?: CreateFormOptions<TValues, NamePath<TValues>>
|
|
689
|
+
): SchemxInstance<TValues>
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
源码中参数类型名为 `UseFormOptions<TValues>`,结构等同上面的 `CreateFormOptions`;`UseFormOptions` 未从根入口导出。`options` 可选,未传 Registry 时使用 Vue 全局 `rendererRegistry` 和 `validatorRegistry`。函数同步返回 `SchemxInstance<TValues>`,不读取 Context,也**不会自动 `provide`**;当前 Vue effect scope 销毁时会调用 `form.destroy()`。非 Vue scope 场景应改用 Core `createForm()` 并自行销毁。
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import { createFormContext, useForm } from "@schemx/vue"
|
|
696
|
+
|
|
697
|
+
type ProfileValues = { nickname: string }
|
|
698
|
+
const form = useForm<ProfileValues>({ initialValues: { nickname: "" } })
|
|
699
|
+
|
|
700
|
+
// 需要让后代 Hook 读取时,必须显式提供。
|
|
701
|
+
createFormContext(form)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
`useForm()` 除了补全 Vue 全局 Registry 外,会把 Core options 原样传给 `createForm()`。因此直接调用时,`modelValue` 会按 Core 规则覆盖同名 `initialValues` 并形成初始快照,`submit()` 也会等待直接传入的 `onFinish` Promise。前文 `modelValue` 不初始化、不持续反向同步,以及 `onFinish` Promise 不被等待的限制,只属于 `<Schemx>` 内部创建实例时的 Props 转换与回调包装,不属于 `useForm()` 本身。`defaultRendererType` 仍会受全局 Renderer Registry 已被补全的影响,见前文说明。
|
|
705
|
+
|
|
706
|
+
### 3 组 Context API
|
|
707
|
+
|
|
708
|
+
3 组 Reader 都要求 Context 存在,没有可选读取模式或默认值。`create*Context()` 必须在 Provider 组件的 `setup()` 同步阶段调用,且只对后代可见;`use*Context()` 仅读取最近祖先提供的值。
|
|
709
|
+
|
|
710
|
+
```ts
|
|
711
|
+
function createFormContext<TValues extends Values = Values>(
|
|
712
|
+
instance: SchemxInstance<TValues>
|
|
713
|
+
): void
|
|
714
|
+
function useFormContext<TValues extends Values = Values>(): SchemxInstance<TValues>
|
|
715
|
+
|
|
716
|
+
function createFieldContext<TValues extends Values = Values>(
|
|
717
|
+
field: FieldInstance<TValues>
|
|
718
|
+
): void
|
|
719
|
+
function useFieldContext(): FieldInstance<Values>
|
|
720
|
+
|
|
721
|
+
function createFormConfigContext<TValues extends Values = Values>(
|
|
722
|
+
props: FormContextProps<TValues>
|
|
723
|
+
): void
|
|
724
|
+
function useFormConfigContext(): FormContextProps
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
上面签名中的 `FieldInstance` 是源码声明名,不是可从 `@schemx/vue` 导入的根类型。`createFieldContext()` 当前只有 `TValues extends Values = Values`,**没有独立的 `TName` 泛型**;`field` 必须是 `useField()` 返回的 Vue 字段控制器,字段路径已由创建该返回值时的 `name` 捕获。调用方应依靠 `useField()` 推导参数,不要尝试深层导入该内部类型。
|
|
728
|
+
|
|
729
|
+
| Provider 签名 | Reader 签名 | 职责、所有权与缺失行为 |
|
|
730
|
+
| ------------------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
731
|
+
| `createFormContext<T>(form: SchemxInstance<T>): void` | `useFormContext<T>(): SchemxInstance<T>` | 提供 / 读取表单实例。Provider 不创建、不销毁实例;缺失时 Reader 抛出带 `createFormContext(form)` 指引的 `Error`。 |
|
|
732
|
+
| `createFieldContext<TValues extends Values = Values>(field): void` | `useFieldContext(): FieldInstance<Values>` | 提供 / 读取 `useField()` 返回的字段控制器。Provider 不创建字段、不接管订阅;缺失时 Reader 抛出指向 `createFieldContext(field)` 的 `Error`。 |
|
|
733
|
+
| `createFormConfigContext<T>(props: FormContextProps<T>): void` | `useFormConfigContext(): FormContextProps` | 提供 / 读取表单展示配置。Provider 不复制、不更新传入对象;缺失时 Reader 抛出带 `createFormConfigContext(props)` 指引的 `Error`。 |
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
import {
|
|
737
|
+
createFormConfigContext,
|
|
738
|
+
createFormContext,
|
|
739
|
+
useForm,
|
|
740
|
+
type FormContextProps,
|
|
741
|
+
type SchemxField,
|
|
742
|
+
} from "@schemx/vue"
|
|
743
|
+
|
|
744
|
+
type ProfileValues = { nickname: string }
|
|
745
|
+
const schemas: SchemxField<ProfileValues>[] = [
|
|
746
|
+
{ name: "nickname", label: "昵称", componentType: "input" },
|
|
747
|
+
]
|
|
748
|
+
const form = useForm<ProfileValues>({ schemas })
|
|
749
|
+
const config: FormContextProps<ProfileValues> = {
|
|
750
|
+
schemas,
|
|
751
|
+
readonly: false,
|
|
752
|
+
validationTrigger: "blur",
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
createFormContext(form)
|
|
756
|
+
createFormConfigContext(config)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
在后代组件的 `setup()` 中只读取所需 Context:
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
import { useFieldContext, useFormConfigContext, useFormContext } from "@schemx/vue"
|
|
763
|
+
|
|
764
|
+
type ProfileValues = { nickname: string }
|
|
765
|
+
const form = useFormContext<ProfileValues>()
|
|
766
|
+
const formConfig = useFormConfigContext()
|
|
767
|
+
const field = useFieldContext()
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### `useField`
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
function useField<TValues extends Values = Values>(
|
|
774
|
+
name: NamePath<TValues>
|
|
775
|
+
): FieldInstance<TValues>
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Hook 从 `useFormContext()` 取得表单,为 `name` 创建 Core 字段控制器,再用 Vue Ref 桥接状态。它保留 Core `SchemxFieldInstance` 的全部成员,并增加 `value: Ref<FieldValue<...> | undefined>`、`error: Readonly<Ref<string[] | undefined>>`、`dirty: Readonly<Ref<boolean>>` 与 `pending: Readonly<Ref<boolean>>`;`dirty` 实际读取 Core `isTouched()`。`getValue()` 被覆盖为读取 `value.value`。Core 方法的完整签名见 [Core 单字段控制器](../core#单字段控制器)。
|
|
779
|
+
|
|
780
|
+
`useField()` 的缓存第 1 层按 `form` 实例引用区分,第 2 层直接以传入的 `name` 作为 `Map` key。因此字符串路径按相同字符串值共享桥接订阅;数组 `NamePath` 只在复用同一数组引用时共享,内容相同但新建的数组会生成不同缓存条目。共享条目使用引用计数:组件卸载时计数减 1,归零后取消 effect 并移除缓存。`FieldInstance` 是内部返回类型名,未从根入口公开,请让 TypeScript 从 `useField()` 推导。
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
import { createFieldContext, useField } from "@schemx/vue"
|
|
784
|
+
|
|
785
|
+
type ProfileValues = { nickname: string }
|
|
786
|
+
const nickname = useField<ProfileValues>("nickname")
|
|
787
|
+
createFieldContext(nickname)
|
|
788
|
+
nickname.setValue("Schemx")
|
|
789
|
+
await nickname.validate()
|
|
790
|
+
console.log(nickname.value.value, nickname.error.value)
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
`createFieldContext(field)` 将这一返回值提供给后代并返回 `void`;`useFieldContext()` 无参数,返回最近祖先提供的同一控制器。它们不会替代表单 Context。
|
|
794
|
+
|
|
795
|
+
### `FormContextProps` 的当前边界
|
|
796
|
+
|
|
797
|
+
`FormContextProps<T>` 保留 `schemas`、`initialValues`、展示默认值、`defaultRendererType`、`validatorRegistry`、`class` 和 `style`,排除 `form`、`modelValue`、`rendererRegistry` 以及 4 个表单回调。当前类型定义的 `Omit` 键中存在 `defaultRendererType` 和 `rulesRegistery` 两个拼写错误,因此类型层并未排除真实的 `defaultRendererType` 与 `validatorRegistry`;`<Schemx>` 运行时则用正确名称剔除它们后再提供。字段展示的实际回退边界见前文 [Props](#props)。
|
|
798
|
+
|
|
799
|
+
### `useWatch`、`useWatchField`、`useWatchFields` 与 `useWatchAll`
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
function useWatch<T extends Values>(
|
|
803
|
+
callback: WatchAllCallback<T>,
|
|
804
|
+
options?: CreateWatchOptions
|
|
805
|
+
): () => void
|
|
806
|
+
function useWatch<T extends Values>(
|
|
807
|
+
name: NamePath<T>,
|
|
808
|
+
callback: WatchFieldCallback<T>,
|
|
809
|
+
options?: CreateWatchOptions
|
|
810
|
+
): () => void
|
|
811
|
+
function useWatch<T extends Values>(
|
|
812
|
+
names: NamePath<T>[],
|
|
813
|
+
callback: WatchFieldsCallback<T>,
|
|
814
|
+
options?: CreateWatchOptions
|
|
815
|
+
): () => void
|
|
816
|
+
|
|
817
|
+
function useWatchField<T extends Values = Values>(
|
|
818
|
+
name: NamePath<T>,
|
|
819
|
+
callback: WatchFieldCallback<T>,
|
|
820
|
+
options?: CreateWatchOptions
|
|
821
|
+
): () => void
|
|
822
|
+
function useWatchFields<T extends Values>(
|
|
823
|
+
names: NamePath<T>[],
|
|
824
|
+
callback: WatchFieldsCallback<T>,
|
|
825
|
+
options?: CreateWatchOptions
|
|
826
|
+
): () => void
|
|
827
|
+
function useWatchAll<T extends Values = Values>(
|
|
828
|
+
callback: WatchAllCallback<T>,
|
|
829
|
+
options?: CreateWatchOptions
|
|
830
|
+
): () => void
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
`useWatch` 按参数形状分发,3 个语义化函数分别对应单字段、多字段和全表重载。它们都从 `useFormContext()` 读取表单,返回可手动调用的取消函数,并在组件卸载时自动取消。`options.immediate` 和 `options.inequality` 默认均为 `false`;后者使用深比较跳过相等值。
|
|
834
|
+
|
|
835
|
+
| 模式 | callback 第 1 个参数 | callback 第 2 个参数 |
|
|
836
|
+
| ------ | ------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
837
|
+
| 单字段 | `{ value, prevValue }`;`immediate` 首次的 `prevValue` 为 `undefined`。 | 变化后的全表快照。 |
|
|
838
|
+
| 多字段 | `{ changedPaths, changedValues, prevValues }`;`immediate` 时路径为全部传入路径、`prevValues` 为 `{}`。 | 变化后的全表快照。 |
|
|
839
|
+
| 全表 | 与多字段相同;`immediate` 时 `changedPaths` 为 `[]`。 | 变化后的全表快照。 |
|
|
840
|
+
|
|
841
|
+
当前 Core 全表实现在 effect 中读取无追踪的 `getFieldsSnapshot()`,因此 `useWatchAll()` 与 `useWatch(callback)` 通常不会在后续字段变化时再执行;`immediate: true` 仍会同步执行 1 次。需要持续监听时应使用 `useWatchField()` 或 `useWatchFields()`。
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
import { useWatch, useWatchAll, useWatchField, useWatchFields } from "@schemx/vue"
|
|
845
|
+
|
|
846
|
+
type ProfileValues = { firstName: string; lastName: string }
|
|
847
|
+
useWatch<ProfileValues>("firstName", ({ value, prevValue }) => {
|
|
848
|
+
console.log(prevValue, value)
|
|
849
|
+
})
|
|
850
|
+
useWatchField<ProfileValues>(
|
|
851
|
+
"firstName",
|
|
852
|
+
({ value, prevValue }, snapshot) => console.log(prevValue, value, snapshot),
|
|
853
|
+
{ immediate: true, inequality: true }
|
|
854
|
+
)
|
|
855
|
+
const stop = useWatchFields<ProfileValues>(["firstName", "lastName"], (payload) => {
|
|
856
|
+
console.log(payload.changedPaths, payload.changedValues)
|
|
857
|
+
})
|
|
858
|
+
useWatchAll<ProfileValues>(
|
|
859
|
+
(payload, snapshot) => {
|
|
860
|
+
console.log(payload.changedPaths, snapshot)
|
|
861
|
+
},
|
|
862
|
+
{ immediate: true }
|
|
863
|
+
)
|
|
864
|
+
stop()
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### `useDictionary` 摘要
|
|
868
|
+
|
|
869
|
+
签名为 `useDictionary<TValues, TName>(options, fieldName?): UseDictionaryReturn`。它通过 `useFormContext()` 取得表单,在 `onMounted` 时按 `immediate` 决定是否加载,并通过 `useWatchFields()` 自动清理依赖订阅。完整参数、返回值、竞态边界与示例见前文 [`useDictionary`](#usedictionary),这里不重复维护第 2 份定义。
|
|
870
|
+
|
|
871
|
+
### `useEffect`
|
|
872
|
+
|
|
873
|
+
```ts
|
|
874
|
+
function useEffect(callback: EffectCallback): CreateEffectReturn
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
`callback` 立即执行并追踪其内读取的 Core reactive value;它可返回 cleanup,在下次重跑前或停止时执行。返回值是 dispose 函数,Hook 在组件卸载时自动调用,也可手动提前调用。它不读取表单 Context,所需实例由 callback 闭包提供。
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
import { useEffect, useFormContext } from "@schemx/vue"
|
|
881
|
+
|
|
882
|
+
const form = useFormContext<{ nickname: string }>()
|
|
883
|
+
const stop = useEffect(() => console.log(form.getFieldValue("nickname")))
|
|
884
|
+
stop()
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### `useStableRef`
|
|
888
|
+
|
|
889
|
+
```ts
|
|
890
|
+
function useStableRef<T extends Record<string, any>>(
|
|
891
|
+
factory: () => T
|
|
892
|
+
): Readonly<ShallowRef<T>>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Hook 在 Vue `watchEffect` 中执行 `factory` 并追踪 Vue 依赖。只有工厂结果与当前值浅比较不等时才替换 Ref 引用。它不使用 Schemx Context,`watchEffect` 由当前 Vue scope 管理。
|
|
896
|
+
|
|
897
|
+
```ts
|
|
898
|
+
import { computed } from "vue"
|
|
899
|
+
import { useStableRef } from "@schemx/vue"
|
|
900
|
+
|
|
901
|
+
const disabled = computed(() => false)
|
|
902
|
+
const stableProps = useStableRef(() => ({ disabled: disabled.value }))
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### `useViewSchemas`
|
|
906
|
+
|
|
907
|
+
```ts
|
|
908
|
+
function useViewSchemas<T extends Values = Values>(
|
|
909
|
+
form: SchemxInstance<T>
|
|
910
|
+
): ShallowRef<readonly SchemxViewSchema<T>[]>
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
Hook 不读取 Context,必须显式传入表单实例。它以 `form.getViewSchemas()` 初始化 `shallowRef`,通过 `form.subscribeViewSchemas()` 更新;当前 Vue effect scope 销毁时自动 unsubscribe。
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
import { useForm, useViewSchemas } from "@schemx/vue"
|
|
917
|
+
|
|
918
|
+
const form = useForm({
|
|
919
|
+
schemas: [{ name: "nickname", label: "昵称", componentType: "input" }],
|
|
920
|
+
})
|
|
921
|
+
const viewSchemas = useViewSchemas(form)
|
|
922
|
+
console.log(viewSchemas.value)
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
## FormItem 与 FormGroup
|
|
926
|
+
|
|
927
|
+
### `FormItem`
|
|
928
|
+
|
|
929
|
+
`FormItem` 只接受一个必填 Prop:
|
|
930
|
+
|
|
931
|
+
| Prop | 类型 | 说明 |
|
|
932
|
+
| -------- | --------------------- | ----------------------------------------------------------------- |
|
|
933
|
+
| `schema` | `SchemxViewSchema<T>` | core 已解析完成的字段或分组 ViewSchema,不是原始 `SchemxField<T>` |
|
|
934
|
+
|
|
935
|
+
传入分组 ViewSchema 时,`FormItem` 委托给 `FormGroup`;传入普通字段 ViewSchema 时,它创建字段控制器、提供字段上下文、查找 Renderer,并处理标签、内容、错误、校验触发和可见性。
|
|
936
|
+
|
|
937
|
+
它支持与 `Schemx` 一致的动态 Slots:`{name}`、`{name}Label`、`{name}Content`、`{name}Error` 和 `{name}:{slotName}`。完整 Slot Props 见 [Schemx 组件的 Slots](#slots)。
|
|
938
|
+
|
|
939
|
+
`FormItem` 不能脱离上下文单独工作:普通字段至少需要祖先同步提供 `SchemxInstance` 和 `FormContextProps`,通常直接放在 `<Schemx>` 内部。若自定义 adapter 直接使用它,需要同时调用 `createFormContext(form)` 与 `createFormConfigContext(config)`。传给它的 Schema 应来自 `form.getViewSchemas()` 或 `useViewSchemas(form)`,不要把尚未编译的 dependency Schema 直接传入。
|
|
940
|
+
|
|
941
|
+
### `FormGroup`
|
|
942
|
+
|
|
943
|
+
`FormGroup` 同样只有一个必填 Prop:
|
|
944
|
+
|
|
945
|
+
| Prop | 类型 | 说明 |
|
|
946
|
+
| -------- | -------------------------- | --------------------------------------- |
|
|
947
|
+
| `schema` | `SchemxViewGroupSchema<T>` | 包含已解析 `children` 的分组 ViewSchema |
|
|
948
|
+
|
|
949
|
+
它会把收到的全部 Slots 原样传给子级 `FormItem`。`collapsible` 控制标题是否可点击和可通过 Enter / Space 切换;`defaultCollapsed` 只用于初始化内部折叠状态。`label` 为空时不渲染标题。Vue 源码的 `types/schemx.d.ts` 通过声明合并为分组 Schema 增加了 `class` 和 `style`,但当前发布根声明没有带入这份 augmentation,普通消费者不能把它们视为稳定的根类型契约。运行时读取 `class` 并添加到 `.schemx-group` 根元素;`style` 即使传入,`FormGroup` 也没有把它绑定到根元素。子级字段仍要求表单实例和展示配置上下文。
|
|
950
|
+
|
|
951
|
+
`FormItem` 和 `FormGroup` 是构建自定义 Vue adapter 或重排 ViewSchemas 时的底层组件。一般业务表单优先使用 `<Schemx>`;只有在需要自定义整体布局、分区或容器时才直接组合这两个组件。
|
|
952
|
+
|
|
953
|
+
## Registry
|
|
954
|
+
|
|
955
|
+
Vue 包自有 2 个模块级单例:
|
|
956
|
+
|
|
957
|
+
| 导出 | 真实类型 | 初始内容 | 与 `useForm()` 的关系 |
|
|
958
|
+
| ------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
959
|
+
| `rendererRegistry` | `RendererRegistryType<SchemxRendererKey>` | 空 Registry,默认 renderer key 预设为 `"input"`;未注册 `input` 时仍无法取得组件。 | 未传 `options.rendererRegistry` 时使用该单例。 |
|
|
960
|
+
| `validatorRegistry` | `ValidatorsRegistryType` | 模块初始化时为空。首次被表单采用时,Core 会写入并覆盖 `required`、`selectRequired`、`uploadRequired` 3 个内置规则工厂。 | 未传 `options.validatorRegistry` 时使用该单例。 |
|
|
961
|
+
|
|
962
|
+
Renderer Registry 包含 `register`、`registerAll`、`getRenderer`、`hasRenderer`、`unregister`、`getTypes`、`setDefault`、`getDefault`、`clear` 和 `size`;Validators Registry 包含 `register`、`registerAll`、`get`、`resolveValidatorsBySchema`、`has`、`unregister`、`getNames`、`clear` 和 `size`。完整签名见 [Core Registry 与 Validator](../core#registry-与-validator)。
|
|
963
|
+
|
|
964
|
+
```ts
|
|
965
|
+
import { markRaw } from "vue"
|
|
966
|
+
|
|
967
|
+
import { rendererRegistry, validatorRegistry, type StandardSchemaV1 } from "@schemx/vue"
|
|
968
|
+
|
|
969
|
+
import InputRenderer from "./InputRenderer.vue"
|
|
970
|
+
|
|
971
|
+
rendererRegistry.register("input", markRaw(InputRenderer))
|
|
972
|
+
|
|
973
|
+
const phoneRule: StandardSchemaV1<string> = {
|
|
974
|
+
"~standard": {
|
|
975
|
+
version: 1,
|
|
976
|
+
vendor: "app",
|
|
977
|
+
validate(value) {
|
|
978
|
+
return typeof value === "string" && /^1\d{10}$/.test(value)
|
|
979
|
+
? { value }
|
|
980
|
+
: { issues: [{ message: "手机号格式错误" }] }
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
validatorRegistry.register("phone", phoneRule)
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
全局 Registry 由所有表单共享。表单独立 Registry 则由 Core 工厂新建,并通过 `useForm()` 选项或 `<Schemx>` Props 传入:
|
|
988
|
+
|
|
989
|
+
```ts
|
|
990
|
+
import { createRendererRegistry, createValidatorsRegistry, useForm } from "@schemx/vue"
|
|
991
|
+
|
|
992
|
+
const renderers = createRendererRegistry("input")
|
|
993
|
+
const validators = createValidatorsRegistry()
|
|
994
|
+
const form = useForm({ rendererRegistry: renderers, validatorRegistry: validators })
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
`createRendererRegistry()` 和 `createValidatorsRegistry()` 是 Core 传递导出,不是 Vue 自有 API。独立 Validators Registry 创建时为空,交给 `useForm()` 后 Core 仍会写入 3 个内置规则。
|
|
998
|
+
|
|
999
|
+
## 类型参考
|
|
1000
|
+
|
|
1001
|
+
Vue 根入口自有 5 个公开类型:
|
|
1002
|
+
|
|
1003
|
+
| 类型 | 定义与用途 |
|
|
1004
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
1005
|
+
| `FormContextProps<TValues>` | 表单展示 Context 类型。当前类型与 `<Schemx>` 运行时 omit 的差异见 [Composition API](#formcontextprops-的当前边界)。 |
|
|
1006
|
+
| `SchemxDictionary<TValues, R>` | 函数式选项源配置;`R` 从 `api` 传递到 `formatter`。见 [Dictionary](#dictionary)。 |
|
|
1007
|
+
| `SchemxInstallOptions` | 空接口。`app.use(Schemx, options)` 接受它,但当前没有配置项,运行时忽略 options。 |
|
|
1008
|
+
| `SchemxWithDictionary<A, TValues>` | `A & { dict?: SchemxDictionary<TValues> }`,只增加可选 `dict`。见 [WithRemoteOptions](#withremoteoptions)。 |
|
|
1009
|
+
| `UseDictionaryReturn` | `{ list, loading, error, loadDict, refresh, mutate }`,各成员类型见 [`useDictionary`](#usedictionary)。 |
|
|
1010
|
+
|
|
1011
|
+
`packages/vue/src/types/index.ts` 内部还导出 `FieldInstance` 和 `SchemxFormProps`,但它们未从 `@schemx/vue` 根入口公开。不要从 `@schemx/vue/src/*` 或 `@schemx/vue/dist/*` 深层导入;应让 TypeScript 从 Hook 和组件推导。
|
|
1012
|
+
|
|
1013
|
+
## Core API
|
|
1014
|
+
|
|
1015
|
+
`@schemx/vue` 通过 `export * from "@schemx/core"` 完整传递 Core 的 81 个命名导出(20 个运行时值 + 61 个类型)。它们仍是 Core API,不是 Vue 自有 Composition API。签名与语义见 [Core README](../core),导出基线见 [Core 完整导出清单](../core#完整导出清单)。下节仍逐项列出名称,以便机械核对 Vue 根入口。
|
|
1016
|
+
|
|
1017
|
+
## 完整导出清单
|
|
1018
|
+
|
|
1019
|
+
当前根入口共有 109 项导出:108 个命名导出和 `default`。命名导出由 42 个运行时值与 66 个类型组成;若工具把 `default` 归入值,则显示为 43 个值 + 66 个类型。按来源是 Vue 自有 22 个命名运行时值 + 5 个类型 + `default`,以及 Core 传递的 81 个命名导出。
|
|
1020
|
+
|
|
1021
|
+
### Vue 自有导出
|
|
1022
|
+
|
|
1023
|
+
| 分类 | 导出 | 用途 |
|
|
1024
|
+
| --------------- | ------------------------- | ---------------------------------------------- |
|
|
1025
|
+
| 表单组件 | `schemxForm` | 可安装的表单组件;与 `default` 指向同一对象。 |
|
|
1026
|
+
| 组件 | `FormItem` | 渲染字段或分组 ViewSchema。 |
|
|
1027
|
+
| 组件 | `FormGroup` | 渲染分组 ViewSchema。 |
|
|
1028
|
+
| HOC | `WithRemoteOptions` | 为 Renderer 接入 Dictionary。 |
|
|
1029
|
+
| Registry | `rendererRegistry` | Vue 全局 Renderer Registry。 |
|
|
1030
|
+
| Registry | `validatorRegistry` | Vue 全局 Validators Registry。 |
|
|
1031
|
+
| Hook | `useForm` | 创建并按 Vue scope 销毁表单。 |
|
|
1032
|
+
| Context | `createFormContext` | 提供表单实例。 |
|
|
1033
|
+
| Context | `useFormContext` | 读取表单实例。 |
|
|
1034
|
+
| Hook | `useField` | 创建 Vue 字段控制器。 |
|
|
1035
|
+
| Context | `createFieldContext` | 提供字段控制器。 |
|
|
1036
|
+
| Context | `useFieldContext` | 读取字段控制器。 |
|
|
1037
|
+
| Context | `createFormConfigContext` | 提供表单展示配置。 |
|
|
1038
|
+
| Context | `useFormConfigContext` | 读取表单展示配置。 |
|
|
1039
|
+
| Watch | `useWatch` | 统一分发 Vue Watch。 |
|
|
1040
|
+
| Watch | `useWatchField` | 单字段 Vue Watch。 |
|
|
1041
|
+
| Watch | `useWatchFields` | 多字段 Vue Watch。 |
|
|
1042
|
+
| Watch | `useWatchAll` | 全表 Vue Watch。 |
|
|
1043
|
+
| Dictionary | `useDictionary` | 管理函数式选项源。 |
|
|
1044
|
+
| Effect | `useEffect` | 创建并自动清理 Core effect。 |
|
|
1045
|
+
| Vue 响应式 | `useStableRef` | 建立浅比较稳定 Ref。 |
|
|
1046
|
+
| ViewSchema | `useViewSchemas` | 桥接 ViewSchemas 为 Ref。 |
|
|
1047
|
+
| 默认导出 | `default` | 与 `schemxForm` 严格相等;不计入 22 个命名值。 |
|
|
1048
|
+
| Context 类型 | `FormContextProps` | 表单展示 Context。 |
|
|
1049
|
+
| Dictionary 类型 | `SchemxDictionary` | 函数式选项源配置。 |
|
|
1050
|
+
| 插件类型 | `SchemxInstallOptions` | 当前为空的安装选项。 |
|
|
1051
|
+
| Dictionary 类型 | `SchemxWithDictionary` | 为 Props 增加 `dict`。 |
|
|
1052
|
+
| Dictionary 类型 | `UseDictionaryReturn` | `useDictionary()` 返回值。 |
|
|
1053
|
+
|
|
1054
|
+
根入口没有名为 `SchemxForm` 的命名导出。
|
|
1055
|
+
|
|
1056
|
+
### Core 传递运行时值(20 个)
|
|
1057
|
+
|
|
1058
|
+
| 分类 | 导出 | 用途 |
|
|
1059
|
+
| ------------- | -------------------------- | -------------------------- |
|
|
1060
|
+
| 表单 | `createForm` | 创建 Core 表单。 |
|
|
1061
|
+
| 字段 | `createField` | 创建 Core 字段控制器。 |
|
|
1062
|
+
| Schema source | `createSchemas` | 创建可更新 Schema source。 |
|
|
1063
|
+
| Schema source | `isSchemxSchemas` | 判断 Schema source。 |
|
|
1064
|
+
| Effect | `createEffect` | 创建 Core effect。 |
|
|
1065
|
+
| Watch | `createWatch` | 分发 Core Watch。 |
|
|
1066
|
+
| Watch | `createWatchField` | 单字段 Core Watch。 |
|
|
1067
|
+
| Watch | `createWatchFields` | 多字段 Core Watch。 |
|
|
1068
|
+
| Watch | `createWatchAll` | 全表 Core Watch。 |
|
|
1069
|
+
| Registry | `createRendererRegistry` | 创建 Renderer Registry。 |
|
|
1070
|
+
| Registry | `createValidatorsRegistry` | 创建 Validators Registry。 |
|
|
1071
|
+
| Validator | `createValidator` | 创建底层 Validator。 |
|
|
1072
|
+
| Schema 守卫 | `isBaseSchema` | 判断原始普通字段。 |
|
|
1073
|
+
| Schema 守卫 | `isGroupSchema` | 判断原始 Group。 |
|
|
1074
|
+
| Schema 守卫 | `isDependencySchema` | 判断原始 Dependency。 |
|
|
1075
|
+
| Schema 守卫 | `isBaseResolvedSchema` | 判断解析后普通字段。 |
|
|
1076
|
+
| Schema 守卫 | `isGroupResolvedSchema` | 判断解析后 Group。 |
|
|
1077
|
+
| 路径 | `getByPath` | 读取嵌套路径。 |
|
|
1078
|
+
| 路径 | `setByPath` | 写入嵌套路径。 |
|
|
1079
|
+
| 路径 | `collectObjectPathsByLeaf` | 收集叶子路径。 |
|
|
1080
|
+
|
|
1081
|
+
### Core 传递类型(61 个)
|
|
1082
|
+
|
|
1083
|
+
| 分类 | 导出 | 用途 |
|
|
1084
|
+
| ------------------ | ------------------------------- | ----------------------------------- |
|
|
1085
|
+
| Effect | `CleanupFn` | effect cleanup 函数。 |
|
|
1086
|
+
| Effect | `EffectCallback` | effect callback。 |
|
|
1087
|
+
| Effect | `CreateEffectReturn` | effect dispose 函数。 |
|
|
1088
|
+
| Watch | `CreateWatchOptions` | Watch 选项。 |
|
|
1089
|
+
| Watch | `CreateWatchReturn` | Watch 取消函数。 |
|
|
1090
|
+
| Watch | `WatchFieldCallback` | 单字段 Watch callback。 |
|
|
1091
|
+
| Watch | `WatchFieldsCallback` | 多字段 Watch callback。 |
|
|
1092
|
+
| Watch | `WatchAllCallback` | 全表 Watch callback。 |
|
|
1093
|
+
| 表单 | `CreateFormOptions` | Core 表单创建选项。 |
|
|
1094
|
+
| 表单 | `SchemxInstance` | Core 表单实例接口。 |
|
|
1095
|
+
| 表单 | `SchemxProps` | UI 适配层表单 Props。 |
|
|
1096
|
+
| 表单 | `SchemxGlobalContext` | Core 全局字段默认配置。 |
|
|
1097
|
+
| 表单 | `SchemxContext` | Core 实例级高级上下文。 |
|
|
1098
|
+
| 基础 | `Values` | 表单值基础约束。 |
|
|
1099
|
+
| 基础 | `Dynamic` | 静态值或同步 / 异步值函数。 |
|
|
1100
|
+
| 路径 | `NamePath` | 类型安全字段路径。 |
|
|
1101
|
+
| 路径 | `FieldValue` | 从路径提取字段值。 |
|
|
1102
|
+
| 工具类型 | `DeepReadonly` | 深层只读类型。 |
|
|
1103
|
+
| 工具类型 | `CSSProperties` | CSS 属性类型。 |
|
|
1104
|
+
| Schema source | `SchemxSchemas` | 可更新 Schema source。 |
|
|
1105
|
+
| Schema source | `SchemxSchemasInput` | Schema 数组或 source 联合。 |
|
|
1106
|
+
| Schema source | `SchemxSchemasListener` | Schema source listener。 |
|
|
1107
|
+
| 字段 | `SchemxFieldInstance` | Core 字段控制器。 |
|
|
1108
|
+
| Schema | `SchemxBase` | 普通字段基础接口。 |
|
|
1109
|
+
| Schema | `SchemxBaseField` | 按 Renderer key 分布的字段联合。 |
|
|
1110
|
+
| Schema | `SchemxGroupField` | 原始 Group Schema。 |
|
|
1111
|
+
| Schema | `SchemxDependencyField` | 原始 Dependency Schema。 |
|
|
1112
|
+
| Schema | `SchemxField` | 全部原始 Schema 联合。 |
|
|
1113
|
+
| Schema | `SchemxResolvedField` | 解析后字段 / Group 联合。 |
|
|
1114
|
+
| Schema | `SchemxBaseComponentProps` | Renderer 公共 Props。 |
|
|
1115
|
+
| Schema | `SchemxComponentProps` | Renderer 专属与公共 Props。 |
|
|
1116
|
+
| Schema | `SchemxFormItemProps` | 表单项字段配置。 |
|
|
1117
|
+
| 扩展 | `SchemxFieldDefinition` | 普通字段声明合并接口。 |
|
|
1118
|
+
| 扩展 | `SchemxGroupFieldDefinition` | Group 声明合并接口。 |
|
|
1119
|
+
| 依赖 | `SchemxDependencies` | 字段动态依赖配置。 |
|
|
1120
|
+
| 依赖 | `SchemxConditionFn` | 动态属性条件函数。 |
|
|
1121
|
+
| 依赖 | `SchemxDependenciesStaticProps` | 依赖函数静态返回值映射。 |
|
|
1122
|
+
| ViewSchema | `SchemxViewDebugMeta` | ViewSchema 诊断元数据。 |
|
|
1123
|
+
| ViewSchema | `SchemxViewFieldSchema` | 字段渲染投影。 |
|
|
1124
|
+
| ViewSchema | `SchemxViewGroupSchema` | Group 渲染投影。 |
|
|
1125
|
+
| ViewSchema | `SchemxViewSchema` | 字段 / Group 投影联合。 |
|
|
1126
|
+
| Renderer | `SchemxRendererKey` | Renderer key 类型。 |
|
|
1127
|
+
| Renderer | `SchemxRendererDefinition` | Renderer Props 声明合并接口。 |
|
|
1128
|
+
| Renderer Registry | `RendererRegistryType` | Renderer Registry 实例类型。 |
|
|
1129
|
+
| Renderer Registry | `RegistryOptions` | Renderer 覆盖选项。 |
|
|
1130
|
+
| Renderer Registry | `RendererMap` | Renderer 批量映射。 |
|
|
1131
|
+
| Validator | `Validator` | 底层 Validator 实例类型。 |
|
|
1132
|
+
| Validator | `ValidateResult` | 校验成功 / 失败联合。 |
|
|
1133
|
+
| Validator | `ValidateError` | 校验失败详情。 |
|
|
1134
|
+
| Validator | `FieldError` | 单字段错误。 |
|
|
1135
|
+
| Validator | `ValidationTrigger` | 校验触发时机。 |
|
|
1136
|
+
| Validator | `StandardSchemaV1` | Standard Schema v1 协议。 |
|
|
1137
|
+
| Rule | `SchemxRuleDefinition` | 自定义规则声明合并接口。 |
|
|
1138
|
+
| Rule | `SchemxRuleDefinitionKey` | 声明合并推导的规则 key。 |
|
|
1139
|
+
| Rule | `SchemxRuleBuiltinKey` | 3 个内置规则 key。 |
|
|
1140
|
+
| Rule | `SchemxRules` | Standard Schema、内置或自定义规则。 |
|
|
1141
|
+
| Validator Registry | `ValidatorsRegistryType` | Validators Registry 实例类型。 |
|
|
1142
|
+
| Validator Registry | `ValidatorsRegistryOptions` | Validators 覆盖选项。 |
|
|
1143
|
+
| Validator Registry | `ValidatorsFactory` | 按字段 Schema 生成规则的工厂。 |
|
|
1144
|
+
| Validator Registry | `ValidatorsEntry` | Standard Schema 或工厂联合。 |
|
|
1145
|
+
| Validator Registry | `ValidatorsEntryMap` | 规则名到条目的批量映射。 |
|