节点表单
术语
节点表单 | 特指流程节点内的表单或点击节点展开的表单,关联节点数据。 |
节点引擎 | FlowGram.ai 内置的引擎之一,它核心维护了节点数据的增删查改,并提供渲染、校验、副作用、画布或变量联动等能力, 除此之外,它还提供节点错误捕获渲染、无内容时的 placeholder 渲染等能力,见以下章节例子。 |
快速开始
开启节点引擎
> API Detail
use-editor-props.ts
// EditorProps
{
nodeEngine: {
/**
* 需要开启节点引擎才能使用
*/
enable: true;
materials: {
/**
* 节点内部报错的渲染组件
*/
nodeErrorRender?: NodeErrorRender;
/**
* 节点无内容时的渲染组件
*/
nodePlaceholderRender?: NodePlaceholderRender;
}
}
}
配置表单
formMeta
是节点表单唯一配置入口,配置在每个节点的NodeRegistry 上。
> node-registries.ts
node-registries.ts
import { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
export const nodeRegistries: FlowNodeRegistry[] = [
{
type: 'start',
/**
* 配置节点表单的校验及渲染
*/
formMeta: {
/**
* 配置校验在数据变更时触发
*/
validateTrigger: ValidateTrigger.onChange,
/**
* 配置校验规则, 'content' 为字段路径,以下配置值对该路径下的数据进行校验。
*
* 也可支持动态函数写法, 用于根据 values 生成校验器:
* validate: (values, ctx) => ({ content: () => {}, })
*/
validate: {
content: ({ value }) => (value ? undefined : 'Content is required'),
},
/**
* 配置表单渲染
*/
render: () => (
<>
<Field<string> name="title">
{({ field }) => <div className="demo-free-node-title">{field.value}</div>}
</Field>
<Field<string> name="content">
{({ field, fieldState }) => (
<>
<input onChange={field.onChange} value={field.value}/>
{fieldState?.invalid && <Feedback errors={fieldState?.errors}/>}
</>
)}
</Field>
</>
)
},
}
]
> 表单写法的基础例子
渲染表单
> base-node.tsx
base-node.tsx
export const BaseNode = () => {
/**
* 提供节点渲染相关的方法
*/
const { form } = useNodeRender()
return (
<div className="demo-free-node" className={form?.state.invalid && "error"}>
{
// 表单渲染通过 formMeta 生成
form?.render()
}
</div>
)
};
核心概念
FormMeta
在 NodeRegistry
中,我们通过formMeta
来配置节点表单, 它遵循以下API。
> FormMeta API
这里特别说明, 节点表单与 通用表单有一个很大的区别,它的数据逻辑(如校验、数据变更后的副作用等)需要在表单不渲染的情况下依然生效,我们称 数据与渲染分离
。所以这些数据逻辑需要配置在formMeta 中的非render 字段中,保证不渲染情况下节点引擎也可以调用到这些逻辑, 而通用表单引擎(如react-hook-form)则没有这个限制, 校验可以直接写在react组件中。
FormMeta.render (渲染)
render
字段用于配置表单的渲染逻辑
render: (props: FormRenderProps<any>) => React.ReactElement;
> FormRenderProps
返回的 react 组件可使用以下表单组件和模型:
Field (组件)
Field
是表单字段的 React 高阶组件,封装了表单字段的通用逻辑,如数据与状态的注入,组件的刷新等。其核心必填参数为 name
, 用于声明表单项的路径,在一个表单中具有唯一性。
> Field Props API
Field 的渲染部分,支持三种写法,如下:
const render = () => (
<div>
<Label> 1. 通过 children </Label>
{/* 该方式适用于简单场景,Field 会将 value onChange 等属性直接注入第一层children组件中 */}
<Field name="c">
<Input />
</Field>
<Label> 2. 通过 Render Props </Label>
{/* 该方式适用于复杂场景,当 return 的组件存在多层嵌套,用户可以主动将field 中的属性注入希望注入的组件中 */}
<Field name="a">
{({ field, fieldState, formState }: FieldRenderProps<string>) => <div><Input {...field} /><Feedbacks errors={fieldState.errors}/></div>}
</Field>
<Label> 3. 通过传 render 函数</Label>
{/* 该方式类似方式2,但通过props 传入 */}
<Field name="b" render={({ field }: FieldRenderProps<string>) => <Input {...field} />} />
</div>
);
interface FieldRenderProps<TValue> {
// Field 实例
field: Field<TValue>;
// Field 状态(响应式)
fieldState: Readonly<FieldState>;
// Form 状态
formState: Readonly<FormState>;
}
> FieldRenderProps API
Field (模型)
Field
实例通常通过render props 传入(如上例子),或通过 useCurrentField
hook 获取。它包含表单字段在渲染层面的常见API。
注意: Field
是一个渲染模型,仅提供一般组件需要的API, 如 value
onChange
onFocus
onBlur
,如果是数据相关的API 请使用 Form
模型实例,如 form.setValueIn(name, value)
设置某字段的值。
> Field 模型 API
FieldArray (组件)
FieldArray
是数组类型字段的 React 高阶组件,封装了数组类型字段的通用逻辑,如数据与状态的注入,组件的刷新,以及数组项的遍历等。其核心必填参数为 name
, 用于声明该表单项的路径,在一个表单中具有唯一性。
FieldArray
的基础用法可以参照以下例子:
> 数组例子
FieldArray (模型)
FieldArray
继承于 Field
,是数组类型字段在渲染层的模型,除了包含渲染层的常见API,还包含 数组的基本操作如 FieldArray.map
, FieldArray.remove
, FieldArray.append
等。API 的使用方法也可见上述数组例子。
> FieldArray 模型 API
Form(组件)
Form
组件是表单的最外层高阶组件,上述 Field
FieldArray
等能力仅在该高阶组件下可以使用。节点表单的渲染已经将<Form />
封装到了引擎内部,所以用户无需关注,可以直接在render
返回的 react 组件中直接使用 Field
。但如果用户需要独立使用表单引擎,或者在节点之外独立再渲染一次表单,需要自行在表单内容外包上Form
组件。
Form(模型)
Form
实例可通过render
函数的入参获得, 也可通过 hook useForm
获取,见例子。它是表单核心模型门面,用户可以通过Form 实例操作表单数据、监听变更、触发校验等。
> Form 模型 API
校验
基于FormMeta章节中提到的"数据与渲染分离"概念,校验逻辑需配置在 FormMeta
全局, 并通过路径匹配方式声明校验逻辑所作用的表单项,如下例子。
路径支持模糊匹配,见路径章节。

export const renderValidateExample = ({ form }: FormRenderProps<FormData>) => (
<>
<Label> a (最大长度为 5)</Label>
<Field name="a">
{({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
<>
<Input value={value} onChange={onChange} />
<Feedback errors={fieldState?.errors} />
</>
)}
</Field>
<Label> b (如果a存在,b可以选填) </Label>
<Field
name="b"
render={({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
<>
<Input value={value} onChange={onChange} />
<Feedback errors={fieldState?.errors} />
</>
)}
/>
</>
);
export const VALIDATE_EXAMPLE: FormMeta = {
render: renderValidateExample,
// 校验时机配置
validateTrigger: ValidateTrigger.onChange,
/*
* 也可支持动态函数写法, 用于根据 values 生成校验器:
* validate: (values, ctx) => ({ a: () => '', b: () => '', c, () => '' })
*/
validate: {
// 单纯校验值
a: ({ value }) => (value.length > 5 ? '最大长度为5' : undefined),
// 校验依赖其他表单项的值
b: ({ value, formValues }) => {
if (formValues.a) {
return undefined;
} else {
return value ? 'undefined' : 'a 存在时 b 必填';
}
},
// 校验依赖节点或画布信息
c: ({ value, formValues, context }) => {
const { node, playgroundContext } = context;
// 此处逻辑省略
},
},
};
校验时机
ValidateTrigger.onChange | 表单数据变更时校验(不包含初始化数据) |
ValidateTrigger.onBlur | 表单项输入控件onBlur时校验。 注意,这里有两个前提:一是表单项的输入控件需要支持 onBlur 入参,二是要将 Field.onBlur 传入该控件:
<Field>{({field})=><Input ... onBlur={field.onBlur}>}</Field> |
validateTrigger
建议配置 ValidateTrigger.onChange
即数据变更时校验,如果配置 ValidateTrigger.onBlur
, 校验只会在组件blur事件触发时触发。那么当节点表单不渲染的情况下,就算是数据变更了,也不会触发校验。
主动触发校验
- 主动触发整个表单的校验
const form = useForm()
form.validate()
- 主动触发单个表单项校验
const validate = useFieldValidate(name)
validate()
name
不传则默认获取当前 <Field />
标签下的 Field
的 validate
, 通过传 name
可获取 <Form />
下任意 Field
。
路径
- 表单路径以
.
为层级分隔符, 如 a.b.c
指向数据 {a:{b:{c:1}}}
下的 1
- 路径支持模糊匹配,在校验和副作用配置中会使用到。如下例子。通常在数组场景中使用较多。
arr.* | arr 字段的所有一级子项 |
arr.x.* | arr.x 的所有一级子项 |
arr.*.x | arr 所有一级子项下的 x |
副作用 (effect)
副作用是节点表单特有的概念,指在节点数据发生变更时需要执行的副作用。同样,遵循 "数据与渲染分离" 的原则,副作用和校验相似,也配置在 FormMeta
全局。
- 通过 key value 形式配置,key 表示表单项路径匹配规则,支持模糊匹配,value 为作用在该路径上的effect。
- value 为数组,即支持一个表单项有多个effect。
export const EFFECT_EXAMPLE: FormMeta = {
...
effect: {
['a.b']: [
{
event: DataEvent.onValueChange,
effect: ({ value }: EffectFuncProps<string, FormData>) => {
console.log('a.b value changed:', value);
},
},
],
['arr.*']:[
{
event: DataEvent.onValueInit,
effect: ({ value, name }: EffectFuncProps<string, FormData>) => {
console.log(name + ' value init:', value);
},
},
]
}
};
interface EffectFuncProps<TFieldValue = any, TFormValues = any> {
name: FieldName;
value: TFieldValue;
prevValue?: TFieldValue;
formValues: TFormValues;
form: IForm;
context: NodeContext;
}
Effect 相关 API
副作用时机
DataEvent.onValueChange | 数据变更时触发 |
DataEvent.onValueInit | 数据初始化时触发 |
DataEvent.onValueInitOrChange | 数据初始化和变更时都会触发 |
联动
> 联动例子
hooks
节点表单内
以下hook 可在节点表单内部使用
useCurrentField
() => Field
该 hook 需要在Field 标签内部使用
const field = useCurrentField()
> Field 模型 API
useCurrentFieldState
() => FieldState
该 hook 需要在Field 标签内部使用
const fieldState = useCurrentFieldState()
> FieldState API
useFieldValidate
(name?: FieldName) => () => Promise<void>
如果需要主动触发字段的校验,可以使用该hook 获取到 Field 的 validate 函数。
name
为 Field 的路径,不传则默认获取当前 <Field />
下的validate
const validate = useFieldValidate()
validate()
useForm
() => Form
用于获取 Form 实例。
注意,该hook 在 render
函数第一层不生效,仅在 render
函数内的 react 组件内部才可使用。render
函数的入参中已经传入了 form: Form
, 可以直接使用。
- 在 render 函数第一层直接使用
props.form
const formMeta = {
render: ({form}) =>
<div>
{form.getValueIn('my.path')}
</div>
}
- 在组件内部可使用
useForm
const formMeta = {
render: () =>
<div>
<Field name={'my.path'}>
<MySelect />
</Field>
</div>
}
// MySelect.tsx
...
const form = useForm()
const valueNeeded = form.getValueIn('my.other.path')
...
注意:Form 的 api 不具备任何响应式能力,若需监听某字段值,可使用 useWatch
useWatch
<TValue = FieldValue>(name: FieldName) => TValue
该 hook 和上述 useForm
相似, 在 render
函数返回组件的第一层不生效,仅在封装过的组件内部可用。如果需要在 render
根级别使用,可以对 render
返回的内容做一层组件封装。
{
render: () =>
<div>
<Field name={'a'}><A /></Field>
<Field name={'b'}><B /></Field>
</div>
}
// A.tsx
...
const b = useWatch('b')
// do something with b
...
节点表单外
以下 hook 用于在节点表单外部,如画布全局、相邻节点上需要去监听某个节点表单的数据或状态。通常需要传入 node: FlowNodeEntity
作为参数
useWatchFormValues
监听 node 内整个表单的值
<TFormValues = any>(node: FlowNodeEntity) => TFormValues | undefined
const values = useWatchFormValues(node)
useWatchFormValueIn
监听 node 内某个表单项的值
<TValue = any>(node: FlowNodeEntity,name: string) => TFormValues | undefined
const value = useWatchFormValueIn(node, name)
useWatchFormState
监听 node 内表单的状态
(node: FlowNodeEntity) => FormState | undefined
const formState = useWatchFormState(node)
useWatchFormErrors
监听 node 内表单的 Errors
(node: FlowNodeEntity) => Errors | undefined
const errors = useWatchFormErrors(node)
useWatchFormWarnings
监听 node 内表单的 Warnings
(node: FlowNodeEntity) => Warnings | undefined
const warnings = useWatchFormErrors(node)