节点表单

术语

节点表单特指流程节点内的表单或点击节点展开的表单,关联节点数据。
节点引擎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 { nodeplaygroundContext } = context;
      // 此处逻辑省略
    },
  },
};

校验时机

ValidateTrigger.onChange表单数据变更时校验(不包含初始化数据)
ValidateTrigger.onBlur表单项输入控件onBlur时校验。
注意,这里有两个前提:一是表单项的输入控件需要支持 onBlur 入参,二是要将 Field.onBlur 传入该控件:
<Field>{({field})=><Input ... onBlur={field.onBlur}>}</Field>

validateTrigger 建议配置 ValidateTrigger.onChange 即数据变更时校验,如果配置 ValidateTrigger.onBlur, 校验只会在组件blur事件触发时触发。那么当节点表单不渲染的情况下,就算是数据变更了,也不会触发校验。

主动触发校验

  1. 主动触发整个表单的校验
const form = useForm()
form.validate()
  1. 主动触发单个表单项校验
const validate = useFieldValidate(name)
validate()

name 不传则默认获取当前 <Field /> 标签下的 Fieldvalidate, 通过传 name 可获取 <Form /> 下任意 Field

路径

  1. 表单路径以.为层级分隔符, 如 a.b.c 指向数据 {a:{b:{c:1}}} 下的 1
  2. 路径支持模糊匹配,在校验和副作用配置中会使用到。如下例子。通常在数组场景中使用较多。

注意:* 仅代表下钻一级

arr.*arr 字段的所有一级子项
arr.x.*arr.x 的所有一级子项
arr.*.xarr 所有一级子项下的 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, 可以直接使用。

  1. 在 render 函数第一层直接使用 props.form
const formMeta = {
  render: ({form}) =>
  <div>
    {form.getValueIn('my.path')}
  </div>
}
  1. 在组件内部可使用 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)