- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
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> ) }这段实现清晰展示了桥接链路:
useForm()获取当前FormProvider提供的表单实例,useField()获取父级字段(用于推导basePath);- 通过
form.createObjectField({ basePath: parent?.address, ...props })创建(或复用)对象字段模型,并将其挂载到表单字段图上; - 将字段模型放入
FieldContext.Provider,供内部嵌套的Field通过useField读取; - 用 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 补充了三个渲染相关属性:
| 属性 | 类型 | 说明 |
|---|---|---|
name | FormPathPattern | 必填,字段在表单值中的路径(如"object"、"a.b") |
basePath | FormPathPattern | 可选,字段的基准路径,默认自动取父级字段地址 |
children | RenderPropsChildren<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 || {})遍历对象键,为每个键内嵌一个普通Field(name={key}相对于当前对象字段路径),并配一个 Remove 按钮; - 动态添加属性:表单里放一个临时输入框
propertyName(注意basePath={''}使其脱离对象字段路径、挂在表单顶层),点击 Add 时读取form.values.propertyName,用field.addProperty(name, '')写入新属性,再用form.deleteValuesIn('propertyName')清空临时输入; - 去重校验:
form.existValuesIn(${field.path}.${name})用于判断目标路径是否已存在值,避免重复添加同名属性; observer:ObjectComponent必须用observer包裹,否则新增/删除属性导致field.value变化时,组件不会重新渲染。
Render Props 用例:不写自定义组件也能动态控制
如果不想额外定义组件,可以直接把 children 写成函数,函数参数即字段模型field。此时无需observer、useField,因为 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) : childrenReactiveField会检测 children 是否为函数:若是函数则以(field, form)为参数调用(render props 分支);否则直接作为普通节点渲染(自定义组件分支)。两种分支共享后续的decorator、component、display(非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修改值也能触发同样的清理逻辑,保证字段图与值保持一致。
使用注意事项与最佳实践
name必须传:ObjectField 的name是必填项(类型层面强制name: FormPathPattern),缺失时字段地址无法解析、字段无法挂载到表单;- 优先 render props 组织子组件:把字段模型作为 children 函数的入参,能在声明式 JSX 中直接调用
addProperty等方法,避免额外编写observer组件; - 区分对象字段与普通字段:若值结构固定(如一个已知 schema 的对象),用
Field+ JSON Schema 即可;只有当键名需要运行时动态增删时才需要 ObjectField,其价值在于addProperty/removeProperty对子字段图的自动维护; - 临时输入框用
basePath={''}隔离:动态添加属性时需要一个"属性名输入框",将其basePath设为空串可避免它被错误地嵌套进对象字段的值路径; - 配合响应式渲染:若采用自定义组件方案,务必用
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
相关推荐
Formily Vue ObjectField 组件全解析:动态对象结构表单的 ViewModel 桥接实践
Formily Vue ObjectField 组件全解析:动态对象结构表单的 ViewModel 桥接实践 ObjectField 是 Formily Vue
前端UI组件Formily Core ObjectField 模型完全指南:对象字段的动态属性增删与状态管理
Formily Core ObjectField 模型完全指南:对象字段的动态属性增删与状态管理 ObjectField 是 Formily 核心模型体系中专门
前端UI组件Formily React ArrayField 组件完全指南:数组型字段的 ViewModel 绑定与增删改排实战
Formily React ArrayField 组件完全指南:数组型字段的 ViewModel 绑定与增删改排实战 ArrayField 是 Formily
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考