十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenMetadata 表单组件规范与 HookForm 实战:从设计令牌到 react-hook-form 表单栈

OpenMetadata 表单组件规范与 HookForm 实战:从设计令牌到 react-hook-form 表单栈 OpenMetadata 表单组件规范与 HookForm 实战从设计令牌到 react-hook-form 表单栈【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata本篇以 OpenMetadata 设计系统规范文档openmetadata-ui/src/main/resources/ui/specs/components/form.md为主体完整覆盖 Form 组件的使用场景、解剖结构、设计令牌、状态样式与代码示例并结合仓库中ui-core-components的HookForm/FormField/Field实现源码hook-form.tsx、form-field.tsx、form-field.types.ts与配套指南 docs/formutils.md 深度展开讲解如何用声明式FieldProp配置 react-hook-form 状态管理搭建可校验、可提交、可维护的复杂表单。一、Form 组件元信息它是什么、何时该用Form 是 OpenMetadata 设计系统中用于数据录入Data entry类别的基础组件状态为Stable稳定。它的样式定义在src/styles/components/form.less组件实现则来自openmetadata/ui-core-components包包含两套并行 API维度新栈推荐本规范主题旧栈遗留组件HookForm/Form新建工作Ant DesignForm导入来源openmetadata/ui-core-componentsutils/formUtilsopenmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx技术底座react-hook-formreact-aria-components Tailwindtw:Ant DesignForm/Form.Item Less批量渲染FormFieldsgenerateFormFields按照设计系统规范 specs/README.md 的要求新工作应使用 UntitledUI Tailwind 栈且yarn tw-guard会拦截新增的 antd 导入与新增.less文件因此新表单一律基于HookForm栈构建。使用场景判断Use when该用表单时收集用户提交的结构化输入——例如服务连接配置service connections、实体元数据entity metadata、设置项settings。新表单使用 react-hook-form react-aria 的HookForm栈。Dont use when不该用表单时单个内联控件如搜索框、筛选器足以满足需求时不要用表单——完整的表单意味着一个经过校验、可提交的 payload。二、表单解剖Anatomy规范文档给出的表单结构示意图┌─────────────────────────────────────────┐ │ Name * │ ← label required mark (right side) │ ┌─────────────────────────────────────┐ │ │ │ input │ │ ← field control │ └─────────────────────────────────────┘ │ │ Helper / error text │ │ [Cancel] [Save] │ ← footer actions └─────────────────────────────────────────┘表单由四个部分组成表单容器form container承载表单的表面包含 padding、圆角radius、阴影shadow字段fieldlabel required mark必填标记 控件辅助 / 错误文本helper / error text字段下方的说明文字或校验错误提示底部操作footer actionsCancel / Save 等提交操作按钮。该结构在源码中与 form.less 的.form-container一一对应background-color: var(--tw-color-bg-primary)、padding: var(--om-space-28) var(--om-space-20)、border-width: 1px、border-radius: var(--om-radius-lg)、box-shadow: 2px 4px 10px var(--om-legacy-color-0-0-0-0-04)、width: 700px。三、设计令牌Tokens颜色与尺寸的语义化来源规范文档列出的令牌对照表如下PartToken容器垂直 / 水平 padding--om-space-28、--om-space-20容器圆角--om-radius-lg容器阴影--om-legacy-color-0-0-0-0-04柔和投影必填标记左侧外边距--om-space-4必填标记字号--om-font-size-sm块编辑器底部 padding--om-space-75字段边框语义化--om-color-border聚焦 / 悬停边框语义化--om-color-border-brand必填标记 / 错误文本语义化--om-color-text-error注意旧版变量grey-300、primary-5、red-14、highlight-color在.less中仍携带颜色值新建工作应引用语义化令牌。这背后的令牌体系分层在 specs/README.md 中有明确说明Layer 1 globals.css 上游原语--color-*、--radius-*、--text-*、--shadow-*、语义 --color-{text,bg,...} 来自 openmetadata/ui-core-components是唯一事实来源。 Layer 2 --om-* 项目别名定义于 tokens.css引用 Layer 1 对应令牌超规格/遗留值直接持有原始值。 Components.less/.css 通过 var(--om-*) 引用 Layer 2绝不写死 hex / px。核心规则组件样式只能使用tokens.css中的var(--om-*)令牌绝不能引入裸 hex、rgb/rgba 或 px 间距值优先选用语义化令牌如--om-color-text-primary、--om-space-16而非调色板/遗留令牌。提交前必须运行yarn token-audit要求零错误才能通过 CI。相关工具链还包括yarn token-migrate把裸值安全、幂等地迁移为var(--om-*)与yarn token-gen重新生成tokens.css的自动生成块。在 form.less 中可以看到令牌与遗留变量的实际混用.new-form-style里控件边框使用grey-300、聚焦边框使用primary-5而新样式路径.form-container、必填标记已改用var(--om-space-*)等语义令牌——这正是迁移过程中新旧并存的真实写照。四、Props / APIHookForm的核心接口规范文档给出HookForm的 Props 表Prop用途formUseFormReturn来自react-hook-formonSubmit提交处理器react-ariaFormshowFieldDocs是否渲染每个字段的文档fieldDocDisplaypopover默认或panelformClassName滚动表单列的样式类对照源码 hook-form.tsxFormProps还支持一组与字段文档展示相关的高级 PropsrenderFieldDoc?: (doc: string) ReactNode—— 自定义字段文档的渲染方式fieldDocHeader?: ReactNode—— 文档面板/气泡的头部fieldDocOffset、fieldDocMaxHeight—— 气泡的偏移与最大高度popover模式emptyFieldDoc?: ReactNode—— 面板模式无焦点字段时展示的占位内容。HookForm的实现结构非常清晰始终维持FormProvider FieldDocProvider AriaForm的树形结构这样切换showFieldDocs不会重挂载表单、也不会重置其状态。FieldDocProvider在禁用时是 no-op 且不产生 DOM。在panel模式下字段文档渲染为表单旁的独立列固定宽度 380px展开/收起有 240ms 过渡动画并通过formClassName传入滚动表单列的类名在popover模式下则渲染跟随焦点字段的气泡。字段本身通过getField/FieldProp/FormFields组合而成。五、状态States默认、聚焦、错误与禁用State处理方式默认边框--om-color-border圆角--om-radius-sm聚焦 / 悬停边框--om-color-border-brand错误边框--om-color-border-error消息--om-color-text-error禁用--om-color-bg-disabled--om-color-text-disabledcursor: not-allowed在 form.less 的.new-form-style中可以看到旧栈的实现方式border-color: grey-300; border-radius: border-rad-xs:hover/:focus时border-color: primary-5.ant-switch-checked使用primary-6label 使用grey-700、font-weight: font-medium、font-size: font-size-base。新栈则对应语义令牌。必填标记Required mark的右侧布局规范解剖图显示必填标记*位于 label右侧这是 OpenMetadata 的刻意设计。源码 form.less 中有详细注释As per our design we need to show it on right side of the label实现方式是// 隐藏默认的必填标记Ant Design 默认渲染在左侧 .ant-form-item-label label.ant-form-item-required:not( .ant-form-item-required-mark-optional )::before { content: ; } // 将必填标记移到 label 右侧 .ant-form-item-label label.ant-form-item-required:not( .ant-form-item-required-mark-optional )::after { display: inline-block; margin-left: var(--om-space-4); color: red-14; font-size: var(--om-font-size-sm); line-height: 1; content: *; }六、代码示例直接可用样式与最小表单6.1 Less 样式容器、输入边框与必填标记规范文档给出的完整 Less 示例.custom-form { .form-container { padding: var(--om-space-28) var(--om-space-20); border-radius: var(--om-radius-lg); box-shadow: 2px 4px 10px var(--om-legacy-color-0-0-0-0-04); } .ant-input { border-color: var(--om-color-border); :hover, :focus { border-color: var(--om-color-border-brand); } } .ant-form-item-required::after { margin-left: var(--om-space-4); font-size: var(--om-font-size-sm); color: var(--om-color-text-error); content: *; } }注意这是新语义令牌写法——容器使用--om-space-28/--om-space-20/--om-radius-lg输入框聚焦使用--om-color-border-brand必填标记使用--om-color-text-error符合设计系统新工作引用语义令牌的要求。6.2 TSX最小HookForm表单import { HookForm } from openmetadata/ui-core-components; HookForm form{form} onSubmit{onSubmit} {/* getField(...) 字段 */} Button colorprimary typesubmit{t(label.save)}/Button /HookForm;这里form来自调用方useFormFormValues()onSubmit传给 react-aria 的FormgetField(...)负责渲染并接线每一个字段。这印证了 formutils.md 中调用方拥有状态与提交、HookForm提供包装器、getField渲染输入的分工原则。七、深度原理声明式FieldProp驱动的新表单栈规范文档指出字段通过getField/FieldProp/FormFields组合其配套指南 docs/formutils.md 用一个公式概括整个栈一个表单 FieldProp[]配置 RHF 状态 一个纯转换函数。四类对象的职责划分源码确认对象本质拥有FieldProp普通 TypeScript 对象单个输入项的描述name、type、label、校验规则useFormT()RHF hook表单状态values、errors、touched、dirty、submittingHookFormReact 组件让表单状态对其下方字段可见的包装器getField(fieldProp)返回ReactNode的函数把配置渲染为已校验、已接线 RHF 的输入7.1FieldProp的完整形态源码form-field.types.tsinterface FieldProp { name: string; // RHF 字段名必须与 FormValues 的某个 key 对应 label: ReactNode; // 输入框上方的可见标签 type: FieldTypes; // 渲染哪种输入原语 required?: boolean; // 是否渲染红色 * 号 rules?: RegisterOptions; // RHF 校验规则 id?: string; // DOM id惯例 root/name placeholder?: string; props?: FieldPropsMap; // 按类型分组的透传属性袋 helperText?: ReactNode; // 额外上下文说明 helperTextType?: HelperTextType; // ALERT默认| TOOLTIP showHelperText?: boolean; /** Markdown 文档展示在字段文档气泡中 */ doc?: string; hasSeparator?: boolean; // true 时字段后渲染 Divider / formItemLayout?: FormItemLayout; // VERTICAL默认| HORIZONTAL }要点FieldProp是数据而非组件——它描述我想要一个名为name、带这些规则的 TEXT 输入并不自己渲染渲染由getField完成。rules就是 RHF 的RegisterOptions支持required、minLength、maxLength、min、max、pattern、validate、deps等错误消息一律通过t(...)翻译。7.2getField/Field/FormFields的实现源码 form-field.tsx 中三者关系一目了然export const Field: FC{ field: FieldProp } ({ field }) { const { control } useFormContext(); // required 为 true 且未提供 rules.required 时自动注入 required 规则 const effectiveRules: RegisterOptions { ...rules }; if (required !effectiveRules.required) { effectiveRules.required true; } // ...通过 FormField control{control} name{name} rules{effectiveRules} // 渲染 FormItemLabel renderFieldElement(controller, field) 错误 HintText }; export const getField (fieldProp: FieldProp): ReactNode ( Field field{fieldProp} / ); export const FormFields: FC{ fields: FieldProp[] } ({ fields }) ( {fields.map((f, i) ( Field field{f} key{f.id ?? f.name ?? i} / ))} / );getField在内部渲染 label、控件、自动的错误HintText、可选的 helper AlertHelperTextType.ALERT时渲染黄色警告 AlertTOOLTIP时渲染 label 旁帮助图标与可选 Divider。硬性要求是getField必须渲染在HookForm或任何 RHFFormProvider内部——Field通过useFormContext()定位表单在外部渲染会抛错。7.3FieldTypes全谱系源码form-field.types.ts定义的枚举共 26 个值export enum FieldTypes { TEXT, PASSWORD, NUMBER, SELECT, AUTOCOMPLETE, MULTI_SELECT, SWITCH, CHECKBOX, TEXTAREA, DESCRIPTION, FILTER_PATTERN, SLIDER, ASYNC_SELECT, TREE_ASYNC_SELECT, TAG_SUGGESTION, UT_TAG_SUGGESTION, GLOSSARY_TAG_SUGGESTION, USER_TEAM_SELECT, USER_MULTI_SELECT, USER_TEAM_SELECT_INPUT, COLOR_PICKER, ICON_PICKER, COVER_IMAGE_UPLOAD, DOMAIN_SELECT, CRON_EDITOR, SELECT_NATIVE, COMPONENT, }选择指引formutils.md文本TEXT/PASSWORD/NUMBER多行用TEXTAREA通用、DESCRIPTION语义上表示描述、FILTER_PATTERN与CRON_EDITOR筛选/cron 表达式单选SELECT弹出式单选、SELECT_NATIVE原生select适合移动端、AUTOCOMPLETE可搜索单选长列表或服务端搜索多选MULTI_SELECT可搜索多选11 种 autocomplete 风味TAG_SUGGESTION分类标签、UT_TAG_SUGGESTION无类型标签、GLOSSARY_TAG_SUGGESTION词汇表术语、USER_TEAM_SELECT仅用户、USER_TEAM_SELECT_INPUT用户团队、USER_MULTI_SELECT多用户、DOMAIN_SELECT域、ASYNC_SELECT通用服务端拉取单选、TREE_ASYNC_SELECT层级异步配renderItem——底层渲染同一个Autocomplete枚举值差异是为了调用点的语义清晰与未来按类型定制选择器COLOR_PICKER色板网格存 hex 字符串、ICON_PICKER图标网格可选 URL 标签、COVER_IMAGE_UPLOAD拖拽区预览可选重定位逃生舱COMPONENT直接把props.children渲染进包装器保留 label 错误脚手架。7.4FieldPropsMap按类型分组的透传属性props是一个类型化属性袋常见键包括通用data-testid、disabledreact-aria 组件用isDisabled选择/自动完成类用options/items、multiple、filterOption返回() true可关闭本地过滤以配合服务端搜索、onFocus常见模式首次聚焦时懒加载选项、onSearchChange接防抖拉取、onSelectionChange/onItemInserted/onItemCleared、renderItem、size、fontSize颜色选择器用colors/emptyStateLabel图标选择器用options/defaultIcon/backgroundColor/allowUrl/labels封面图上传用acceptedFileTypes/maxSizeMB/maxDimensions/onValidationError触发 snackbar 而非行内错误/repositionable/previewHeight/renderPreview。其余键onChange、onBlur等透传给底层原语RHF 自身的 change/blur 处理器仍会触发。7.5 表单值类型契约Autocomplete 存完整项而非 IDFormSelectItem是SelectItemType的别名type SelectItemType { id: string; label?: string; avatarUrl?: string; isDisabled?: boolean; supportingText?: string; icon?: FC | ReactNode; };autocomplete 字段的FormValues应为FormSelectItem/FormSelectItem[]或其扩展而非string/string[]——这样转换函数form values → API payload保持纯函数无需选项列表即可重建 API 对象。携带类型化 payload 时扩展基类并添加value字段Domain 表单的DomainFormSelectItem正是如此value: TagLabel | EntityReference | DomainType | string转换时读取item.value即可。八、校验机制规则配置、提交门控与错误渲染规则放在FieldProp.rules上——required、minLength、maxLength、pattern、validate等消息必须翻译提交被门控——form.handleSubmit(onSubmit)在任意规则失败时不会调用onSubmit错误自动渲染——字段下方渲染红色HintText输入框标记为无效抽屉自动滚动到第一个无效字段。你永远不会手写onChange{(v) setError(...)}或自己渲染错误 JSX——配置携带规则包装器渲染错误。8.1 校验时机RHF 默认行为按 RHF 默认提交时总是失败则阻止提交与首次提交后的变更时。可通过useForm({ mode: onBlur })开启失焦校验但默认值对大多数表单已足够。8.2 异步校验rules.validate可返回 Promise用于服务端检查rules: { validate: async (value) { const exists await checkNameExists(value); return exists ? t(message.name-already-exists) : true; }, }返回true/undefined表示有效返回字符串表示无效RHF 会在允许提交前等待 Promise。8.3required: true与rules.required的关系源码Field会做归一化required: true同时做两件事——渲染红色*并且在未提供rules.required时注入required: true规则来门控提交。但注入的规则没有消息用户会看到 RHF 的默认提示因此最佳实践是两者都设置required: true, rules: { required: t(label.field-required, { field: t(label.name) }) },条件必填时传rules: undefined而非rules: {}。8.4 跨字段校验使用rules.validate的第二个参数完整表单值配合rules.deps触发重校验{ name: confirmPassword, rules: { validate: (value, formValues) value formValues.password || t(message.passwords-must-match), deps: [password], // password 变化时重新校验 }, }九、多字段组合三种布局模式9.1FormFields fields{[...]}批量渲染垂直堆叠、按数组顺序排列适合无需自定义布局的场景HookForm form{form} FormFields fields{[nameField, displayNameField, descriptionField, tagsField]} / /HookForm签名const FormFields: FC{ fields: FieldProp[] }key 取field.id其次field.name最后数组索引——建议设置id或name避免数组变化时出现重渲染 bug。9.2 堆叠getField()手动布局需要行、列或条件渲染时AddDomainForm的做法HookForm form{form} Box gap{4} div classNametw:flex-1{getField(nameField)}/div div classNametw:flex-1{getField(displayNameField)}/div /Box {type DATA_PRODUCT getField(domainField)} div{getField(tagsField)}/div /HookForm9.3 混合模式getField()与FormFields可自由混用条件字段有两种写法过滤数组配FormFields或行内配堆叠getField。需要分节时在FieldProp上设hasSeparator: true即可在字段后渲染Divider /。十、逃生舱FormField与FieldTypes.COMPONENT对任何FieldTypes都装不下的输入如RichTextEditor、GlossaryTermTreeSelect可降级到直接FormField配 render-prop 子节点FormField control{form.control} namedescription rules{{ required: t(...) }} {({ field, fieldState }) ( Box aria-invalid{fieldState.invalid || undefined} directioncol FormItemLabel label{t(label.description)} required / RichTextEditor value{field.value} onTextChange{field.onChange} / {fieldState.error?.message ( HintText isInvalid{fieldState.error.message}/HintText )} /Box )} /FormField复刻FormItemLabel、HintText、controller.field{ value, onChange, onBlur, name, ref }已设置validationBehavior: aria与controller.fieldState{ invalid, error, isTouched, isDirty }四件套后逃生舱字段在视觉与行为上与类型化getField字段无异。经验法则同一个 widget 手写两次逃生舱就为它新增一个FieldTypes值。另注意useFormFieldContext()在FormField之外调用会抛错源码 hook-form.tsx L65-L78。两种逃生舱的取舍FieldTypes.COMPONENT停留在类型化字段流程内Field包装器仍渲染 label 错误适合自管状态的行内自定义直接FormField则完全掌控脚手架适合自带 value/change 处理器、必须接线 RHF 的 widget富文本、树选择等。十一、端到端实战调用方 抽屉Drawer完整模式formutils.md 给出了抽屉承载表单的完整调用方模式AddDomainForm的消费方式const form useFormFormValues({ defaultValues: FORM_DEFAULTS }); const [isLoading, setIsLoading] useState(false); const handleSubmit useCallback( async (data: FormValues) { const payload transformXFormData(data); setIsLoading(true); try { await createEntity(payload); } finally { setIsLoading(false); } }, [...] ); const { formDrawer, openDrawer, closeDrawer } useFormDrawerWithHookFormValues({ title: t(label.add-entity, { entity: t(label.x) }), hookForm: form, form: ( YourForm form{form} onSubmit{(data) submitAndClose(data, handleSubmit, closeDrawer, refresh)} / ), onSubmit: (data) submitAndClose(data, handleSubmit, closeDrawer, refresh), loading: isLoading, });三个新件useFormDrawerWithHookT(config)把表单包进带 Save/Cancel 按钮的抽屉底层由你拥有的 RHFUseFormReturn支撑委托hookForm.handleSubmit(...)管理完整提交生命周期。校验失败时滚动到第一个[aria-invalidtrue]字段取消时执行hookForm.reset()再调用onCancel。提交按钮不会自动关闭抽屉——由消费方通过submitAndClose关闭。配置项包括title、hookForm、form、onSubmit、onCancel、loading、submitLabel默认t(label.save)、cancelLabel、submitTestId默认save-btn、cancelTestId、headerActions、footerAlign、closeOnEscape等。submitAndClose(data, handler, closeDrawer, onSuccess?)先await handler(data)成功后关闭抽屉并执行onSuccesshandler 抛错/拒绝时跳过closeDrawer与onSuccess抽屉保持打开以便重试。绝不在 handler 内自行调用closeDrawer()。transformXFormData(values, context)每个表单自行编写的纯函数把表单值转换为 API payload无 React、无异步、无副作用可独立测试。Domain 表单版本会提取item.valuetags、owners、打包 style 字段、剥离 UI-only 键。完整提交流程用户点击抽屉中的 Save ↓ useFormDrawerWithHook 调用 hookForm.handleSubmit(...) ↓ RHF 校验规则 → 失败则滚动到第一个 [aria-invalid] 并停止 ↓ 成功后调用 onSubmit(data: FormValues) ↓ 你调用 submitAndClose(data, handleSubmit, closeDrawer, refresh) ↓ submitAndClose await handleSubmit(data) ↓ handleSubmit 内部transformXFormData(data) → API 调用 ↓ 成功 → submitAndClose 关闭抽屉 调用 refresh 失败 → submitAndClose 重新抛出抽屉保持打开等待重试十二、验证实现HookForm源码中的关键设计决策结合 hook-form.tsx 源码有四个值得注意的实现细节树形结构恒定FormProvider FieldDocProvider AriaForm始终相同切换showFieldDocs不会重挂载表单、不会重置状态FieldDocProvider禁用时是 no-op。面板模式布局约束表单列采用tw:min-w-95 tw:flex-1 tw:overflow-y-automin-width是承重设计——没有它hint 的宽度会被先预留窄视口下表单被挤压而不是 hint 收缩。文档列固定 380px 宽收起时动画到 0tw:transition-[width,min-width] 240ms。FormField的接线useController(props)获取绑定并把validationBehavior: aria注入controller.field通过FormFieldContext提供id/name/control供useFormFieldContext()读取。提交语义HookForm把剩余 propsclassName、data-testid、onSubmit透传给底层 react-ariaForm——所以onSubmit{form.handleSubmit(...)}的写法是标准做法。十三、样式微调与相关规范如需进一步微调表单样式可参考同目录规范Table · Tabs · Button基础令牌见 Color、Spacing、Radius完整令牌清单见 token-reference.md。实际应用样式文件为src/styles/components/form.less其中还包含水平/垂直布局.form-item-horizontal下 label 占 40% / 控件占 60% 的 flex 布局、块编辑器.block-editor-wrapper与开关.ant-switch等配套样式可在自定义表单时直接复用。快速上手总结新表单一律走HookForm栈——useFormT()持有状态FieldProp[]描述字段getField/FormFields渲染接线transformXFormData纯函数转换 payload样式只引用var(--om-*)语义令牌提交前运行yarn token-audit保证零硬编码。这套配置即表单的模式让校验、错误、无障碍aria-invalid、label 关联全部由框架代劳调用方只需关注状态、转换与提交。【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表