- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
本指南以 Formily 体系中
@formily/next的 Radio 组件文档为核心,系统讲解如何在表单中实现单选功能。你将掌握 Markup Schema、JSON Schema、纯 JSX 三种声明方式下 Radio 的完整用法,理解enum/dataSource选项数据的映射机制,并深入connect/mapProps/mapReadPretty等源码层实现,搞清楚单选值如何与表单模型双向同步、如何自动支持只读预览,从而在实际项目中游刃有余地选用最合适的写法。
一、组件定位:Formily 中的单选组件
Radio(单选框)是表单中最基础的选择类组件之一,用于在若干互斥选项中仅选取一项。在 Formily 体系中,各 UI 库适配层均提供了统一的Radio组件接口,但它们底层渲染的具体控件不同:
@formily/next:基于阿里巴巴 Fusion Next;@formily/antd:基于 Ant Design 的Radio,源码位于 packages/antd/src/radio/index.tsx;@formily/element:基于 Element UI 的Radio/RadioButton,源码位于 packages/element/src/radio/index.ts。
本文以 packages/next/docs/components/Radio.md 文档为主展开,其余实现作为对照参考。所有适配层都遵循同一个设计模式:把第三方 UI 组件的 Props 通过connect桥接到 Formily 的字段模型上,因此无论你使用哪套组件库,写法与行为都高度一致。
文档中组件被定义为 "Single selection box"(单选选择框),并声明了两种形态:
| 形态 | 说明 |
|---|---|
Radio | 单个单选项(对应底层NextRadio/AntdRadio/ElRadio) |
Radio.Group | 单选组(对应底层NextRadio.Group/AntdRadio.Group/ElRadioGroup),配合选项数据一次性渲染一组单选 |
在实际业务中,Radio.Group是更常用的形态,因为它直接对接选项数据源,与表单字段的值绑定。
二、Markup Schema 写法:声明式组件树
Markup Schema 是 Formily 在 JSX 内部书写 JSON Schema 的 DSL 形式。它把 Schema 节点写成组件标签(如<SchemaField.Number />),由编译器在运行时转换回标准 Schema 结构。
文档中的示例完整代码如下(源码见 packages/next/docs/components/Radio.md):
import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() export default () => ( <FormProvider form={form}> <SchemaField> <SchemaField.Number name="radio" title="single choice" enum={[ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ]} x-decorator="FormItem" x-component="Radio.Group" /> </SchemaField> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )关键点拆解:
- 注册组件:
createSchemaField({ components: { Radio, FormItem } })将Radio与FormItem注册进 SchemaField 的组件映射表,这样x-component="Radio.Group"和x-decorator="FormItem"才能在运行时被正确解析。 - 字段类型:
SchemaField.Number表明该字段的数据类型为number,这也是单选值常用的类型(选项 value 为 1、2)。 - 选项数据:
enum属性传入{ label, value }结构数组。在 Schema 编译阶段,enum会被自动映射为字段的dataSource(见后文「enum 与 dataSource 的映射原理」),最终透传给Radio.Group渲染。 - 装饰器与组件:
x-decorator="FormItem"表示用 FormItem 包裹(负责布局、标签、校验错误展示),x-component="Radio.Group"指定渲染单选组。 - 提交:
Submit配合FormButtonGroup组成提交按钮,onSubmit={console.log}会在提交时打印表单值,此时form.values中的radio字段即为选中项的 value(如1或2)。
三、JSON Schema 写法:纯数据驱动
JSON Schema 写法与 Markup Schema 在语义上完全等价,只是把 Schema 从 JSX 中抽离为独立的 JSON 对象,适合动态化、服务端下发的场景。文档示例(见 packages/next/docs/components/Radio.md):
import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, createSchemaField } from '@formily/react' const SchemaField = createSchemaField({ components: { Radio, FormItem, }, }) const form = createForm() const schema = { type: 'object', properties: { radio: { type: 'number', title: 'Single selection', enum: [ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ], 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', }, }, } export default () => ( <FormProvider form={form}> <SchemaField schema={schema} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )与 Markup Schema 的差异仅在声明形式上:
- Markup 写法用
<SchemaField.Number name="radio" ... />; - JSON 写法用
properties.radio = { type: 'number', ... }。
两者在运行时生成完全一致的字段模型。createSchemaField负责将 JSON Schema 编译为可被@formily/react消费的内部结构,enum同样会被转换为dataSource注入字段。
四、纯 JSX 写法:完全编程式控制
如果不需要 Schema 的声明式能力(例如选项数据来自运行时计算、或需要精细控制组件 Props),可以直接使用Field组件以 JSX 方式编写。文档示例(见 packages/next/docs/components/Radio.md):
import React from 'react' import { Radio, FormItem, FormButtonGroup, Submit } from '@formily/next' import { createForm } from '@formily/core' import { FormProvider, Field } from '@formily/react' const form = createForm() export default () => ( <FormProvider form={form}> <Field name="radio" title="single choice" dataSource={[ { label: 'Option 1', value: 1, }, { label: 'Option 2', value: 2, }, ]} decorator={FormItem} component={Radio.Group} /> <FormButtonGroup> <Submit onSubmit={console.log}>Submit</Submit> </FormButtonGroup> </FormProvider> )注意纯 JSX 写法与 Schema 写法的对应关系:
| Schema 属性 | JSX 属性 | 说明 |
|---|---|---|
enum | dataSource | 选项数据源,JSX 中直接使用字段模型的概念 |
x-decorator="FormItem" | decorator={FormItem} | 装饰器组件,直接传组件引用而非字符串 |
x-component="Radio.Group" | component={Radio.Group} | 渲染组件,直接传组件引用 |
Field组件把name、title、dataSource等属性写入字段模型,并将Radio.Group作为实际渲染组件,同时由FormItem装饰。三者中纯 JSX 写法最贴近传统 React 组件用法,也是理解connect原理最直观的入口。
五、enum 与 dataSource 的映射原理
为什么 Schema 里写的是enum,而 JSX 里写的是dataSource?这是 Formily 刻意设计的标准 JSON Schema 关键字到内部状态模型的关键字映射。
在 packages/json-schema/src/shared.ts 中定义了SchemaStateMap:
export const SchemaStateMap = { title: 'title', description: 'description', default: 'initialValue', enum: 'dataSource', // ← enum 映射为 dataSource readOnly: 'readOnly', writeOnly: 'editable', 'x-content': 'content', 'x-data': 'data', 'x-value': 'value', 'x-editable': 'editable', 'x-disabled': 'disabled', 'x-read-pretty': 'readPretty', 'x-read-only': 'readOnly', 'x-visible': 'visible', 'x-hidden': 'hidden', 'x-display': 'display', 'x-pattern': 'pattern', 'x-validator': 'validator', 'x-decorator': 'decoratorType', 'x-component': 'componentType', 'x-decorator-props': 'decoratorProps', 'x-component-props': 'componentProps', }Schema 编译时,enum键会被翻译为字段状态中的dataSource。同文件中的patchStateFormSchema(packages/json-schema/src/shared.ts)进一步处理了enum数组的标准化:
const isEnum = key === 'enum' && isArr(compiled) // ... isEnum ? createDataSource(compiled) : compiled而createDataSource(packages/json-schema/src/shared.ts)会把纯字符串/数字数组自动包装成{ label, value }结构:
export const createDataSource = (source: any[]) => { return toArr(source).map((item) => { if (typeof item === 'object') { return item } else { return { label: item, value: item, } } }) }这意味着你既可以写enum={[{ label: 'Option 1', value: 1 }]}这样的完整对象数组,也可以简写为enum={['Option 1', 'Option 2']},后者会被自动补齐为{ label: 'Option 1', value: 'Option 1' }。这与@formily/next组件层mapProps({ dataSource: true })的透传配合,最终将标准化的选项数组送入 Fusion Next 的Radio.Group。
六、组件源码解析:connect 如何打通双向绑定
Radio与Radio.Group的本质是@formily/react的connect高阶组件包装。以 packages/next/src/radio/index.tsx 为例:
export const Radio: ComposedRadio = connect( NextRadio, mapProps( { value: 'checked', }, mapSize ) ) Radio.Group = connect( NextRadio.Group, mapProps( { dataSource: true, }, mapSize ), mapReadPretty(PreviewText.Select) )1. 单个 Radio 的 value → checked 映射
mapProps({ value: 'checked' })将 Formily 字段模型的value映射为 Fusion Next 单选项的checkedprop。单个Radio通常用于固定布尔型或独立开关场景(如"是否同意协议"),选中时checked = true会通过onChange回写字段值。
2. Radio.Group 的 dataSource 透传与尺寸联动
mapProps({ dataSource: true })表示将字段的dataSource原样透传给Radio.Group。结合上文可知,无论选项来自 Schema 的enum还是 JSX 的dataSource,最终都会落在这个属性上。
第二个映射mapSize来自 packages/next/src/builtins/mapSize.ts:
export const mapSize = (props: any) => { const layout = { ...useFormShallowLayout(), ...useFormLayout() } const takeSize = () => { return layout.size === 'default' ? 'medium' : layout.size } return { ...props, size: props.size || takeSize(), } }mapSize从表单布局上下文(FormLayout)读取全局尺寸配置并注入组件,使 Radio 能跟随表单统一调整大小(small/medium/large),无需在每个组件上手动声明。这正是 Formily 全局布局能力的体现,详见 packages/next/src/form-layout/index.tsx。
3. mapReadPretty:自动只读预览
mapReadPretty(PreviewText.Select)是单选组在只读态(如详情页、表单回显)下的关键实现。当字段处于readPretty模式时,connect会自动用PreviewText.Select替换Radio.Group的渲染。PreviewText.Select的实现位于 packages/next/src/preview-text/index.tsx:
const Select: React.FC<React.PropsWithChildren<SelectProps>> = observer( (props) => { const field = useField<Field>() // ... const dataSource: any[] = field?.dataSource?.length ? field.dataSource : props?.dataSource?.length ? props.dataSource : [] const placeholder = usePlaceholder() const getLabel = (target: any) => { return ( dataSource?.find((item) => item.value == target?.value)?.label || target.label || placeholder ) } // ... return <div className={cls(prefixCls, props.className)}>{getLabels()}</div> } )它通过useField读取字段模型上的dataSource,把当前选中的value反查为对应的label并以纯文本渲染(无值时显示默认占位符N/A)。这样在只读场景下,单选值展示为可读文案而非被禁用的控件。占位符可通过PreviewText.Placeholder全局定制,例如设置为'暂无'。
4. Antd 版本对照
packages/antd/src/radio/index.tsx 与 Next 版本高度一致,差异仅在适配层细节:
export const Radio: ComposedRadio = connect( AntdRadio, mapProps({ value: 'checked', }) ) Radio.__ANT_RADIO = true Radio.Group = connect( AntdRadio.Group, mapProps({ dataSource: 'options', // Antd 的选项属性名是 options }), mapReadPretty(PreviewText.Select) )- Antd 版本额外设置了
Radio.__ANT_RADIO = true标记,供内部逻辑识别组件来源; dataSource: 'options'将字段的dataSource映射为 Antd Radio.Group 的options属性,而 Next 版本是dataSource: true原样透传——这是两套组件库 API 差异在适配层被抹平的直接证据;- 只读预览同样使用
PreviewText.Select,对应实现在 packages/antd/src/preview-text/index.tsx,额外支持fieldNames自定义label/value字段名。
Element 版本(Vue 场景)见 packages/element/src/radio/index.ts,它额外实现了options渲染逻辑与optionType: 'default' | 'button'的按钮形态切换(RadioButton),且选项值为label,同样以mapProps({ dataSource: 'options' })与mapReadPretty(PreviewText.Select)完成桥接。
七、API 说明与扩展属性
文档末尾给出的 API 说明为:
Reference https://fusion.design/pc/component/basic/radio
即@formily/next的Radio直接继承 Fusion Next Radio 的完整 API,包括Radio.Group的dataSource、value、onChange、disabled、size等全部属性。Formily 适配层不改变底层组件的 API 语义,只是把字段的值/数据源/状态与组件对接,因此底层组件的任何能力(如Radio.Group的方向布局、按钮样式等)均可直接使用。
在此基础上,Formily 还叠加了下列由适配层提供的增强能力:
| 能力 | 实现机制 | 说明 |
|---|---|---|
| 字段值双向同步 | connect+mapProps({ value: 'checked' }) | 选中状态与字段模型value实时同步 |
| 选项数据自动注入 | mapProps({ dataSource: true / 'options' }) | 从字段dataSource映射到底层组件的选项属性 |
| 尺寸联动 | mapSize(仅 Next) | 跟随FormLayout全局尺寸自动调整 |
| 只读预览 | mapReadPretty(PreviewText.Select) | readPretty模式下渲染为纯文本 label |
| 校验与错误展示 | x-decorator="FormItem"的字段联动 | 单选必选校验通过 FormItem 呈现 |
必选校验示例
结合FormItem装饰器,只需为字段声明required即可启用单选必选校验:
<SchemaField.Number name="radio" title="single choice" required enum={[ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, ]} x-decorator="FormItem" x-component="Radio.Group" />未选择任何选项时提交,FormItem 会展示"该字段必填"的校验提示,Submit也不会触发onSubmit回调。
八、三种写法的选型建议
| 写法 | 适用场景 | 优势 | 注意点 |
|---|---|---|---|
| Markup Schema | 常规表单开发,希望 Schema 与 JSX 共处 | 类型友好、IDE 提示好、可读性高 | 需createSchemaField注册组件 |
| JSON Schema | 动态表单、服务端下发 Schema、低代码平台 | 纯数据可序列化、易于持久化与远程传输 | Schema 需自行维护与校验 |
| 纯 JSX | 逻辑复杂、选项运行时计算、精细控制组件 | 无 Schema 心智负担、最接近原生 React 用法 | 数据源需显式传dataSource |
三者最终都会汇入@formily/core的字段模型,通过FormProvider共享同一个form实例,因此校验、联动、提交等能力完全一致,可以在同一表单内混用。
九、小结
本文围绕 packages/next/docs/components/Radio.md 的三个完整示例,完整呈现了@formily/next中 Radio 单选框的三种声明方式:
- Markup Schema:
<SchemaField.Number x-component="Radio.Group" enum={...} />; - JSON Schema:
properties.radio = { type: 'number', enum: [...], 'x-component': 'Radio.Group' }; - 纯 JSX:
<Field component={Radio.Group} dataSource={...} />。
并从源码层验证了三个关键机制:enum → dataSource的标准 JSON Schema 关键字映射(packages/json-schema/src/shared.ts)、connect/mapProps/mapSize的双向绑定与尺寸联动(packages/next/src/radio/index.tsx、packages/next/src/builtins/mapSize.ts)、以及mapReadPretty(PreviewText.Select)的只读文本预览(packages/next/src/preview-text/index.tsx)。这些机制在@formily/antd、@formily/element中同样成立,只是底层映射的组件属性名略有差异。
掌握了 Radio 的用法,也就掌握了 Formily 中所有"选项型"组件(Select、Checkbox、Cascader 等)的通用接入范式——它们共享同一套dataSource数据契约与connect桥接模型,可以举一反三。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
Formily Next 的 Radio 单选框组件:三种 Schema 写法与源码级封装解析
Formily Next 的 Radio 单选框组件:三种 Schema 写法与源码级封装解析 本文基于 Formily 2.x 的 @formily/next
前端UI组件Formily Next 复选框组件 Checkbox 完全使用指南:Markup Schema / JSON Schema / Pure JSX 三种写法与源码原理
Formily Next 复选框组件 Checkbox 完全使用指南:Markup Schema / JSON Schema / Pure JSX 三种写法与源
前端UI组件Formily 复选框组件 Checkbox 完全指南:Markup Schema / JSON Schema / JSX 三种用法与源码级桥接原理
Formily 复选框组件 Checkbox 完全指南:Markup Schema / JSON Schema / JSX 三种用法与源码级桥接原理 本文围绕
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考