Formily ArrayItems 中的联动控制

37 阅读3分钟

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-reactionsschema['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;
            })()}}`,
          },
        },
      },
    },
  },
}

遇到的问题

  1. 空节点问题:使用 x-visible: false 时,虽然 itemContent 被隐藏了,但是 ArrayItems.Item 的容器 div(ant-formily-array-items-item-inner)仍然存在于 DOM 中,形成空节点。

  2. 元素被删除:当需要保留 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 中,获取数组项的索引有多种方式:

  1. 使用 $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);
      }
    }
    
  2. 使用 $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-visiblestate.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 或类型判断)

最佳实践

  1. 优先使用 state.display:如果需要保留 DOM 元素和数据,使用 state.display 而不是 x-visible

  2. 在合适的层级控制:根据需求选择合适的层级,避免产生空节点

  3. 完善的错误处理:确保在异常情况下有合理的默认行为

  4. 清晰的注释:复杂的联动逻辑应该添加清晰的注释,说明映射关系和判断逻辑

  5. 使用 $self.path 获取索引:相比 $self.address.segments$self.path 更直观和可靠

总结

通过这次问题修复,我们深入理解了 Formily 的联动机制:

  • x-visible 会完全移除 DOM 元素,适合需要完全隐藏的场景
  • state.display 通过 CSS 控制显示,适合需要保留元素的场景
  • items 层级控制可以避免空节点问题
  • 完善的错误处理是保证功能稳定性的关键

希望这篇文章能帮助遇到类似问题的开发者,少走弯路,快速解决问题。

参考资源



如果这篇文章对你有帮助,欢迎点赞和收藏!如有问题,欢迎在评论区讨论。