Parcourir la source

feat(form): validate support dynamic function (#603)

* feat(form): validate support dynamic function

* docs: update validate docs
xiamidaxia il y a 5 mois
Parent
commit
cd0fdc8f37

+ 5 - 0
apps/demo-fixed-layout/src/nodes/default-form-meta.tsx

@@ -45,6 +45,11 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON['data']> = {
    * @param ctx
    */
   formatOnSubmit: (value, ctx) => value,
+  /**
+   * Supported writing as:
+   * 1: validate as options: { title: () => {} , ... }
+   * 2: validate as dynamic function: (values,  ctx) => ({ title: () => {}, ... })
+   */
   validate: {
     title: ({ value }) => (value ? undefined : 'Title is required'),
     'inputsValues.*': ({ value, context, formValues, name }) => {

+ 5 - 0
apps/demo-free-layout/src/nodes/default-form-meta.tsx

@@ -31,6 +31,11 @@ export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
 export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
   render: renderForm,
   validateTrigger: ValidateTrigger.onChange,
+  /**
+   * Supported writing as:
+   * 1: validate as options: { title: () => {} , ... }
+   * 2: validate as dynamic function: (values,  ctx) => ({ title: () => {}, ... })
+   */
   validate: {
     title: ({ value }) => (value ? undefined : 'Title is required'),
     'inputsValues.*': ({ value, context, formValues, name }) => {

+ 1 - 0
apps/demo-free-layout/src/plugins/context-menu-plugin/context-menu-layer.tsx

@@ -35,6 +35,7 @@ export class ContextMenuLayer extends Layer {
 
   onReady() {
     this.listenPlaygroundEvent('contextmenu', (e) => {
+      if (this.config.readonlyOrDisabled) return;
       this.openNodePanel(e);
       e.preventDefault();
       e.stopPropagation();

+ 6 - 0
apps/docs/src/en/guide/advanced/form.mdx

@@ -64,6 +64,8 @@ export const nodeRegistries: FlowNodeRegistry[] = [
       validateTrigger: ValidateTrigger.onChange,
       /**
        * Configure validation rules, 'content' is the field path, the following configuration values validate data under this path
+       * Use Dynamic function  to generate a validator based on values:
+       *  validate: (values, ctx) => ({ content: () => {}, })
        */
       validate: {
         content: ({ value }) => (value ? undefined : 'Content is required'),
@@ -246,6 +248,10 @@ export const VALIDATE_EXAMPLE: FormMeta = {
   render: renderValidateExample,
   // Validation timing configuration
   validateTrigger: ValidateTrigger.onChange,
+  /*
+   * Use Dynamic function to generate a validator based on values:
+   *  validate: (values, ctx) => ({ a: () => '', b: () => '', c, () => '' })
+  */
   validate: {
     // Simply validate value
     a: ({ value }) => (value.length > 5 ? 'Max length is 5' : undefined),

+ 8 - 1
apps/docs/src/zh/guide/advanced/form.mdx

@@ -64,7 +64,10 @@ export const nodeRegistries: FlowNodeRegistry[] = [
       validateTrigger: ValidateTrigger.onChange,
       /**
        * 配置校验规则, 'content' 为字段路径,以下配置值对该路径下的数据进行校验。
-       */
+       *
+       * 也可支持动态函数写法, 用于根据 values 生成校验器:
+       *  validate: (values, ctx) => ({ content: () => {}, })
+      */
       validate: {
         content: ({ value }) => (value ? undefined : 'Content is required'),
       },
@@ -250,6 +253,10 @@ 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),

+ 23 - 0
packages/node-engine/form/__tests__/form-model.test.ts

@@ -4,6 +4,7 @@
  */
 
 import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { mapValues } from 'lodash';
 
 import { ValidateTrigger } from '@/types';
 import { FormModel } from '@/core/form-model';
@@ -162,6 +163,28 @@ describe('FormModel', () => {
       expect(formModel.state?.errors?.['a.b.x']).toEqual([]);
       expect(formModel.state?.errors?.['a.b.y']).toEqual([]);
     });
+    it('validate as dynamic function', async () => {
+      formModel.init({
+        initialValues: { a: 3, b: 'str' },
+        validate: (v, ctx) => {
+          expect(ctx).toEqual('context');
+          return mapValues(v, (value) => {
+            if (typeof value === 'string') {
+              return () => 'string error';
+            }
+            return () => 'num error';
+          });
+        },
+        context: 'context',
+      });
+      const fieldResult = await formModel.validateIn('a');
+      expect(fieldResult).toEqual(['num error']);
+      const results = await formModel.validate();
+      expect(results).toEqual([
+        { name: 'a', message: 'num error', level: 'error' },
+        { name: 'b', message: 'string error', level: 'error' },
+      ]);
+    });
   });
   describe('FormModel set/get values', () => {
     beforeEach(() => {

+ 21 - 10
packages/node-engine/form/src/core/form-model.ts

@@ -20,7 +20,7 @@ import {
   OnFormValuesUpdatedPayload,
 } from '../types/form';
 import { FieldName, FieldValue } from '../types/field';
-import { Errors, FeedbackLevel, FormValidateReturn, Warnings } from '../types';
+import { Errors, FeedbackLevel, FormValidateReturn, Validate, Warnings } from '../types';
 import { createFormModelState } from '../constants';
 import { getValidByErrors, mergeFeedbacks } from './utils';
 import { Store } from './store';
@@ -244,16 +244,17 @@ export class FormModel<TValues = any> implements Disposable {
   }
 
   async validateIn(name: FieldName) {
-    if (!this._options.validate) {
+    const validateOptions = this.getValidateOptions();
+    if (!validateOptions) {
       return;
     }
 
-    const validateKeys = Object.keys(this._options.validate).filter((pattern) =>
+    const validateKeys = Object.keys(validateOptions).filter((pattern) =>
       Glob.isMatch(pattern, name)
     );
 
     const validatePromises = validateKeys.map(async (validateKey) => {
-      const validate = this._options.validate![validateKey];
+      const validate = validateOptions![validateKey];
 
       return validate({
         value: this.getValueIn(name),
@@ -266,19 +267,29 @@ export class FormModel<TValues = any> implements Disposable {
     return Promise.all(validatePromises);
   }
 
+  protected getValidateOptions(): Record<string, Validate> | undefined {
+    const validate = this._options.validate;
+    if (typeof validate === 'function') {
+      return validate(this.values, this.context);
+    }
+    return validate;
+  }
+
   async validate(): Promise<FormValidateReturn> {
-    if (!this._options.validate) {
+    const validateOptions = this.getValidateOptions();
+    if (!validateOptions) {
       return [];
     }
 
-    const feedbacksArrPromises = Object.keys(this._options.validate).map(async (nameRule) => {
-      const validate = this._options.validate![nameRule];
-      const paths = Glob.findMatchPathsWithEmptyValue(this.values, nameRule);
+    const feedbacksArrPromises = Object.keys(validateOptions).map(async (nameRule) => {
+      const validate = validateOptions![nameRule];
+      const values = this.values;
+      const paths = Glob.findMatchPathsWithEmptyValue(values, nameRule);
       return Promise.all(
         paths.map(async (path) => {
           const result = await validate({
-            value: get(this.values, path),
-            formValues: this.values,
+            value: get(values, path),
+            formValues: values,
             context: this.context,
             name: path,
           });

+ 3 - 1
packages/node-engine/form/src/types/form.ts

@@ -53,7 +53,9 @@ export interface FormOptions<TValues = any> {
   /**
    * Form data's validation rules. It's a key value map, where the key is a pattern of data's path (or field name), the value is a validate function.
    */
-  validate?: Record<string, Validate>;
+  validate?:
+    | Record<string, Validate>
+    | ((value: TValues, ctx: Context) => Record<string, Validate>);
   /**
    * Custom context. It will be accessible via form instance or in validate function.
    */

+ 3 - 1
packages/node-engine/node/src/types.ts

@@ -97,7 +97,9 @@ export interface FormMeta<TValues = any> {
   /**
    * Form data's validation rules. It's a key value map, where the key is a pattern of data's path (or field name), the value is a validate function.
    */
-  validate?: Record<FieldName, Validate>;
+  validate?:
+    | Record<FieldName, Validate>
+    | ((values: TValues, ctx: NodeContext) => Record<FieldName, Validate>);
   /**
    * Form data's effects. It's a key value map, where the key is a pattern of data's path (or field name), the value is an array of effect configuration.
    */