news 2026/9/24 18:50:32

formily-react ObjectField 组件完全指南:动态对象表单的 ViewModel 桥接与属性增删实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
formily-react ObjectField 组件完全指南:动态对象表单的 ViewModel 桥接与属性增删实战
  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

ObjectField 是 @formily/react 中用于将 @formily/core 的 createObjectField 字段模型(ViewModel)与输入控件做绑定的桥接组件,专治"对象值动态增删属性"这类复杂表单场景。读完本文,你将掌握 ObjectField 的签名与核心属性、两种子组件组织方式(自定义组件与 render props),以及基于addProperty/removeProperty/existProperty实现动态键值编辑器(如自定义 JSON 对象属性面板)的完整实战方案。

组件定位:为什么需要 ObjectField

在 formily 的字段体系中,字段模型分为普通字段(Field)、对象字段(ObjectField)、数组字段(ArrayField)与虚字段(VoidField)等类型。ObjectField 对应值类型为对象的字段:它的value是一个Record<string, any>,因此天然适合表达"一组由用户动态决定键名的属性集合",例如环境变量配置、自定义扩展属性、KV 映射表等。

从源码结构看,ObjectField 是 @formily/core 的ObjectField字段模型在 React 层的封装,其实现位于 packages/react/src/components/ObjectField.tsx:

export const ObjectField = <D extends JSXComponent, C extends JSXComponent>( props: IFieldProps<D, C, ObjectFieldType> ) => { const form = useForm() const parent = useField() const field = useAttach( form.createObjectField({ basePath: parent?.address, ...props }) ) return ( <FieldContext.Provider value={field}> <ReactiveField field={field}>{props.children}</ReactiveField> </FieldContext.Provider> ) }

这段实现清晰展示了桥接链路:

  1. useForm()获取当前FormProvider提供的表单实例,useField()获取父级字段(用于推导basePath);
  2. 通过form.createObjectField({ basePath: parent?.address, ...props })创建(或复用)对象字段模型,并将其挂载到表单字段图上;
  3. 将字段模型放入FieldContext.Provider,供内部嵌套的Field通过useField读取;
  4. 用 ReactiveField 包裹渲染,保证字段状态变化时响应式更新 UI。

在 @formily/core 中,createObjectField会保证对象字段的默认值为{}(而非undefined),相关逻辑见 packages/core/src/models/Form.ts:

createObjectField = <Decorator, Component>(props: IFieldFactoryProps<...>) => { const address = FormPath.parse(props.basePath).concat(props.name) const identifier = address.toString() if (!identifier) return if (!this.fields[identifier] || this.props.designable) { batch(() => { new ObjectField(address, { ...props, value: isObj(props.value) ? props.value : {}, }, this, this.props.designable) }) this.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE) } return this.fields[identifier] }

注意isObj(props.value) ? props.value : {}这一兜底逻辑:即使不传value,字段值也会被规范化为对象,这正是后续Object.keys(field.value || {})遍历安全的前提。

签名与核心属性

type ObjectField = React.FC<React.PropsWithChildren<IFieldFactoryProps>>

ObjectField 的 Props 继承自IFieldFactoryProps(定义于 packages/core/src/types.ts):

export interface IFieldFactoryProps< Decorator extends JSXComponent, Component extends JSXComponent, TextType = any, ValueType = any > extends IFieldProps<Decorator, Component, TextType, ValueType> { name: FormPathPattern // 必填:字段路径,决定字段在表单值树中的位置 basePath?: FormPathPattern // 可选:基准路径,默认取父级字段地址 }

在此基础上,React 层的 IFieldProps 补充了三个渲染相关属性:

属性类型说明
nameFormPathPattern必填,字段在表单值中的路径(如"object""a.b"
basePathFormPathPattern可选,字段的基准路径,默认自动取父级字段地址
childrenRenderPropsChildren<Field>可传普通 ReactNode,也可传(field, form) => ReactNode函数(render props)
decorator[] \| [D] \| [D, props]装饰器组件及其属性(如 FormItem)
component[] \| [C] \| [C, props]输入组件及其属性(如 Input),也可不传、纯靠 children 组织 UI

使用警示:使用 ObjectField 时必须传name属性;同时推荐用 render props 形式组织子组件(这样能直接拿到字段模型做动态控制),这也是下文两个用例的共同基础。

自定义组件用例:用 observer 订阅字段模型

第一种组织方式是自定义组件 +useField。自定义组件通过observer包裹后,即可像订阅普通响应式数据一样订阅字段模型的状态变化。完整用例(来自 ObjectField.zh-CN.md):

import React from 'react' import { createForm, ObjectField as ObjectFieldType } from '@formily/core' import { FormProvider, Field, ObjectField, useField, observer, } from '@formily/react' import { Input, Button, Space } from 'antd' const form = createForm() const ObjectComponent = observer(() => { const field = useField<ObjectFieldType>() return ( <> <div> {Object.keys(field.value || {}).map((key) => ( <div key={key} style={{ display: 'flex-block', marginBottom: 10 }}> <Space> <Field name={key} component={[Input, { placeholder: key }]} /> <Button onClick={() => { field.removeProperty(key) }} > Remove </Button> </Space> </div> ))} </div> <Space> <Field name="propertyName" basePath={''} required component={[Input, { placeholder: 'Property Name' }]} /> <Button onClick={() => { const name = form.values.propertyName if (name && !form.existValuesIn(`${field.path}.${name}`)) { field.addProperty(name, '') form.deleteValuesIn('propertyName') } }} > Add </Button> </Space> </> ) }) export default () => ( <FormProvider form={form}> <ObjectField name="object" component={[ObjectComponent]} /> </FormProvider> )

用例解读

  • useField<ObjectFieldType>():在FieldContext内读取当前对象字段模型,类型标注为 core 层的ObjectField,从而获得addProperty等扩展方法的类型提示;
  • 渲染已有属性Object.keys(field.value || {})遍历对象键,为每个键内嵌一个普通Fieldname={key}相对于当前对象字段路径),并配一个 Remove 按钮;
  • 动态添加属性:表单里放一个临时输入框propertyName(注意basePath={''}使其脱离对象字段路径、挂在表单顶层),点击 Add 时读取form.values.propertyName,用field.addProperty(name, '')写入新属性,再用form.deleteValuesIn('propertyName')清空临时输入;
  • 去重校验form.existValuesIn(${field.path}.${name})用于判断目标路径是否已存在值,避免重复添加同名属性;
  • observerObjectComponent必须用observer包裹,否则新增/删除属性导致field.value变化时,组件不会重新渲染。

Render Props 用例:不写自定义组件也能动态控制

如果不想额外定义组件,可以直接把 children 写成函数,函数参数即字段模型field。此时无需observeruseField,因为 render props 本身就是把字段模型作为渲染数据直接传入。完整用例:

import React from 'react' import { createForm } from '@formily/core' import { FormProvider, Field, ObjectField } from '@formily/react' import { Input, Button, Space } from 'antd' const form = createForm() export default () => ( <FormProvider form={form}> <ObjectField name="object"> {(field) => { return ( <> <div> {Object.keys(field.value || {}).map((key) => ( <div key={key} style={{ display: 'flex-block', marginBottom: 10 }} > <Space> <Field name={key} component={[Input, { placeholder: key }]} /> <Button onClick={() => { field.removeProperty(key) }} > Remove </Button> </Space> </div> ))} </div> <Space> <Field name="propertyName" basePath={''} required component={[Input, { placeholder: 'Property Name' }]} /> <Button onClick={() => { const name = form.values.propertyName if (name && !form.existValuesIn(`${field.path}.${name}`)) { field.addProperty(name, '') form.deleteValuesIn('propertyName') } }} > Add </Button> </Space> </> ) }} </ObjectField> </FormProvider> )

与自定义组件方案的取舍

两种方案的核心逻辑(遍历、增删属性)完全一致,区别在于组织方式:

  • 自定义组件方案:适合属性面板逻辑复杂、需要复用的场景。组件独立成函数,配合observer获得响应式订阅,后续可抽成通用组件;
  • Render Props 方案:适合简单、一次性的动态对象编辑,内联书写、无需额外组件定义,代码更紧凑。

从 ReactiveField 的实现看,两种方式最终汇合到同一条渲染路径:

const renderChildren = (children, field?, form?) => isFn(children) ? children(field, form) : children

ReactiveField会检测 children 是否为函数:若是函数则以(field, form)为参数调用(render props 分支);否则直接作为普通节点渲染(自定义组件分支)。两种分支共享后续的decoratorcomponentdisplay(非visible不渲染)、pattern(disabled/readOnly 映射)等统一渲染逻辑。

源码深处:addProperty 与 removeProperty 的实现原理

用例中反复使用的三个方法定义在 core 层的字段模型 packages/core/src/models/ObjectField.ts:

addProperty = (key: string, value: any) => { this.form.setValuesIn(this.path.concat(key), value) this.additionalProperties.push(key) return this.onInput(this.value) } removeProperty = (key: string) => { this.form.deleteValuesIn(this.path.concat(key)) this.additionalProperties.splice(this.additionalProperties.indexOf(key), 1) return this.onInput(this.value) } existProperty = (key: string) => { return this.form.existValuesIn(this.path.concat(key)) }

三个方法的语义(与 core 层 ObjectField 模型文档 一致):

方法签名行为
addProperty(key: FormPathPattern, value: any) => Promise<void>向对象写入属性(setValuesIn)并触发onInput
removeProperty(key: FormPathPattern) => Promise<void>删除对象属性(deleteValuesIn)并触发onInput
existProperty(key: FormPathPattern) => boolean判断对象中是否存在指定属性

值得关注的是模型构造阶段的自动清理机制(ObjectField.ts):

protected makeAutoCleanable() { this.disposers.push( reaction( () => Object.keys(this.value || {}), (newKeys) => { const filterKeys = this.additionalProperties.filter( (key) => !newKeys.includes(key) ) cleanupObjectChildren(this, filterKeys) } ) ) }

该模型通过reaction监听对象键集合的变化:当某个通过addProperty动态添加的属性键从值中消失(被删除)时,会同步清理该键对应的子字段模型,防止字段图(Form Graph)中残留孤立字段导致状态不同步。这意味着:

  • 通过addProperty/removeProperty增删属性时,子字段会被正确创建/销毁;
  • 直接通过form.setValuesIn/form.deleteValuesIn修改值也能触发同样的清理逻辑,保证字段图与值保持一致。

使用注意事项与最佳实践

  1. name必须传:ObjectField 的name是必填项(类型层面强制name: FormPathPattern),缺失时字段地址无法解析、字段无法挂载到表单;
  2. 优先 render props 组织子组件:把字段模型作为 children 函数的入参,能在声明式 JSX 中直接调用addProperty等方法,避免额外编写observer组件;
  3. 区分对象字段与普通字段:若值结构固定(如一个已知 schema 的对象),用Field+ JSON Schema 即可;只有当键名需要运行时动态增删时才需要 ObjectField,其价值在于addProperty/removeProperty对子字段图的自动维护;
  4. 临时输入框用basePath={''}隔离:动态添加属性时需要一个"属性名输入框",将其basePath设为空串可避免它被错误地嵌套进对象字段的值路径;
  5. 配合响应式渲染:若采用自定义组件方案,务必用observer包裹;render props 方案则由 ReactiveField 内置的响应式机制保证更新,无需手动处理。

小结

ObjectField 是 formily 对象类型字段在 React 层的标准桥接组件:它把 core 层的对象字段模型通过FieldContext注入到子组件树,并借助ReactiveField完成响应式渲染。配合addProperty/removeProperty/existProperty三个扩展方法与自动清理机制,可以高效实现"属性动态增删"的复杂编辑场景。本文的两个完整用例(自定义组件 + render props)均可直接复制运行,适合作为自定义 KV 编辑器、动态扩展属性面板等功能的基础模板。

  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:50:31

Jira替代方案选型指南:Gitee等国产研发管理工具对比与落地实践

1. 研发管理工具选型的底层逻辑与市场格局 1.1 为什么“替代 Jira”这件事突然变得紧迫 做研发管理的朋友这两年应该都有一个明显感受&#xff1a;团队里讨论“要不要换掉 Jira”的频率越来越高。原因其实不复杂&#xff0c;我把它拆成三层来看。 第一层是 成本与合规 。Ji…

作者头像 李华
网站建设 2026/9/24 18:50:19

睡觉忘了摘隐形,第二天眼睛会不会出事?/钟祥极博视科普

一、先说个咱钟祥街坊的日常前两天在莫愁大道那边遛弯&#xff0c;碰到位大姐&#xff0c;一边揉眼睛一边跟我唠&#xff1a;昨晚看电视看着看着睡着了&#xff0c;早上起来才想起隐形还戴在眼里&#xff0c;一睁眼又干又涩&#xff0c;心里直打鼓。这事儿真不少见。咱们钟祥这…

作者头像 李华
网站建设 2026/9/24 18:49:33

2026年AI数据资产管理平台厂商全景梳理与企业选型指南

在数据要素与大模型加速融合的背景下&#xff0c;企业的数据管理对象正在从传统的表、字段&#xff0c;扩展到数据集、模型、提示词、向量库与多模态内容&#xff0c;AI数据资产管理平台也由此成为企业数据建设的新焦点。根据 IDC《中国数据治理平台市场份额》研究&#xff0c;…

作者头像 李华
网站建设 2026/9/24 18:48:58

Linux tar命令详解:归档、压缩与解压实用技巧

1. tar命令的本质&#xff1a;它跟压缩其实是两码事 我刚用Linux那会儿&#xff0c;一直以为tar就是个“压缩解压命令”&#xff0c;后来才发现这个理解偏差会带来不少问题。其实tar全称是Tape Archive&#xff0c;最早的设计目标是把一堆文件打包到磁带这种顺序存储设备上&…

作者头像 李华
网站建设 2026/9/24 18:48:17

ST-GCN动作识别全链路实战:从图构建到实时部署

简介&#xff1a;本资源是一套基于时空图卷积网络&#xff08;ST-GCN&#xff09;的骨骼动作识别完整毕设实现&#xff0c;面向计算机、人工智能及相关专业高年级本科生&#xff0c;专为毕业设计、课程设计及深度学习项目实战打造。代码复现了ST-GCN在NTU-RGBD与Kinetics骨骼数…

作者头像 李华
网站建设 2026/9/24 18:47:57

《熊出没》从科幻到奇幻:用顶级技术激活传统文化轮回

最近《熊出没年年有熊》这个名字一出来&#xff0c;圈内圈外都在聊。不光是家长群在问“今年熊大熊二又搞什么新花样”&#xff0c;就连做动画、做视觉特效的同行也在盯着——毕竟敢把“科幻”和“奇幻”两个词同时放进口号里&#xff0c;还喊出“激活传统文化轮回之作”这种定…

作者头像 李华