Formily 联动关系问题修复:从元素被删除到优雅的显示控制
前言
在使用 Formily 开发动态表单时,我们经常会遇到需要根据某个字段的值来控制其他字段显示/隐藏的场景。最近在开发一个动态表单组件时,遇到了一个棘手的问题:使用 x-visible 控制元素显示时,元素会被完全从 DOM 中移除,导致外层容器 div 仍然存在,形成空节点。本文将详细记录这个问题的修复过程。
问题背景
业务场景
假设我们需要开发一个动态表单组件,该组件包含一个 items 数组字段,数组中的每个项都应该根据 type 字段的值来决定是否显示。具体的映射关系如下:
type = 1: 显示 1 个项(索引 0)type = 2: 显示 2 个项(索引 0-1)type = 3,4,5: 显示 3 个项(索引 0-2)type = 6,7: 显示 4 个项(索引 0-3)
初始实现
最初,我们在 itemContent 层级使用 x-reactions 和 schema['x-visible'] 来控制显示:
items: {
type: 'object',
'x-decorator': 'ArrayItems.Item',
properties: {
itemContent: {
type: 'void',
'x-component': 'FormLayout',
'x-reactions': {
dependencies: [
{
property: 'value',
type: 'any',
source: 'typeField',
name: 'type',
},
],
fulfill: {
schema: {
'x-visible': `{{(() => {
// 获取索引和 type 的逻辑
return index < maxIndex;
})()}}`,
},
},
},
},
},
}
遇到的问题
-
空节点问题:使用
x-visible: false时,虽然itemContent被隐藏了,但是ArrayItems.Item的容器 div(ant-formily-array-items-item-inner)仍然存在于 DOM 中,形成空节点。 -
元素被删除:当需要保留 DOM 元素时(比如需要保留数据),
x-visible会完全移除元素,不符合需求。
解决方案探索
方案一:在 items 层级控制显示
既然在 itemContent 层级控制会导致外层容器仍然存在,那么我们应该在 items 层级直接控制整个 ArrayItems.Item 的显示。
items: {
type: 'object',
'x-decorator': 'ArrayItems.Item',
'x-reactions': {
dependencies: [
{
property: 'value',
type: 'any',
source: 'typeField',
name: 'type',
},
],
fulfill: {
schema: {
'x-visible': `{{...}}`,
},
},
},
properties: {
itemContent: {
// ...
},
},
}
这个方案解决了空节点问题,但是仍然存在元素被删除的问题。
方案二:使用 state.display 控制
Formily 提供了 state.display 来控制元素的显示/隐藏,这种方式不会删除 DOM 元素,只是通过 CSS 的 display 属性来控制。
fulfill: {
state: {
display: `{{index < maxIndex ? 'visible' : 'hidden'}}`,
},
}
最终实现
索引获取方式
在 Formily 中,获取数组项的索引有多种方式:
-
使用
$self.address.segments:let index = -1; if ($self.address?.segments) { const itemsIndex = $self.address.segments.findIndex(seg => seg === 'items'); if (itemsIndex !== -1 && $self.address.segments[itemsIndex + 1] !== undefined) { const indexValue = $self.address.segments[itemsIndex + 1]; index = typeof indexValue === 'number' ? indexValue : parseInt(indexValue, 10); } } -
使用
$self.path(更推荐):const pathMatch = $self.path?.match(/items\.(\d+)/); if (pathMatch) { const index = parseInt(pathMatch[1], 10); }
完整的实现代码
items: {
type: 'object',
'x-decorator': 'ArrayItems.Item',
'x-reactions': {
dependencies: [
{
property: 'value',
type: 'any',
source: 'typeField',
name: 'type',
},
],
fulfill: {
state: {
display: `{{(() => {
// type 与最大显示索引的映射关系
const typeMap = {
1: 1, // type=1 时显示 1 个项
2: 2, // type=2 时显示 2 个项
3: 3, // type=3 时显示 3 个项
4: 3, // type=4 时显示 3 个项
5: 3, // type=5 时显示 3 个项
6: 4, // type=6 时显示 4 个项
7: 4 // type=7 时显示 4 个项
};
// 从 address.segments 数组中获取索引
let index = -1;
if ($self.address?.segments) {
const itemsIndex = $self.address.segments.findIndex(seg => seg === 'items');
if (itemsIndex !== -1 && $self.address.segments[itemsIndex + 1] !== undefined) {
const indexValue = $self.address.segments[itemsIndex + 1];
index = typeof indexValue === 'number' ? indexValue : parseInt(indexValue, 10);
}
}
// 如果无法获取索引,默认显示
if (index === -1 || isNaN(index)) {
return 'visible';
}
// 从依赖项中获取 type
const type = $deps.type?.value ?? $deps.type;
// 如果 type 未定义,默认显示所有项
if (type === undefined || type === null) {
return 'visible';
}
// 根据 type 确定最大显示索引
const maxIndex = typeMap[type];
// 如果 type 不在映射表中,默认显示所有项
if (maxIndex === undefined) {
return 'visible';
}
// 根据索引和 type 决定是否显示
return index < maxIndex ? 'visible' : 'hidden';
})()}}`,
},
},
},
properties: {
itemContent: {
// ... 其他配置
},
},
}
关键点总结
1. x-visible vs state.display
| 特性 | x-visible | state.display |
|---|---|---|
| DOM 元素 | 完全移除 | 保留在 DOM 中 |
| CSS 控制 | 无 | 通过 display 属性 |
| 适用场景 | 需要完全移除元素 | 需要保留元素和数据 |
| 返回值 | boolean | 'visible'/'hidden' |
2. 层级选择
- 在
items层级控制:可以控制整个ArrayItems.Item,避免空节点 - 在
itemContent层级控制:只能控制内部内容,外层容器仍然存在
3. 错误处理
在实现联动关系时,一定要做好错误处理:
- 索引获取失败时,应该默认显示
- 控制字段(如
type)未定义时,应该默认显示所有项 - 映射表中找不到对应值时,应该有默认行为
4. 索引获取的注意事项
- 使用
$self.address.segments时,要注意索引可能是0,必须使用!== undefined判断,不能用 truthy 判断 - 使用
$self.path时,要注意路径格式,使用正则表达式匹配(如/items\.(\d+)/) - 索引值可能是字符串类型,需要转换为数字(使用
parseInt或类型判断)
最佳实践
-
优先使用
state.display:如果需要保留 DOM 元素和数据,使用state.display而不是x-visible -
在合适的层级控制:根据需求选择合适的层级,避免产生空节点
-
完善的错误处理:确保在异常情况下有合理的默认行为
-
清晰的注释:复杂的联动逻辑应该添加清晰的注释,说明映射关系和判断逻辑
-
使用
$self.path获取索引:相比$self.address.segments,$self.path更直观和可靠
总结
通过这次问题修复,我们深入理解了 Formily 的联动机制:
x-visible会完全移除 DOM 元素,适合需要完全隐藏的场景state.display通过 CSS 控制显示,适合需要保留元素的场景- 在
items层级控制可以避免空节点问题 - 完善的错误处理是保证功能稳定性的关键
希望这篇文章能帮助遇到类似问题的开发者,少走弯路,快速解决问题。
参考资源
如果这篇文章对你有帮助,欢迎点赞和收藏!如有问题,欢迎在评论区讨论。