Linkage Examples
This page focuses on common SchemaReactions patterns. For convenience, the key type signature is repeated here. For the full type definition, see SchemaReactions.
type SchemaReaction<Field = any>
= | {
dependencies?: // dependency path list, supports FormPathPattern syntax; passive dependencies support relative paths
| Array<
| string // string items are exposed as an array when reading from scope
| {
// object items can be read from $deps by alias
name?: string // alias used when reading from $deps
type?: string // field type
source?: string // field path
property?: string // dependent property, defaults to value
}
>
| Record<string, string> // object form is also exposed as an object, with keys acting as aliases
when?: string | boolean // linkage condition
target?: string // target field path, supports FormPathPattern matching syntax; relative paths are not supported
effects?: SchemaReactionEffect[] // lifecycle hooks available in active mode
fulfill?: {
// when the condition matches
state?: IGeneralFieldState // update field state
schema?: ISchema // update schema
run?: string // execute statement
}
otherwise?: {
// when the condition does not match
state?: IGeneralFieldState // update field state
schema?: ISchema // update schema
run?: string // execute statement
}
}
| ((field: Field) => void) // function form for complex linkageBuilt-in Expression Scope
Built-in expression scope is mainly used to build different linkage relationships inside expressions.
| Scope Variable | Meaning | Common Usage |
|---|---|---|
$self | current field instance | property expressions, x-reactions |
$values | top-level form values | property expressions, x-reactions |
$form | current Form instance | property expressions, x-reactions |
$observable | creates reactive objects, equivalent to observable | complex reaction functions, helpers |
$memo | creates persistent references, equivalent to autorun.memo | complex reaction functions, helpers |
$effect | handles microtasks and dispose after autorun first runs | complex reaction functions, helpers |
$dependencies | dependency values in x-reactions, aligned with dependencies | passive linkage expressions |
$deps | dependency values in x-reactions, aligned with dependencies | passive linkage expressions |
$target | target field instance in active linkage mode | active linkage expressions |
Examples
Active Linkage
Standard active linkage
<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: 'Type 123 to hide 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: 'Visibility is controlled by source in active mode',
},
},
},
}
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>{}Partial expression dispatch linkage
<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: 'Type 123 to show target (expression is written in state.visible)',
},
'x-reactions': {
target: 'target',
fulfill: {
state: {
visible: '{{$self.value === "123"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Visibility is decided by the expression',
},
},
},
}
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-based linkage
<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: 'Type 123 to show target (via 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: 'Linked through the schema protocol',
},
},
},
}
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 statement linkage
<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: 'Type 123 to let the run statement show 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 is controlled by the run statement',
},
},
},
}
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>{}Lifecycle-hook-based linkage
<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: 'Trigger target linkage during the input lifecycle',
},
'x-reactions': {
target: 'target',
effects: ['onFieldInputValueChange'],
fulfill: {
state: {
visible: '{{$self.value === "123"}}',
},
},
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Only reacts on input value changes',
},
},
},
}
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>{}Expression scope: $self + $values + $form
In active linkage mode, source updates the content of hint to demonstrate three built-in scope values.
<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: 'Type anything to inspect the scope values below',
},
'x-reactions': {
target: 'hint',
fulfill: {
schema: {
'x-component-props.text': `{{$self.value
? '$self.value=' + $self.value + ' | $values.source=' + $values.source + ' | hasForm=' + !!$form
: 'Please enter the source field'}}`,
},
},
},
},
hint: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'Please enter the source field',
},
},
},
}
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>{}Expression scope: $target fallback value
In active linkage mode, source updates target. When source is empty, it falls back to $target.value and keeps the current 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: 'Typing here actively updates 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: 'You can edit it manually; clearing source keeps the current value',
},
},
notice: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'When source is empty, the expression uses $target.value and keeps the current target value',
},
},
},
}
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>{
"target": "manual-default"
}Passive Linkage
<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: 'Type 123 to show target (passive dependency)',
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Read $deps[0] to decide visibility',
},
'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>{}Neighbor linkage inside array items
For sibling fields inside an array item, passive dependencies are usually clearer. In this example, the current row's target depends on the current row's .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: 'Row source: type 123 to control the target in the same row',
},
},
target: {
'type': 'string',
'title': 'target',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Affected only by source in the same row (depends on .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>{
"rows": [
{}
]
}Expression scope: $deps + $dependencies
In passive linkage mode, summary depends on price/count and reads both $deps and $dependencies in the same expression.
<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: 'Unit price, e.g. 12.5',
},
},
count: {
'type': 'string',
'title': 'count',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Quantity, e.g. 3',
},
},
summary: {
'type': 'void',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'Waiting for price / count input',
},
'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>{}Complex Linkage
<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 matched: target is visible'
: 'myReaction not matched: target is hidden',
}
}
const schema = {
type: 'object',
properties: {
source: {
'type': 'string',
'title': 'source',
'x-component': 'InputBox',
'x-component-props': {
placeholder: 'Type 123 to trigger external myReaction',
},
},
target: {
'type': 'void',
'title': 'target',
'x-component': 'PreviewBlock',
'x-component-props': {
text: 'myReaction initial state',
},
'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>{}Component Prop Linkage
Updating state
<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: 'Enter any CSS color, e.g. mistyrose or 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: 'Observe background color changes (state.component path)',
},
},
},
}
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"
}Updating schema protocol
<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: 'Enter any CSS color, e.g. mistyrose or 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: 'Observe background color changes (schema path)',
},
},
},
}
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"
}