联动示例
本章聚焦 SchemaReactions 的常见联动写法。为阅读方便,本章节会重复列出联动的关键类型签名,完整的类型签名请查看 SchemaReactions。
ts
type SchemaReaction<Field = any>
= | {
dependencies?: // 依赖的字段路径列表,支持FormPathPattern数据路径语法, 只能以点路径描述依赖,支持相对路径
| Array<
| string // 如果数组里是string,那么读的时候也是数组格式
| {
// 如果数组里是对象, 那么读的时候通过name从$deps获取
name?: string // 从$deps读取时的别名
type?: string // 字段类型
source?: string // 字段路径
property?: string // 依赖属性, 默认为value
}
>
| Record<string, string> // 如果是对象格式,读的时候也是对象格式,只是对象的key相当于别名
when?: string | boolean // 联动条件
target?: string // 要操作的字段路径,支持FormPathPattern匹配路径语法,注意:不支持相对路径!!
effects?: SchemaReactionEffect[] // 主动模式下的独立生命周期钩子
fulfill?: {
// 满足条件
state?: IGeneralFieldState // 更新状态
schema?: ISchema // 更新Schema
run?: string // 执行语句
}
otherwise?: {
// 不满足条件
state?: IGeneralFieldState // 更新状态
schema?: ISchema // 更新Schema
run?: string // 执行语句
}
}
| ((field: Field) => void) // 支持函数, 可以复杂联动内置表达式作用域
内置表达式作用域主要用于在表达式中实现各种联动关系。
| 作用域变量 | 含义 | 常见使用位置 |
|---|---|---|
$self | 当前字段实例 | 普通属性表达式、x-reactions |
$values | 顶层表单数据 | 普通属性表达式、x-reactions |
$form | 当前 Form 实例 | 普通属性表达式、x-reactions |
$observable | 创建响应式对象,语义与 observable 一致 | 复杂联动函数、作用域工具函数 |
$memo | 创建持久引用数据,语义与 autorun.memo 一致 | 复杂联动函数、作用域工具函数 |
$effect | 响应 autorun 首次执行后的微任务与 dispose,语义与 autorun.effect 一致 | 复杂联动函数、作用域工具函数 |
$dependencies | x-reactions 的依赖值集合(与 dependencies 对应) | 被动联动表达式 |
$deps | x-reactions 的依赖值集合(与 dependencies 对应) | 被动联动表达式 |
$target | 主动联动模式中的目标字段实例 | 主动联动表达式 |
用例
主动联动
标准主动联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 可隐藏 target',
},
'x-reactions': {
target: 'target',
when: '{{$self.value === "123"}}',
fulfill: {
state: {
visible: false,
},
},
otherwise: {
state: {
visible: true,
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '被 source 主动联动控制可见性',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
局部表达式分发联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 显示 target(表达式写在 state.visible)',
},
'x-reactions': {
target: 'target',
fulfill: {
state: {
visible: '{{$self.value === "123"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '可见性由表达式决定',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
基于 Schema 协议联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 显示 target(写在 schema.x-visible)',
},
'x-reactions': {
target: 'target',
fulfill: {
schema: {
'x-visible': '{{$self.value === "123"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '通过 schema 协议被联动',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
基于 run 语句联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 让 run 语句显示 target',
},
'x-reactions': {
fulfill: {
run: '$form.setFieldState("target",state=>{state.visible = $self.value === "123"})',
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'visible 由 run 语句控制',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
基于生命周期钩子联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '在输入生命周期触发对 target 的联动',
},
'x-reactions': {
target: 'target',
effects: ['onFieldInputValueChange'],
fulfill: {
state: {
visible: '{{$self.value === "123"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '仅在输入值变化时联动',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
表达式作用域:$self + $values + $form
在主动联动里,从 source 字段更新 hint 字段内容,展示 3 个内置作用域值。
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { defineComponent, h } from 'vue'
const InputBox = defineComponent({
name: 'InputBox',
props: {
modelValue: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '',
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const handleInput = (event: Event) => {
const nextValue = (event.target as HTMLInputElement).value
emit('update:modelValue', nextValue)
}
return () =>
h('input', {
value: props.modelValue ?? '',
placeholder: props.placeholder,
onInput: handleInput,
style: {
width: '100%',
maxWidth: '360px',
border: '1px solid var(--vp-c-divider)',
borderRadius: '8px',
padding: '8px 10px',
fontSize: '14px',
},
})
},
})
const PreviewBlock = defineComponent({
name: 'PreviewBlock',
props: {
text: {
type: [String, Number],
default: '-',
},
},
setup(props) {
return () =>
h(
'div',
{
style: {
marginTop: '8px',
border: '1px dashed var(--vp-c-divider)',
borderRadius: '8px',
padding: '10px 12px',
fontSize: '13px',
lineHeight: '1.5',
},
},
String(props.text ?? '-'),
)
},
})
const { SchemaField } = createSchemaField({
components: {
InputBox,
PreviewBlock,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入任意值,观察下方作用域变量输出',
},
'x-reactions': {
target: 'hint',
fulfill: {
schema: {
'x-component-props.text': `{{$self.value
? '$self.value=' + $self.value + ' | $values.source=' + $values.source + ' | hasForm=' + !!$form
: '请输入 source 字段'}}`,
},
},
},
},
hint: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: '请输入 source 字段',
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>请输入 source 字段
{}查看源码
表达式作用域:$target 回退值
在主动联动里,source 更新 target;当 source 为空时回退到 $target.value,保留目标字段当前值。
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { defineComponent, h } from 'vue'
const InputBox = defineComponent({
name: 'InputBox',
props: {
modelValue: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '',
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const handleInput = (event: Event) => {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
return () =>
h('input', {
value: props.modelValue ?? '',
placeholder: props.placeholder,
onInput: handleInput,
style: {
width: '100%',
maxWidth: '360px',
border: '1px solid var(--vp-c-divider)',
borderRadius: '8px',
padding: '8px 10px',
fontSize: '14px',
},
})
},
})
const PreviewBlock = defineComponent({
name: 'PreviewBlock',
props: {
text: {
type: [String, Number],
default: '-',
},
},
setup(props) {
return () =>
h(
'div',
{
style: {
marginTop: '8px',
border: '1px dashed var(--vp-c-divider)',
borderRadius: '8px',
padding: '10px 12px',
fontSize: '13px',
lineHeight: '1.5',
},
},
String(props.text ?? '-'),
)
},
})
const { SchemaField } = createSchemaField({
components: {
InputBox,
PreviewBlock,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入后会主动更新 target',
},
'x-reactions': {
target: 'target',
effects: ['onFieldInputValueChange'],
fulfill: {
state: {
value: `{{$self.value ? $self.value + '-from-source' : $target.value}}`,
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '可手动输入;清空 source 时将保留当前值',
},
},
notice: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'source 为空时,表达式走 $target.value(即保留 target 当前值)',
},
},
},
}
const form = createForm({
values: {
target: 'manual-default',
},
})
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>source 为空时,表达式走 $target.value(即保留 target 当前值)
{
"target": "manual-default"
}查看源码
被动联动
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 显示 target(被动依赖)',
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '读取 $deps[0] 判断可见性',
},
'x-reactions': {
dependencies: ['source'],
fulfill: {
schema: {
'x-visible': '{{$deps[0] === "123"}}',
},
},
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
相邻元素联动(数组项内)
数组项里的同级字段推荐用被动依赖实现,例如当前行 target 依赖当前行 .source。
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { ArrayItems, InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
ArrayItems,
InputBox,
},
})
const schema = {
type: 'object',
properties: {
rows: {
'type': 'array',
'title': 'rows',
'x-component': 'ArrayItems',
'items': {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '行内 source,输入 123 控制同一行 target',
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '只受同一行 source 影响(依赖 .source)',
},
'x-reactions': {
dependencies: ['.source'],
fulfill: {
schema: {
'x-visible': '{{$deps[0] === "123"}}',
},
},
},
},
},
},
},
},
}
const form = createForm({
values: {
rows: [{}],
},
})
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>Row 1
{
"rows": [
{}
]
}查看源码
表达式作用域:$deps + $dependencies
在被动联动里,summary 依赖 price/count,通过表达式同时读取 $deps 和 $dependencies。
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { defineComponent, h } from 'vue'
const InputBox = defineComponent({
name: 'InputBox',
props: {
modelValue: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '',
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const handleInput = (event: Event) => {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
return () =>
h('input', {
value: props.modelValue ?? '',
placeholder: props.placeholder,
onInput: handleInput,
style: {
width: '100%',
maxWidth: '360px',
border: '1px solid var(--vp-c-divider)',
borderRadius: '8px',
padding: '8px 10px',
fontSize: '14px',
},
})
},
})
const PreviewBlock = defineComponent({
name: 'PreviewBlock',
props: {
text: {
type: [String, Number],
default: '-',
},
},
setup(props) {
return () =>
h(
'div',
{
style: {
marginTop: '8px',
border: '1px dashed var(--vp-c-divider)',
borderRadius: '8px',
padding: '10px 12px',
fontSize: '13px',
lineHeight: '1.5',
},
},
String(props.text ?? '-'),
)
},
})
const { SchemaField } = createSchemaField({
components: {
InputBox,
PreviewBlock,
},
})
const schema = {
type: 'object',
properties: {
price: {
'type': 'string',
'title': 'price',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '单价,例如 12.5',
},
},
count: {
'type': 'string',
'title': 'count',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '数量,例如 3',
},
},
summary: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: '等待 price / count 输入',
},
'x-reactions': {
dependencies: ['price', 'count'],
fulfill: {
schema: {
'x-component-props.text': `{{'$deps[0]=' + ($deps[0] || 0) + ', $deps[1]=' + ($deps[1] || 0) + ', $dependencies[0]=' + ($dependencies[0] || 0) + ', total=' + ((Number($deps[0]) || 0) * (Number($deps[1]) || 0))}}`,
},
},
},
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>$deps[0]=0, $deps[1]=0, $dependencies[0]=0, total=0
{}查看源码
复杂联动
<script setup lang="ts">
import type { GeneralField } from '@formily/core'
import { createForm, isField } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox, PreviewBlock } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
PreviewBlock,
},
})
function myReaction(field: GeneralField) {
const sourceField = field.query('source').take()
const sourceValue = isField(sourceField) ? sourceField.value : undefined
field.visible = sourceValue === '123'
field.componentProps = {
...(field.componentProps || {}),
text: sourceValue === '123'
? 'myReaction 命中:target 可见'
: 'myReaction 未命中:target 隐藏',
}
}
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入 123 触发外部 myReaction',
},
},
target: {
'type': 'void',
'title': 'target',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'myReaction 初始状态',
},
'x-reactions': '{{myReaction}}',
},
},
}
const form = createForm()
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" :scope="{ myReaction }" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{}查看源码
组件属性联动
操作状态
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入任意颜色值,例如 mistyrose 或 lightblue',
},
'x-reactions': {
target: 'target',
fulfill: {
state: {
'component[1].style.backgroundColor': '{{$self.value || "white"}}',
} as any,
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '观察背景色变化(state.component 路径)',
},
},
},
}
const form = createForm({
values: {
target: 'target preview',
},
})
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{
"target": "target preview"
}查看源码
操作 Schema 协议
<script setup lang="ts">
import { createForm } from '@formily/core'
import { createSchemaField, FormConsumer, FormProvider } from '@silver-formily/vue'
import { InputBox } from './shared'
const { SchemaField } = createSchemaField({
components: {
InputBox,
},
})
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '输入任意颜色值,例如 mistyrose 或 lightblue',
},
'x-reactions': {
target: 'target',
fulfill: {
schema: {
'x-component-props.style.backgroundColor': '{{$self.value || "white"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: '观察背景色变化(schema 路径)',
},
},
},
}
const form = createForm({
values: {
target: 'target preview',
},
})
</script>
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
<FormConsumer>
<template #default="{ form: currentForm }">
<pre style="margin-top: 10px; white-space: pre-wrap;">{{ JSON.stringify(currentForm.values, null, 2) }}</pre>
</template>
</FormConsumer>
</FormProvider>
</template>{
"target": "target preview"
}查看源码