Formily的core的经验汇总

2021-07-14 fishedee 前端

0 概述

我们进入了Formily的下一步,core库,其实有了reactive组件以后,我们开发表单已经比原来要简直快捷多了,但是为什么还需要core库呢?

让我们先看一下,只用Formily的Reactive库的情况,开发一个表单会遇到什么问题。

0.1 为什么

代码在这里

0.1.1 目标

我们目标在只用Formily的Reactive库情况下,建立这个有4个表单项的表单。要求为:

表单有required的校验项,首次未输入的时候不触发,而后的每次input变动的时候,当value为空的时候弹出错误

可以通过按钮,来回切换“名字”项下的input的placeholder属性

可以通过按钮,来更改“年龄”项下的title与input组件之间的距离

可以通过按钮,来回切换“密码”项的Input或者Password组件

“名字长度”是不可修改属性项,它的值由“名字”项来自动计算出来的

最后,我们要求框架开发者足够的抽象性,让使用者轻松实现直观地实现以上功能

0.1.2 context

import React, {
    createContext,
    ReactElement,
    useContext,
    FunctionComponent,
    ReactNode,
} from 'react';
import { observer } from '@formily/reactive-react';

export type ValidatorType = (data: any) => string;

export type FieldType = {
    title: string;
    value: any;
    errors: string[];
    visible: boolean;
    validator: ValidatorType[];
    component: (props: any) => JSX.Element;
    componentProps: object;
    decorator: React.FunctionComponent;
    decoratorProps: object;
    onInput: (data: any) => void;
};

export type FormType = {
    [key in string]: FieldType;
};

export function validate(data: any, validator: ValidatorType[]): string[] {
    let errors = [];
    for (let i in validator) {
        let singleValidator = validator[i];
        let error = singleValidator(data);
        if (error != '') {
            errors.push(error);
        }
    }
    return errors;
}

//创建上下文,方便Field消费
const FormContext = createContext<FormType>({});
//创建上下文,方便FormItem消费
const FieldContext = createContext<FieldType>({} as FieldType);

export { FormContext };
export { FieldContext };

//表单管理入口
type FormProviderProps = {
    form: FormType;
    children: ReactNode;
};
export const FormProvider = (props: FormProviderProps) => {
    return (
        <FormContext.Provider value={props.form}>
            {props.children}
        </FormContext.Provider>
    );
};

//状态桥接器组件
type FieldWrapperType = {
    name: string;
};
export const Field = observer((props: FieldWrapperType) => {
    console.log('Child Component Field: ' + props.name + ' Render');
    const form = useContext(FormContext);
    const field = form[props.name];
    if (!field.visible) return null;
    //渲染字段,将字段状态与UI组件关联
    const component = React.createElement(field.component, {
        ...field.componentProps,
        value: field.value,
        onChange: field.onInput,
    } as React.Attributes);

    //渲染字段包装器
    const decorator = React.createElement(
        field.decorator,
        field.decoratorProps,
        component,
    );

    return (
        <FieldContext.Provider value={field}>{decorator}</FieldContext.Provider>
    );
});

我们首先建立FormContext与FieldContext组件,这是借鉴了core库的实现,我们用了useContext,这能在不破坏实现的程度,轻松在多层组件中传递当前的field与form。同时,我们进一步创建了FormProvider与Field组件

0.1.3 FormItem

import { observer } from '@formily/reactive-react';
import { FieldContext } from './Context';
import { useContext } from 'react';

// FormItem UI组件
export default observer(({ children }) => {
    const field = useContext(FieldContext);
    const decoratorProps = field.decoratorProps as any;
    let style = {};
    if (decoratorProps.style) {
        style = decoratorProps.style;
    }
    return (
        <div>
            <div style={{ height: 20, ...style }}>{field.title}:</div>
            {children}
            <div style={{ height: 20, fontSize: 12, color: 'red' }}>
                {field.errors.join(',')}
            </div>
        </div>
    );
});

我们创建了FormItem,作为Form表单项的包围组件,它用来显示标题,以及错误

0.1.4 RequireValidator

export default function () {
    return function (data: any): string {
        if (typeof data === 'string') {
            return data == '' ? '缺少参数' : '';
        } else if (typeof data === 'undefined') {
            return '缺少参数';
        } else {
            return '';
        }
    };
}

这是必填项的校验器

0.1.5 可复用组件

import React from 'react'
import {FieldType} from './Context'

// Input UI组件
export default (props:FieldType) => {
    return (
      <input
        {...props}
        value={props.value || ''}
        style={{
          border: '2px solid rgb(186 203 255)',
          borderRadius: 6,
          width: '100%',
          height: 28,
          padding: '0 5px',
        }}
      />
    )
}

Input组件

import React from 'react'
import {FieldType} from './Context'

// Input UI组件
export default (props:FieldType) => {
    return (
      <input
        type="number" 
        {...props}
        value={props.value || ''}
        style={{
          border: '2px solid rgb(186 203 255)',
          borderRadius: 6,
          width: '100%',
          height: 28,
          padding: '0 5px',
        }}
      />
    )
}

Input Digit 组件

import React from 'react';
import { FieldType } from './Context';

// Input UI组件
export default (props: FieldType) => {
    return (
        <input
            type="password"
            {...props}
            value={props.value || ''}
            style={{
                border: '2px solid rgb(186 203 255)',
                borderRadius: 6,
                width: '100%',
                height: 28,
                padding: '0 5px',
            }}
        />
    );
};

Password组件

import React from 'react';
import { FieldType } from './Context';

// Label UI组件
export default (props: FieldType) => {
    return (
        <div
            {...props}
            style={{
                border: '2px solid rgb(186 203 255)',
                borderRadius: 6,
                width: '100%',
                height: 28,
                padding: '0 5px',
            }}
        >
            {props.value ? props.value : ''}
        </div>
    );
};

纯展示组件

0.1.6 主页面

import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormProvider, Field, FieldType, validate } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';

const data = observable({
    name: {
        title: '名字',
        value: '',
        errors: [] as string[],
        visible: true,
        component: Input,
        componentProps: {
            placeholder: '你是谁',
        },
        decorator: FormItem,
        decoratorProps: {},
        validator: [RequireValidator()],
        onInput: function (e: any) {
            data.name.value = e.target.value;
            data.name.errors = validate(data.name.value, data.name.validator);
        },
    },
    nameLength: {
        title: '名字长度',
        value: 0,
        errors: [] as string[],
        visible: true,
        component: Label,
        componentProps: {
            disabled: true,
        },
        decorator: FormItem,
        decoratorProps: {},
        validator: [RequireValidator()],
        onInput: function (e: any) {},
    },
    age: {
        title: '年龄',
        value: undefined,
        errors: [] as string[],
        visible: true,
        component: InputDigit,
        componentProps: {},
        decorator: FormItem,
        decoratorProps: {
            style: { height: 20 },
        },
        validator: [RequireValidator()],
        onInput: function (e: any) {
            data.age.value = e.target.value;
            data.age.errors = validate(data.age.value, data.age.validator);
        },
    },
    password: {
        title: '密码',
        value: undefined,
        errors: [] as string[],
        visible: true,
        component: Password,
        componentProps: {},
        decorator: FormItem,
        decoratorProps: {},
        validator: [RequireValidator()],
        onInput: function (e: any) {
            data.password.value = e.target.value;
            data.password.errors = validate(
                data.password.value,
                data.password.validator,
            );
        },
    },
});

//派生属性
autorun(() => {
    data.nameLength.value = data.name.value.length;
});

export default () => {
    console.log('Top Render');
    return (
        <FormProvider form={data}>
            <button
                onClick={() => {
                    let componentProps = data.name.componentProps as any;
                    if (componentProps.placeholder.indexOf('你是谁') != -1) {
                        componentProps.placeholder = '我是我';
                    } else {
                        componentProps.placeholder = '你是谁';
                    }
                }}
            >
                切换name组件的componentProps[placeholder]
            </button>
            <Field name="name" />
            <Field name="nameLength" />
            <button
                onClick={() => {
                    let decoratorProps = data.age.decoratorProps as any;
                    decoratorProps.style.height += 5;
                }}
            >
                切换age组件的decoratorProps[style.height]
            </button>
            <Field name="age" />
            <button
                onClick={() => {
                    let field = data.password;
                    if (field.component == Password) {
                        field.component = Input;
                    } else {
                        field.component = Password;
                    }
                }}
            >
                切换password组件的Component
            </button>
            <Field name="password" />
        </FormProvider>
    );
};

最后,这是主页面。思路主要是:

  • 将表单的errors,component,componentProps,decorator,decoratorProps全部纳入Observable的管理中。这使得我们可以轻松地通过更改Observable的字段,来更新页面。
  • 将自定义组件,用value与onChange,设计为受控组件,而且是表单项UI的约定接口,这使得复用不同的UI组件,或者自定义UI组件更加简单。
  • Field组件,通过FormContext来获取当前的Form,使用自身的title属性获取自己所属的Field字段。并且,FormItem组件使用FieldContext来获取自己所属的Field信息。这使得Field组件即使在被多层div嵌套底下依然能正常使用,换句话说,使用FormProvider,并没有限制Field组件的排版。
  • 在onInput的输入中,进行各个字段的校验
  • 使用autorun来实现,自动化的数据联动

MVVM的原理让业务逻辑变得直观和轻松。

性能也不错,当”名字”项变动的时候,只有“名字”项,以及派生的”名字长度”项会重新render。顶层页面,以及其他组件都不需要重新render。而且,重点是,这种性能是不需要写任何的componentShouldUpdate的基础上的,纯粹地铺业务逻辑就可以得到较好的性能优化。

0.1.7 问题

经过上面的试验,我们也看出了一点其他的问题:

  • 每个表单的字段都需要重复写title,Component,errors,visible,onInput真的好麻烦。作为表单这个特定的领域下,应该有一个通用的领域描述对象,而不是每次都需要用户自己手写每一个字段。
  • 如果某个组件,需要同时控制age与name两个字段,显然现在这种做法还做不到。(目前我的项目基本上都遇到这个需求,这真的不是奇怪的需求)
  • 当页面onInput的时候,需要用户手动去调用Validator的函数,这不应该是自动触发的吗,麻烦。
  • Validator的触发方式,应该包括有onInput,onFocus,onBlur等不同情况的实现。而不是,只有onInput就触发Validator的实现。
  • autorun能实现被动的数据联动,但是当数据是循环联动的时候,这种被动联动就会造成无穷循环触发。所以,应该还需要有主动的数据联动的支持。(认真思考一下,为什么会出现无穷循环触发)
  • 如果对表单的状态进行持久化(读与写)的话,应该有提供这样的方法。毕竟你不能直接对Observable持久化,里面有React组件,还有function。
  • 表单里面应该还支持嵌套表单,嵌套对象编辑的情况
  • 表单的初始值怎样导入
  • 缺少表单对象的反馈,例如当表单任意一个字段的变化时,希望得到回调通知

0.2 有什么

好了,现在core库出来了,官方文档在这里。Core库的定位是MVVM中的ViewModel层,它是针对表单这个特定的领域,创建的一个通用的Observable对象。这样会大幅降低我们的开发成本,但是有一定的门槛。如果说,Reactive仅仅是对Mobx的一种性能优化版本而已,那么Core库就是站在海量的表单业务下抽象出来的通用组件,它的设计的确很棒!

Reactive作为一个领域对象,它的核心内容是:

  • 字段值的获取与更新,包括value与initalValue
  • 显示与隐藏,visbile与hidden
  • 校验器与消息反馈,校验器(validator)是对字段变化的响应,消息反馈(feedback)是校验器执行后的结果
  • 交互模式,表单是以展示,还是编辑,还是不可编辑的样式。
  • 生命周期,表单与字段级别的Init,Mount,Unmount,Change,onInput,onValidate的通知
  • 依赖追踪器,更轻松地实现主动与被动联动
  • 对Object与List支持,ObjectField与ArrayField。
  • 对装饰组件的支持,VoidField没有字段值,它仅仅是布局与装饰的描述
  • 字段路径,对字段地址的DSL描述。这个设计实在巧妙

最后,Reactive的设计是UI无关的,业务逻辑与UI组件是彻底解耦的。换句话说,底层的UI组件任意变动情况下,业务逻辑的代码一行都不用改!

由于代码是UI无关的,所以在后续的测试代码中,生命周期的事件都是需要手动触发的。在实际的使用环境中,组件的生命周期事件是自动触发的,这点要注意。

1 字段值的获取与更新

代码在这里

表单中最重要的属性当然是value,每个表单项的值,但是core库提供了多个可以对表单项进行set与get操作的API,它们的区别是什么?难点在于:

  • 当要对表单项A进行set操作的时候,表单项A可能还没有创建起来,如何对一个还没创建的表单项进行set与get的操作?
  • 表单项除了有value,还有initialValue,它们之间的区别是什么,什么时候一个值会覆盖另外一个值,什么时候不会?

1.1 当前值

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            initialValues: {
                name: 'fish',
            },
            effects() {
                onFieldMount('name', (field) => {
                    console.log('field : [name] mount!');
                });
                onFieldChange('name', (field, form) => {
                    //当Field是VoidField的时候,没有value
                    let field2 = field as Field;
                    console.log('field : [name] change!', field2.value);
                });
                onFieldChange('name', ['component'], (field, form) => {
                    console.log(
                        'field : [name] component change!',
                        field.component,
                    );
                });

                //测试已创建field,但没初始化的字段,get操作,
                onFieldChange('name', (field, form) => {
                    //获取value的方法1,直接以本field为基点查找其他field
                    console.log(field.query('.age').value());

                    //获取value的方法2,query以后使用take转换为GeneralField
                    let ageField = field.query('.age').take() as Field;
                    console.log(ageField.value);

                    //获取value的方法3,通过form获取value,需要field在initialValues已经配置好了
                    console.log(form.values.age);

                    //获取value的方法4,通过form获取value
                    console.log(form.getValuesIn('age'));

                    //获取value的方法5,通过form获取value
                    console.log(form.getFieldState('age').value);
                });

                /*
                对于未创建field,以及还没初始化的字段,要进行get操作的话
                field.query('xxx').value(),相当方便的相对位置查找
                field.query('xxx').take().value,相当方便的相对位置查找,可以获取多种属性
                form.getValuesIn,获取value,只能从顶层开始查找
                form.getFieldState(),获取多种属性,只能从顶层开始查找
                 */

                //测试对未创建field,以及还没初始化的字段,get操作,
                onFieldChange('name', (field, form) => {
                    //获取value的方法1,直接以本field为基点查找其他field
                    console.log(field.query('.age1').value());

                    //获取value的方法2,通过form获取value
                    console.log(form.getValuesIn('age2'));

                    //获取value的方法3,通过form获取value,需要field在initialValues已经配置好了
                    //但是当字段是深层嵌套的时候就会报错
                    console.log(form.values.age3);

                    //获取value的方法4,query以后使用take转换为GeneralField
                    //这个方法不行,因为ageField还没通过createField创建出来
                    //let ageField = field.query('.age4').take() as Field;
                    //console.log(ageField.value);

                    //获取value的方法5,通过form获取value
                    //这个方法也不行,因为ageField还没通过createField创建出来
                    //console.log(form.getFieldState('age5').value);
                });

                /*
                对于未创建field,以及还没初始化的字段,要进行get操作的话
                field.query('xxx').value(),相当方便的相对位置查找
                form.getValuesIn,获取value,只能从顶层开始查找
                 */

                //测试对未创建field,以及还没初始化的字段,set操作,
                onFieldChange('name', (field, form) => {
                    //设置value的方法1,直接以本field为基点查找其他field
                    //这个方法不行
                    //let field2 = field.query('.age1').take() as Field;
                    //field2.value = 10;

                    //设置value的方法2,通过form获取value
                    form.setValuesIn('age2', 11);

                    //设置value的方法3,通过form获取value
                    //但是当字段是深层嵌套的时候就会报错
                    form.values.age3 = 12;

                    //获取value的方法4,直接以本field为基点查找其他field
                    //如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
                    form.setFieldState('age4', (state) => {
                        console.log('age4 set');
                        state.value = 13;
                        //这个方法可以设置field的其他属性
                        state.componentProps = { a: 3 };
                    });
                });

                /*
                对于未创建field,以及还没初始化的字段,要进行set操作的话
                form.setValuesIn,只设置value
                form.setFieldState,设置value,component等其他任意属性
                 */
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createField的时候就会触发onFieldChange,包括value与component
                    let ageField = form.createField({ name: 'age' });

                    console.log('create Field');
                    //createField的时候就会触发onFieldChange,包括value与component
                    let field = form.createField({ name: 'name' });

                    //手动触发field的onMount方法
                    console.log('mount');
                    field.onMount();

                    let setHasCrateFieldOfValue = async () => {
                        //手动更改field的value的方法
                        console.log('change value');
                        field.value = '123';

                        //手动更改field的value的方法2
                        await sleep(1000);
                        field.setValue('1234');

                        //手动更改field的value的方法3,通过form,需要有initalValue
                        await sleep(1000);
                        form.values.name = '12345';

                        //手动更改field的value的方法4,通过form
                        await sleep(1000);
                        form.setValuesIn('name', '123456');

                        //手动更改field的value的方法5
                        //这种方法会覆盖其他的字段,像setState一样与其他字段的原有值合并后一起更新
                        await sleep(1000);
                        form.setValues({
                            name: '1234567',
                            age: 11,
                        });

                        //手动更改field的value的方法6

                        form.setFieldState('name', (state) => {
                            state.value = '12345678';
                        });
                    };
                    await setHasCrateFieldOfValue();
                }}
            >
                点击,更改[fish]field的值
            </button>
        </div>
    );
};

对于未创建field,并初始化的字段,要进行get操作的话:

  • field.query(‘xxx’).value(),相当方便的相对位置查找
  • field.query(‘xxx’).take().value,相当方便的相对位置查找,可以获取多种属性
  • form.getValuesIn,获取value,只能从顶层开始查找
  • form.getFieldState(),获取多种属性,只能从顶层开始查找

对于未创建field,但还没初始化的字段,要进行get操作的话:

  • field.query(‘xxx’).value(),相当方便的相对位置查找
  • form.getValuesIn,获取value,只能从顶层开始查找

对于未创建field,以及还没初始化的字段,要进行set操作的话:

  • form.setValuesIn,只设置value
  • form.setFieldState,设置value,component等其他任意属性

1.2 未赋值时,设置初始值

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldChange('name', (field, form) => {
                    //当Field是VoidField的时候,没有value
                    let field2 = field as Field;
                    console.log('field : [name] value = ', field2.value);
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //初始化的时候,值为undefined
                    let field = form.createField({ name: 'name' });

                    //数据未赋值过的情况下,设置initialValue,会自动赋值到value
                    await sleep(1000);
                    form.setInitialValues({
                        name: '213',
                    });
                }}
            >
                点击,初始化name的value值
            </button>
        </div>
    );
};

数据未赋值过的情况下,设置initialValue,会自动赋值到value。覆盖更新

1.3 已赋值时,设置初始值

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldChange('name', (field, form) => {
                    //当Field是VoidField的时候,没有value
                    let field2 = field as Field;
                    console.log('field : [name] value = ', field2.value);
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //初始化的时候,值为undefined
                    let field = form.createField({ name: 'name' });

                    //设置了一次value
                    await sleep(1000);
                    field.setValue('1');

                    //后续再触发initialValues,依然会触发设置value
                    await sleep(1000);
                    form.setInitialValues({
                        name: '2',
                    });
                }}
            >
                点击,初始化name的value值
            </button>
        </div>
    );
};

数据已赋值过的情况下,设置initialValue,会自动赋值到value。覆盖更新

1.4 已用户输入时,设置初始值

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldChange('name', (field, form) => {
                    //当Field是VoidField的时候,没有value
                    let field2 = field as Field;
                    console.log('field : [name] value = ', field2.value);
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //初始化的时候,值为undefined
                    let field = form.createField({ name: 'name' });

                    //使用onInput,来模拟用户输入
                    await sleep(1000);
                    field.onInput('1');

                    //onInput以后,使用form来设置initialValues是会失败的,无法覆盖进去
                    await sleep(1000);
                    form.setInitialValues({
                        name: '2',
                    });
                }}
            >
                点击,初始化name的value值
            </button>
        </div>
    );
};

数据已被用户输入的情况下,设置initialValue,无法赋值到value。阻止更新

2 显示与隐藏

代码在这里

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createField的时候就会触发onFieldChange,包括value与component
                    let field = form.createField({ name: 'name' });

                    //模拟用户输入
                    field.onInput('fish');
                    console.log(field.value);

                    //使用display为none,会导致value被清空为undefined
                    await sleep(1000);
                    field.display = 'none';
                    console.log(field.value);
                }}
            >
                点击,更改[name]的display为none
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //createField的时候就会触发onFieldChange,包括value与component
                    let field = form.createField({ name: 'name2' });

                    //模拟用户输入
                    field.onInput('cat');
                    console.log(field.value);

                    //使用display为hidden,value依然不会被清空
                    await sleep(1000);
                    field.display = 'hidden';
                    console.log(field.value);
                }}
            >
                点击,更改[name2]的display为hidden
            </button>
        </div>
    );
};

display有三种状态,visible,none和hidden。

  • visible是正常的显示状态
  • hidden是隐藏状态,组件会消失,但数据不会清空
  • none是去掉状态,组件会消失,而且数据会清空

3 校验器与消息反馈

代码在这里

校验器是数据变更的自动触发的行为,消息反馈是校验器计算的结果。表单的校验器的业务难点在于:

  • 要区分什么时候进行校验,值变更,onInput,onFoucs,onBlur?
  • 校验的过程可以是异步进行的
  • 消息反馈可能是多个校验器的结果
  • 校验的提示可以是

3.1 触发机制

3.1.1 非值变更触发

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createField,并且设置required为true
                    let field = form.createField({
                        name: 'name',
                        required: true,
                    });

                    //模拟用户输入
                    field.onInput('f');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为空数组
                    //feedbacks是空数组
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入
                    field.onInput('');
                    await sleep(100);
                    //field.errors为1个元素的数组,数据为"该字段是必填字段"
                    //feedbacks是3个元素的数组
                    /*
                    [{
                        code: "ValidateError"
                        messages: ["该字段是必填字段"]
                        triggerType: "onInput"
                        type: "error"
                    },
                    {
                        code: "ValidateSuccess"
                        messages: []
                        triggerType: "onInput"
                        type: "success"
                    },{
                        code: "ValidateWarning"
                        messages: []
                        triggerType: "onInput"
                        type: "warning"
                    }]
                     */

                    console.log(field.feedbacks, field.errors);
                }}
            >
                onInput触发校验,required的校验,校验的结果放在feedbacks上
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //createField,并且设置required为true
                    let field = form.createField({
                        name: 'name2',
                        required: true,
                    });

                    //直接修改数据
                    field.value = 'f';
                    await sleep(100);
                    //field.errors为空数组
                    //feedbacks是空数组
                    console.log(field.feedbacks, field.errors);

                    //直接修改数据
                    field.value = '';
                    await sleep(100);
                    //errors依然为空,因为validator是基于onInput,onFocus或者onBlur来触发的,直接修改value是不会触发校验的
                    console.log(field.feedbacks, field.errors);
                }}
            >
                直接修改value是没有触发校验的
            </button>
        </div>
    );
};

在实验中可以看到,onInput能触发消息反馈,仅仅的值变更不会触发消息反馈。这样的设计是经过考虑的,因为程序员设置的数据不应该触发校验器,而只有用户输入的数据才应该触发校验器。而且,经过onInput触发校验器,能避免首次校验出错的问题。试想一下,刚开始创建的表单项,值当然为undefined,这个时候就触发required校验器,意味着所有的表单项都有错误提示的,这样当然是不好的用户体验。

3.1.2 onBlur与onFocus触发

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name',
                    });
                    field.setValidator({
                        triggerType: 'onBlur',
                        validator: (value) => {
                            console.log('value:', value);
                            if (value != '123') {
                                return '输入的字符串不是123';
                            }
                            return '';
                        },
                    });

                    //直接设置value
                    field.onInput('f');
                    await sleep(500);
                    //field.errors依然为空数组,因为triggerType为onBlur
                    console.log(field.feedbacks, field.errors);

                    //模拟控件blur
                    field.onBlur();
                    await sleep(500);
                    //field.errors现在不是为空数组了,因为blur的时候触发了校验
                    //它的值为1个元素的数组
                    console.log(field.feedbacks, field.errors);
                }}
            >
                onBlur触发校验
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name2',
                    });
                    field.setValidator({
                        triggerType: 'onFocus',
                        validator: (value) => {
                            console.log('value:', value);
                            if (value != '123') {
                                return '输入的字符串不是123';
                            }
                            return '';
                        },
                    });

                    //直接设置value
                    field.onInput('f');
                    await sleep(500);
                    //field.errors依然为空数组,因为triggerType为onBlur
                    console.log(field.feedbacks, field.errors);

                    //模拟控件focus
                    field.onFocus();
                    await sleep(500);
                    //field.errors现在不是为空数组了,因为blur的时候触发了校验
                    //它的值为1个元素的数组
                    console.log(field.feedbacks, field.errors);
                }}
            >
                onFocus触发校验
            </button>
        </div>
    );
};

core库除了支持onInput的时候触发,还支持onBlur与onFocus的校验触发。当然,组件自身需要配置core库进行回调onBlur与onFocus方法才能成功。自定义校验器在没有配置triggerType的情况下,默认就是onInput的触发校验。

3.2 校验方式

3.2.1 规则校验

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'age',
                        validator: {
                            required: true, //必填项
                            minimum: 5, //最小值为5
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput(1);
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput(7);
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的属性方式校验,minimum,最小值
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'name',
                        validator: {
                            required: true, //必填项
                            min: 5, //字符串长度最小为5
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput('abc');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('abcdef');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的属性方式校验,min,最小字符串长度
            </button>
        </div>
    );
};

像required,min,minimum都是规则校验的方式,具体可以看这里

3.2.2 自定义规则校验

import {
    createForm,
    onFieldChange,
    onFieldMount,
    registerValidateRules,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

registerValidateRules({
    GlobalPropertyFormat(value) {
        if (!value) return '';
        return value !== '123' ? '错误了❎' : '';
    },
});
export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'age',
                        validator: {
                            required: true, //必填项
                            GlobalPropertyFormat: true, //自定义的format
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput('137');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('123');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的自定义属性方式校验,123格式
            </button>
        </div>
    );
};

core库支持自定义规则校验,通过registerValidateRules注册一个全局的校验规则,就可以在validator中使用这个规则项了

3.2.3 格式校验

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'age',
                        validator: {
                            required: true, //必填项
                            format: 'phone', //格式必须为电话格式
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput('137');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('13712345678');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的format方式校验,phone格式
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'name',
                        validator: {
                            required: true, //必填项
                            format: 'email', //格式必须为url格式
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput('abc');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('abc@qq.com');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的format方式校验,email格式
            </button>
        </div>
    );
};

格式校验,就是用validator的format项进行校验,这种校验方式都是以正则表达式为依据的校验。core库默认就写好了很多格式的校验,具体看这里

3.2.4 自定义格式校验

import {
    createForm,
    onFieldChange,
    onFieldMount,
    registerValidateFormats,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

//注册一个自定义的format
registerValidateFormats({
    MyFormat: /123/,
});

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    let field = form.createField({
                        name: 'age',
                        validator: {
                            required: true, //必填项
                            format: 'MyFormat', //格式必须为电话格式
                        },
                    });

                    //模拟用户输入,错误输入
                    field.onInput('137');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为不为空
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('123');
                    //校验过程是异步的,所以需要一个await
                    await sleep(100);
                    //field.errors为空
                    console.log(field.feedbacks, field.errors);
                }}
            >
                validator的自定义format方式校验,123格式
            </button>
        </div>
    );
};

我们可以用registerValidateFormats来注册一个全局的自定义格式,然后在validator的format中指定我们的格式名称就可以了。

3.2.5 闭包校验

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name',
                    });
                    field.setValidator((value) => {
                        if (value != '123') {
                            return '非123格式';
                        }
                        return '';
                    });

                    //模拟用户输入,错误输入
                    field.onInput('897');
                    await sleep(500);
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('123');
                    await sleep(500);
                    console.log(field.feedbacks, field.errors);
                }}
            >
                自定义闭包的校验方式,123格式
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name2',
                    });
                    field.setValidator({
                        triggerType: 'onFocus',
                        validator: (value) => {
                            if (value != '123') {
                                return '输入的字符串不是123';
                            }
                            return '';
                        },
                    });

                    //模拟用户输入,错误输入
                    field.value = '897';
                    field.onFocus();
                    await sleep(500);
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.value = '123';
                    field.onFocus();
                    await sleep(500);
                    console.log(field.feedbacks, field.errors);
                }}
            >
                自定义闭包的校验方式,123格式,带triggerType
            </button>
        </div>
    );
};

闭包校验的方式,就是一种局部的校验方式了。在validator中直接传入闭包函数,或者一个对象,含有triggerType与validator属性就可以了。注意一下,自定义闭包校验的时候,是如何返回错误描述的。

3.2.6 异步校验

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name',
                    });
                    field.setValidator(async (value) => {
                        await sleep(1000);
                        if (value != '123') {
                            return '非123格式';
                        }
                        return '';
                    });

                    //模拟用户输入,错误输入
                    field.onInput('897');
                    await sleep(1500);
                    console.log(field.feedbacks, field.errors);

                    //模拟用户输入,正确输入
                    field.onInput('123');
                    await sleep(1500);
                    console.log(field.feedbacks, field.errors);
                }}
            >
                自定义闭包的异步校验方式,支持返回的是Promise的对象
            </button>
        </div>
    );
};

core库的设计中本来就支持异步的校验,只需要在闭包中返回一个Promise就可以了。返回Promise容易呀,我们完全可以用async来包装一下函数就可以了。

3.2.7 组合校验

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //我们使用自定义的校验器,并且设置只有blur的时候才触发校验
                    let field = form.createField({
                        name: 'name',
                    });
                    field.setValidator([
                        { required: true },
                        { format: 'email' },
                        {
                            triggerType: 'onInput',
                            validator: (value) => {
                                let str = value as string;
                                if (str.startsWith('fish@') == false) {
                                    return '只支持fish的发件人';
                                }
                                return '';
                            },
                        },
                    ]);

                    //直接设置value
                    field.onInput('');
                    await sleep(500);
                    //2个错误,非必填,不是fish开头
                    console.log(field.feedbacks, field.errors);

                    //直接设置value
                    field.onInput('fish@123');
                    await sleep(500);
                    //1个错误,非email格式
                    console.log(field.feedbacks, field.errors);

                    //直接设置value
                    field.onInput('123@163.com');
                    await sleep(500);
                    //1个错误,非fish开头
                    console.log(field.feedbacks, field.errors);
                }}
            >
                组合多种方式的校验
            </button>
        </div>
    );
};

validator也支持多种校验方式的组合,直接传递一个数组进去就可以了,然后数组的每一项可以是规则校验,格式校验,和闭包校验的其中一种都可以。

4 交互模式

代码在这里

表单的交互模式有四种,core库在这点考虑得比较细致,分别是:

  • editable,可以编辑的正常状态
  • disabled,不可以编辑,保留输入框,输入框是不可激活的
  • readOnly,不可以编辑,保留输入框,输入框是可激活的
  • readPretty,不可以编辑,不保留输入框
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { Field } from '@formily/react';
import { Form, FormItem, Input, Password, Submit } from '@formily/antd';

export default () => {
    const form = useMemo(() => {
        return createForm({
            initialValues: {
                username: 'a',
                username2: 'b',
                username3: 'c',
                username4: 'd',
            },
            effects() {},
        });
    }, []);

    return (
        <Form
            form={form}
            layout="vertical"
            size="small"
            onAutoSubmit={console.log}
        >
            <Field
                name="username"
                title="用户名"
                //交互模式,可编辑,框是深灰
                pattern="editable"
                required
                decorator={[FormItem]}
                component={[Input, {}]}
            />
            <Field
                name="username2"
                title="用户名2"
                //交互模式,不可编辑,框是浅灰色的,像被禁用了
                pattern="disabled"
                required
                decorator={[FormItem]}
                component={[Input, {}]}
            />
            <Field
                name="username3"
                title="用户名3"
                //交互模式,不可编辑,框是深灰
                pattern="readOnly"
                required
                decorator={[FormItem]}
                component={[Input, {}]}
            />
            <Field
                //交互模式,不可编辑,没有框
                name="username4"
                title="用户名4"
                pattern="readPretty"
                required
                decorator={[FormItem]}
                component={[Input, {}]}
            />
        </Form>
    );
};

代码还是比较简单的,这里我们直接用了antd的Formily包装组件

分别是editable,disabled,readOnly,readPretty的交互模式

5 生命周期

代码在这里

生命周期分两种,表单的生命周期,与字段的生命周期。生命周期包括:

  • 挂载时与卸载时
  • 表单或字段的,value和state变更的触发
  • 校验时的触发
  • onInput,onFocus,onBlur的触发

要注意,触发的时候,是否包括首次触发

5.1 字段生命周期

import {
    createForm,
    onFieldChange,
    onFieldInit,
    onFieldInputValueChange,
    onFieldMount,
    onFieldUnmount,
    onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                //createField的时候自动触发
                onFieldInit('name', (field) => {
                    console.log('字段[name]初始化');
                });

                //onMount的时候手动触发
                onFieldMount('name', (field) => {
                    console.log('字段[name] mount');
                });

                //onUnMount的时候手动触发
                onFieldUnmount('name', (field) => {
                    console.log('字段[name] unmount');
                });

                //onChange的时候自动触发,默认就是只有value变化的时候触发
                //注意,在首次createField也会自动触发
                onFieldChange('name', (field) => {
                    console.log('字段[name] change');
                });

                //onChange的时候自动触发,默认就是只有value变化的时候触发
                //注意,在首次createField也会自动触发,即使是非value的属性
                onFieldChange('name', 'componentProps', (field) => {
                    console.log('字段[name] comonentProps change');
                });

                //onValueChange的时候自动触发,只有value变化的时候触发
                //注意,首次不会自动触发
                onFieldValueChange('name', (field) => {
                    console.log('字段[name] value change');
                });

                //onInput的时候手动触发,这个注意与onFieldValueChange的不同
                onFieldInputValueChange('name', (field) => {
                    console.log('字段[name] value input change');
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    //触发onFieldInit,首次会触发onFieldChange
                    console.log('createField');
                    let field = form.createField({ name: 'name' });

                    //触发onFieldMount
                    await sleep(100);
                    console.log('onMount');
                    field.onMount();

                    //触发onFieldChange
                    await sleep(100);
                    console.log('set componentProps');
                    field.componentProps = { size: 10 };

                    //触发onFieldChange与onFieldValueChange
                    await sleep(100);
                    console.log('set value');
                    field.value = 10;

                    //触发onFieldChange与onFieldValueChange,onFieldInputValueChange
                    await sleep(100);
                    console.log('set input');
                    field.onInput('cat');

                    //触发onUnmount
                    await sleep(100);
                    console.log('set onUnmount');
                    field.onUnmount();
                }}
            >
                触发field的生命周期
            </button>
        </div>
    );
};

注意点如下:

  • onFieldChange,首次会触发,默认为value字段变更触发,也可以设置为其他属性变更时触发。这个用得比较多,而且能同时倾听多个字段的触发,后续会有介绍。
  • onFieldValueChange,首次不会触发,仅value字段变更时触发
  • onFieldInputValueChange,仅字段onInput的时候触发

5.2 表单生命周期

import {
    createForm,
    onFormInit,
    onFormInputChange,
    onFormMount,
    onFormSubmit,
    onFormUnmount,
    onFormValuesChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                //createForm的时候触发
                onFormInit(() => {
                    console.log('表单初始化');
                });

                //onMount的时候手动触发
                onFormMount(() => {
                    console.log('表单 mount');
                });

                //onUnMount的时候手动触发
                onFormUnmount(() => {
                    console.log('表单 unmount');
                });

                //onChange的时候自动触发,默认就是只有value变化的时候触发
                //注意,首次不会触发
                onFormValuesChange(() => {
                    console.log('form value change');
                });

                //onInput的时候手动触发,这个注意与onFieldValueChange的不同
                onFormInputChange(() => {
                    console.log('form value input change');
                });

                //submit的时候自动触发
                onFormSubmit(() => {
                    console.log('form submit');
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    //触发onFieldInit
                    console.log('createField');
                    let field = form.createField({ name: 'name' });

                    //触发form.onMount
                    await sleep(100);
                    console.log('onMount');
                    form.onMount();

                    //不会触发onFormValuesChange
                    await sleep(100);
                    console.log('set componentProps');
                    field.componentProps = { size: 10 };

                    //触发onFormValuesChange
                    await sleep(100);
                    console.log('set value');
                    field.value = 10;

                    //触发onFormInputChange与onFormValuesChange
                    await sleep(100);
                    console.log('set input');
                    field.onInput('cat');

                    //触发onSubmit
                    await sleep(100);
                    console.log('set submit');
                    form.submit();

                    //触发onUnmount
                    await sleep(100);
                    console.log('set onUnmount');
                    form.onUnmount();
                }}
            >
                触发form的生命周期
            </button>
        </div>
    );
};

注意点如下:

  • onFormValuesChange,如何onFieldValueChange的设计,首次不会触发,只有value变更的时候会触发
  • onFormInputChange,只有Form的任何一个字段回调onInput函数的时候触发
  • onFormSubmit,表单提交的时候触发

6 依赖追踪器

代码在这里

依赖跟踪,是数据联动与逻辑联动的关键实现。在core库中,主要分为主动联动,与被动联动两种。

6.1 主动联动

import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            initialValues: {
                count: 10,
                price: 1,
                total: 0,
            },
            effects() {
                //主动联动,对A数据主动倾听,然后设置B数据
                //对count数据倾听,然后设置total数据
                //首次createField(count) 会自动触发
                onFieldChange('count', (field) => {
                    console.log('Field[count] change');
                    const count = (field as Field).value;
                    const price = field.query('.price').value();
                    form.setFieldState('total', (state) => {
                        state.value = count * price;
                    });
                });

                //主动联动
                //对price数据倾听,然后设置total数据
                //首次createField(price) 会自动触发
                onFieldChange('price', (field) => {
                    console.log('Field[price] change');
                    const count = (field as Field).value;
                    const price = field.query('.count').value();
                    form.setFieldState('total', (state) => {
                        state.value = count * price;
                    });
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    let countField = form.createField({ name: 'count' });
                    let priceField = form.createField({ name: 'price' });
                    //只有totalField创建了以后,form的setFieldState才会生效
                    let totalField = form.createField({ name: 'total' });

                    //虽然初始值为0,但是这里已经被自动计算为10
                    let total = form.getValuesIn('total');
                    console.log(total);

                    //更改count的数值,total也会更新
                    countField.value = 20;
                    let total2 = form.getValuesIn('total');
                    console.log(total2);

                    //更改price的数值,total也会更新
                    priceField.value = 2;
                    let total3 = form.getValuesIn('total');
                    console.log(total3);
                }}
            >
                主动联动
            </button>
        </div>
    );
};

主动联动,就是指定数据变更以后,触发指定的回调方法,然后在回调方法里面更新字段的值。主动联动的实现,依赖于reactive的observe方法。

6.2 被动联动

import {
    createForm,
    onFieldChange,
    onFieldMount,
    onFieldReact,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            initialValues: {
                count: 10,
                price: 1,
                total: 0,
            },
            effects() {
                //被动联动,指定A数据受到B或者C数据的影响,当B数据或者C数据变化的时候,要自动触发
                //利用Reactive的能力,core库是自动收集依赖的,不需要显式指定依赖哪些数据,当依赖变化时,自动触发重新计算
                //首次createField(total) 会自动触发
                onFieldReact('total', (field) => {
                    console.log('total recompute');
                    const count = field.query('.count').value();
                    const price = field.query('.price').value();
                    const myField = field as Field;
                    myField.value = count * price;
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    let countField = form.createField({ name: 'count' });
                    let priceField = form.createField({ name: 'price' });
                    //只有totalField创建了以后,form的setFieldState才会生效
                    let totalField = form.createField({ name: 'total' });

                    //虽然初始值为0,但是这里已经被自动计算为10
                    let total = form.getValuesIn('total');
                    console.log(total);

                    //更改count的数值,total也会更新
                    countField.value = 20;
                    let total2 = form.getValuesIn('total');
                    console.log(total2);

                    //更改price的数值,total也会更新
                    priceField.value = 2;
                    let total3 = form.getValuesIn('total');
                    console.log(total3);
                }}
            >
                被动联动
            </button>
        </div>
    );
};

onFieldReact是core库中创新性的联动方法,大大方便了联动逻辑的实现。它会自动收集数据的依赖,当依赖数据发生变动以后,自动触发新数据的计算来赋值字段。被动联动的实现,依赖于reactive库的autorun方法。

7 其他Field类型

代码在这里

对于,表单中嵌套的数据类型,如数组,对象,core库也有支持。另外一方面,core库对于某些仅仅为了提供布局,和装饰用的虚拟字段提供支持。

7.1 ArrayField

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldInputValueChange('items', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);

    const form2 = useMemo(() => {
        return createForm({
            effects() {
                onFieldInputValueChange('items', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);

    const form3 = useMemo(() => {
        return createForm({
            effects() {
                onFieldInputValueChange('items', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createArrayField
                    let field = form.createArrayField({ name: 'items' });

                    //push操作会触发onInuput
                    field.push('fish2');
                    field.push('cat2');

                    //可以通过value获取到数组的值
                    console.log(field.value.length);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem = form.createField({ name: 'items.0' });
                    console.log(fieldItem.value);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem2 = form.createField({ name: 'items.1' });
                    console.log(fieldItem2.value);

                    //这个field没有push进去,所以是undefined的
                    let fieldItem3 = form.createField({ name: 'items.2' });
                    console.log(fieldItem3.value);
                }}
            >
                ArrayField,通过官方ArrayField操作
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //createArrayField
                    let field = form2.createArrayField({ name: 'items' });

                    //可以push数据,但是触发不了onInput
                    field.value.push('fish2');
                    field.value.push('cat2');

                    //可以通过value获取到数组的值
                    console.log(field.value.length);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem = form2.createField({ name: 'items.0' });
                    console.log(fieldItem.value);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem2 = form2.createField({ name: 'items.1' });
                    console.log(fieldItem2.value);

                    //这个field没有push进去,所以是undefined的
                    let fieldItem3 = form2.createField({ name: 'items.2' });
                    console.log(fieldItem3.value);
                }}
            >
                ArrayField,通过Field的value来操作,触发不了onInput,因此无法实现校验等操作
            </button>

            <button
                onClick={async () => {
                    console.log('create Field');
                    //createArrayField
                    let field = form3.createField({ name: 'items', value: [] });

                    //可以push数据,但是触发不了onInput
                    field.value.push('fish3');
                    field.value.push('cat3');

                    //可以通过value获取到数组的值
                    console.log(field.value.length);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem = form3.createField({ name: 'items.0' });
                    console.log(fieldItem.value);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem2 = form3.createField({ name: 'items.1' });
                    console.log(fieldItem2.value);

                    //这个field没有push进去,所以是undefined的
                    let fieldItem3 = form3.createField({ name: 'items.2' });
                    console.log(fieldItem3.value);
                }}
            >
                用普通的Field来模拟Array,,触发不了onInput,因此无法实现校验等操作,可以获取到path
            </button>
        </div>
    );
};

注意点如下:

  • 使用createArrayField就可以创建一个数组类型的Field字段了
  • 使用createField,填写好路径,就能操控到数组下的子元素的Field了
  • 尽量避免直接对Field的value进行支持,因为这样会丢掉onInput的回调(没有validator和feedback的内置支持)
  • 使用普通的createField,也能创建数组类型的Field字段,不过也会失去onInput的回调支持
import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldInputValueChange('items', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createArrayField
                    let field = form.createArrayField({ name: 'items' });

                    //push操作会触发onInuput
                    field.push(undefined);
                    field.push(undefined);

                    //可以通过value获取到数组的值
                    console.log(field.value.length);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem = form.createField({
                        name: 'items.0',
                        value: 'hello',
                    });
                    console.log(fieldItem.value);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem2 = form.createField({
                        name: 'items.1',
                        value: 123,
                    });
                    console.log(fieldItem2.value);

                    //这个field没有push进去,所以是undefined的
                    let fieldItem3 = form.createField({
                        name: 'items.2',
                    });
                    console.log(fieldItem3.value);
                }}
            >
                ArrayField,可以通过push一个undefined来让后续来配置格式
            </button>
        </div>
    );
};

对于数组下元素的不确定类型,可以直接push一个undefined的值

7.2 ObjectField

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldInputValueChange('person', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createObjectField
                    let field = form.createObjectField({ name: 'person' });

                    //push操作会触发onInuput
                    field.addProperty('name', 'fish');
                    field.addProperty('age', 123);
                    field.addProperty('sex', 'male');

                    //通过path,我们可以获取到下一级的field
                    let fieldItem = form.createField({
                        name: 'person.name',
                    });
                    console.log(fieldItem.value);

                    //通过path,我们可以获取到下一级的field
                    let fieldItem2 = form.createField({
                        name: 'person.age',
                        value: 123,
                    });
                    console.log(fieldItem2.value);

                    //这个field没有addProperty进去,所以是undefined的
                    let fieldItem3 = form.createField({
                        name: 'person.qq',
                    });
                    console.log(fieldItem3.value);

                    //bashPath的用法,实际取的字段是person.sex
                    let fieldItem4 = form.createField({
                        name: 'sex',
                        basePath: 'person',
                    });
                    console.log(fieldItem4.value);
                }}
            >
                ObjectField
            </button>
        </div>
    );
};

ObjectField的使用与ArrayField相当类似了,也是用createObjectField创建Field,而后用addProperty来添加属性。注意,basePath的用法

7.3 VoidField

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects() {
                onFieldChange('layout', () => {
                    console.log('Field [items] onInput');
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    console.log('create Field');
                    //createVoidField
                    //void field是布局和装饰用的组件,没有value的,也没有onInput,也没有validator,其他的属性基本都有
                    let field = form.createVoidField({ name: 'layout' });

                    field.setComponentProps({ size: 10 });

                    //可以通过query的语法,来查询节点,这里返回了{size:10}的值
                    console.log(form.query('layout').take().componentProps);

                    console.log('void field son!');
                    let field2 = form.createField({ name: 'layout.name' });
                    field2.value = 20;
                    field2.setComponentProps({ width: 30 });

                    //VoidField的路径,在query的时候,可以不省略
                    console.log(
                        form.query('layout.name').take().componentProps,
                    );
                    let field3 = form.query('layout.name').take() as Field;
                    console.log(field3.value);

                    //VoidField的路径,在query的时候,也可以省略
                    console.log(form.query('name').take().componentProps);
                    let field4 = form.query('name').take() as Field;
                    console.log(field4.value);

                    //但是,直接取value的话,必须省略,这里的API设计有点奇怪
                    console.log(form.query('name').value());
                }}
            >
                VoidField
            </button>
        </div>
    );
};

VoidField是虚拟字段,它的作用就是为了布局与装饰用的,没有value,也没有onInput,也没有validator。注意的是,VoidField在query的时候是可以忽略的,这是为了嵌套多层VoidField的时候,依然不需要更改业务逻辑,这个设计很棒。

8 字段路径

代码在这里

对于单个组件需要操控多个字段,或者onFieldChange需要同时倾听多个字段的情况,core库提供了独特的path表达式支持,它以特殊的语法来对多个字段的指定支持。

8.1 字段匹配

8.1.1 同级字段

import { createForm, onFieldChange } from '@formily/core';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldChange('person.age', (field) => {
                    //注意有一个点号
                    console.log(field.query('.name').value());
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    let ageField = form.createField({
                        name: 'person.age',
                        value: 123,
                    });
                    let nameField = form.createField({
                        name: 'person.name',
                        value: 'fish',
                    });

                    ageField.value = 12;
                }}
            >
                选择同级的path
            </button>
        </div>
    );
};

一个点号就能获取同级下的其他字段

8.1.2 子级字段

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
} from '@formily/core';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldInputValueChange('person.fish', (field) => {
                    //注意,没有点号
                    console.log(field.path + '.name');
                    console.log(form.query(field.path + '.name').value());
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    let ageField = form.createObjectField({
                        name: 'person.fish',
                    });
                    ageField.addProperty('name', 'fish');
                    ageField.addProperty('age', 123);
                }}
            >
                选择子级的path
            </button>
        </div>
    );
};

子级字段,就要在本级后面,加上后续的路径字段了

8.1.3 父级字段

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldValueChange,
} from '@formily/core';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldValueChange('person.name', (field) => {
                    console.log(field.parent.path.toString());
                    //注意,两个点号
                    console.log(field.query('..').value());
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    let ageField = form.createObjectField({
                        name: 'person',
                    });
                    ageField.addProperty('name', 'fish');
                    ageField.addProperty('age', 123);

                    let field = form.createField({ name: 'person.name' });
                    field.setValue('cat');
                }}
            >
                选择父级的path
            </button>
        </div>
    );
};

直接用parent,也可以用两个点号

8.1.4 多key指定字段

import { createForm, onFieldChange } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                //同时匹配,person的age或者name字段
                onFieldChange('person.*(age,name)', (field) => {
                    //注意有一个点号
                    console.log('Field [' + field.path + '] change');
                    let field2 = field as Field;
                    console.log(field2.value);
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    let ageField = form.createField({
                        name: 'person.age',
                        value: 123,
                    });
                    let nameField = form.createField({
                        name: 'person.name',
                        value: 'fish',
                    });

                    ageField.value = 12;
                    nameField.value = 'cat';
                }}
            >
                选择多个key的path
            </button>
        </div>
    );
};

使用星号,加上括号下的多个字段名就可以了。注意,这个语法可以嵌套的

8.1.5 多index指定字段

import { createForm, onFieldChange } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                //同时匹配,person的age或者name字段
                //下标从0开始
                onFieldChange('persons.*[1:2]', (field) => {
                    //注意有一个点号
                    let field2 = field as Field;
                    console.log(
                        'Field [' + field.path + '] change ' + field2.value,
                    );
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    let personsField = form.createArrayField({
                        name: 'persons',
                    });
                    personsField.push('1');
                    personsField.push('2');
                    personsField.push('3');
                    personsField.push('4');

                    for (var i = 0; i != 5; i++) {
                        let person = form.createArrayField({
                            name: 'persons.' + i,
                        });
                        person.value = 'MM' + person.value;
                    }
                }}
            >
                选择一段range的path
            </button>
        </div>
    );
};

多index指定字段,与多key指定字段是类似的,只是将圆括号,换成方括号而已。

8.2 字段解构赋值

8.2.1 批量get与set

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldChange('name', (field) => {
                    let field2 = field as Field;
                    console.log(
                        'Field [' + field.path + '] change ' + field2.value,
                    );
                });

                onFieldChange('age', (field) => {
                    let field2 = field as Field;
                    console.log(
                        'Field [' + field.path + '] change ' + field2.value,
                    );
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    //解构赋值,将两个字段合并在一起操作
                    let nameField = form.createField({
                        name: 'name',
                    });
                    let ageField = form.createField({
                        name: 'age',
                    });

                    console.log('setValuesIn');
                    //同时赋值多个字段
                    form.setValuesIn('[name,age]', ['fish', 123]);
                    console.log(form.getValuesIn('[name,age]'));
                    console.log(form.getValuesIn('name'));
                    console.log(form.getValuesIn('age'));
                }}
            >
                解构赋值
            </button>
        </div>
    );
};

我们可以创建多个字段,然后在setValuesIn,或者getValuesIn里面,使用方括号的解构赋值与取值的语法,注意,不支持ES6解构的剩余参数的写法。

8.2.2 多字段组合Field

import {
    createForm,
    onFieldChange,
    onFieldInputValueChange,
    onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';

export default () => {
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldChange('name', (field) => {
                    let field2 = field as Field;
                    console.log(
                        'Field [' + field.path + '] change ' + field2.value,
                    );
                });

                onFieldChange('age', (field) => {
                    let field2 = field as Field;
                    console.log(
                        'Field [' + field.path + '] change ' + field2.value,
                    );
                });
            },
        });
    }, []);
    return (
        <div>
            <button
                onClick={() => {
                    //解构赋值,将两个字段合并在一起操作
                    let allField = form.createField({
                        name: '[name,age]',
                    });

                    console.log('value', allField.getState().value);

                    allField.onInput(['fish', 123]);

                    console.log(form.getValuesIn('[name,age]'));
                    console.log(form.getValuesIn('name'));
                    console.log(form.getValuesIn('age'));

                    allField.value = ['cat', 456];

                    console.log(form.getValuesIn('[name,age]'));
                    console.log(form.getValuesIn('name'));
                    console.log(form.getValuesIn('age'));
                }}
            >
                解构赋值
            </button>
        </div>
    );
};

我们创建field的时候也可以同时指定多个key,那么赋值的时候就能写入到多个key中。同时,也能用getValuesIn与setValuesIn来存取其中一个key的数据。

9 表单受控

代码在这里

在大部分的业务场景中,我们的数据都是存放在单个form里面的。但是,在某些场景里面,我们希望外部数据能同步到form数据里面,或者反过来,form数据同步到外部数据去。

9.1 表单受控

import {
    createForm,
    onFieldChange,
    onFieldMount,
    onFieldReact,
} from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { autorun, observable } from '@formily/reactive';
import { Field } from '@formily/core/esm/models';

//注意,这里的实验core库版本要在2.0.0-beta.79以后。在78版本以前的试过不行
export default () => {
    const obs = useMemo(() => {
        let result = observable({
            name: 'kk',
        });
        autorun(() => {
            console.log('autorun:', result.name);
        });
        return result;
    }, []);
    const form = useMemo(() => {
        return createForm({
            //将可观察数据,在createForm的时候,就注入value中,两者就能保持同步
            //这称为,响应式表单受控
            //可以做双向同步
            values: obs, //需要整个结构到复制过去
            effects: () => {},
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    let field = form.createField({ name: 'name' });

                    obs.name = '123';
                    await sleep(100);
                    //这里field value变为123
                    console.log('field value:', field.value);

                    field.value = '789';
                    await sleep(100);
                    //这里obs value也变为789
                    console.log('obs value:', obs.name);
                }}
            >
                createForm的双向整个表单同步
            </button>
        </div>
    );
};

表单受控的写法较为简单,就是将整个外部的obserable对象赋值到form的values就可以了。而且,这个写法性能超好,还是双向同步的。

9.2 字段受控

import {
    createForm,
    onFieldChange,
    onFieldMount,
    onFieldReact,
} from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { autorun, observable } from '@formily/reactive';
import { Field } from '@formily/core/esm/models';

export default () => {
    const obs = useMemo(() => {
        let result = observable({
            name: 'kk',
        });
        autorun(() => {
            console.log('autorun:', result.name);
        });
        return result;
    }, []);
    const form = useMemo(() => {
        return createForm({
            //将可观察数据,通过onFieldReact进行同步
            //这称为,响应式字段级受控
            //但是,这样只能做单向同步,从observable到form的字段,不能从form的字段同步到observable
            effects: () => {
                onFieldReact('age', (field) => {
                    let field2 = field as Field;
                    field2.value = obs.name;
                });
            },
        });
    }, []);

    return (
        <div>
            <button
                onClick={async () => {
                    let field = form.createField({ name: 'age' });

                    obs.name = '123';
                    await sleep(100);
                    //这里field value变为123
                    console.log('field value:', field.value);

                    field.value = '789';
                    await sleep(100);
                    //这里的obs value依然为旧值,123
                    console.log('obs value:', obs.name);
                }}
            >
                createForm的字段单向同步
            </button>
        </div>
    );
};

当我们仅仅是希望表单的部分字段受控于外部数据的时候,就要使用onFieldReact来做外部数据的同步,注意,这种方式只能实现单向同步。

10 集成core库到自定义UI

代码在这里

我们在第0章,我们介绍了如何仅使用reative库就能构造一个表单,但是效果不尽如意,那么我们就整合core库来进一步优化这个例子吧

10.1 Context

import React, {
    createContext,
    ReactElement,
    useContext,
    FunctionComponent,
    ReactNode,
    Component,
} from 'react';
import { observer } from '@formily/reactive-react';
import { Field, Form, IFieldFactoryProps } from '@formily/core';

//创建上下文,方便Field消费
const FormContext = createContext<Form>({} as Form);
//创建上下文,方便FormItem消费
const FieldContext = createContext<Field>({} as Field);

export { FormContext };
export { FieldContext };

//表单管理入口
type FormProviderProps = {
    form: Form;
    children: ReactNode;
};
export const FormProvider = (props: FormProviderProps) => {
    return (
        <FormContext.Provider value={props.form}>
            {props.children}
        </FormContext.Provider>
    );
};

//状态桥接器组件
export const MyField = observer(
    (props: IFieldFactoryProps<any, any, any, any>) => {
        console.log('Child Component Field: ' + props.name + ' Render');
        const form = useContext(FormContext);
        const field = form.createField(props);
        if (!field.visible) return null;
        //渲染字段,将字段状态与UI组件关联
        const component = React.createElement(
            (field.component[0] as unknown) as string,
            {
                ...field.componentProps,
                value: field.value,
                onChange: field.onInput,
            } as React.Attributes,
        );

        //渲染字段包装器
        const decorator = React.createElement(
            (field.decorator[0] as unknown) as string,
            field.decoratorProps,
            component,
        );

        return (
            <FieldContext.Provider value={field}>
                {decorator}
            </FieldContext.Provider>
        );
    },
);

Context代码直接引用core库的类型就可以了,更加简单

10.2 FormItem

import { observer } from '@formily/reactive-react';
import { FieldContext } from './Context';
import { useContext } from 'react';

// FormItem UI组件
export default observer(({ children }) => {
    const field = useContext(FieldContext);
    const decoratorProps = field.decoratorProps as any;
    let style = {};
    if (decoratorProps.style) {
        style = decoratorProps.style;
    }
    return (
        <div>
            <div style={{ height: 20, ...style }}>{field.title}:</div>
            {children}
            <div style={{ height: 20, fontSize: 12, color: 'red' }}>
                {field.errors.join(',')}
            </div>
        </div>
    );
});

FormItem基本没变

10.3 Input

import { Field } from '@formily/core';
import React from 'react';

// Input UI组件
export default (props: Field) => {
    return (
        <input
            {...props}
            value={props.value || ''}
            style={{
                border: '2px solid rgb(186 203 255)',
                borderRadius: 6,
                width: '100%',
                height: 28,
                padding: '0 5px',
            }}
        />
    );
};

Input自定义组件也基本没变

10.4 主代码

import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormProvider, MyField } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';
import { useMemo } from 'react';
import { createForm, Field, onFieldReact } from '@formily/core';

export default () => {
    console.log('Top Render');
    const form = useMemo(() => {
        return createForm({
            effects: () => {
                onFieldReact('nameLength', (field) => {
                    let field2 = field as Field;
                    field2.value = field2.query('.name').value()?.length;
                });
            },
        });
    }, []);
    return (
        <FormProvider form={form}>
            <button
                onClick={() => {
                    form.getFieldState('name', (state) => {
                        if (
                            state.componentProps?.placeholder?.indexOf(
                                '你是谁',
                            ) != -1
                        ) {
                            state.componentProps = { placeholder: '我是我' };
                        } else {
                            state.componentProps = { placeholder: '你是谁' };
                        }
                    });
                }}
            >
                切换name组件的componentProps[placeholder]
            </button>
            <MyField
                title="名称"
                name="name"
                required
                component={[Input, {}]}
                decorator={[FormItem]}
            />
            <MyField
                title="名称长度"
                name="nameLength"
                component={[Label]}
                decorator={[FormItem]}
            />
            <button
                onClick={() => {
                    form.getFieldState('age', (state) => {
                        let decoratorProps = state.decorator[1] as any;
                        decoratorProps.style.height += 5;
                    });
                }}
            >
                切换age组件的decoratorProps[style.height]
            </button>
            <MyField
                title="年龄"
                name="age"
                required
                component={[InputDigit, {}]}
                decorator={[FormItem, { style: { height: 30 } }]}
            />
            <button
                onClick={() => {
                    form.getFieldState('password', (state) => {
                        let components = state.component as any;
                        if (components[0] === Password) {
                            state.component = [Input];
                        } else {
                            state.component = [Password];
                        }
                    });
                }}
            >
                切换password组件的Component
            </button>
            <MyField
                title="密码"
                name="password"
                required
                component={[Password]}
                decorator={[FormItem, {}]}
            />
        </FormProvider>
    );
};

整合了core库的主代码,可读性好得多了。

  • 我们不再需要在头部声明表单的所有字段了,我们只需要在MyField组件里面声明我们用到的哪些属性就可以了。
  • 完整使用core库的其他特性,丰富的生命周期,校验器,依赖追踪与联动,复杂结构的字段类型,灵活的path语法。

11 总结

一个设计良好的领域组件,值得学习。而且,另一方面,它可以看成是对MVVM架构的一种模范使用。在已经存在一个类似Reactive或者MobX组件的情况,如何进一步创建一个特定领域的通用ViewModel组件。

  • 建立一个ViewModel,ViewModel里面包含了业务需要用到的所有渲染数据,例如表单中的component,decorator,validator,feedback等等。然后将这些数据包装起来放在一个observable,提供外部接口来访问这些数据,View层直接读取这个ViewModel的数据来渲染。(在core库中,这个大observable数据可以通过form.getFormGraph方法来获取)
  • ViewModel提供了额外的生命周期(onMount,onInit,onUnmount,onSubmit,onFieldChange,onFieldReact),虚拟属性访问(实际是get与set方法),属性设置与获取的方法(setValuesIn,getValuesIn),公开的可调用方法(onInput,onFocus,onBlur)。这些方法的底层其实都在操控底层的obserable对象,同时嵌入业务固定的业务数据流。例如,onInput的调用总是会触发validator的检查,submit的调用总是会触发validator的检查,对visible设置为none总是会对value设置为undefined,对required这个虚拟属性设置为true,就会对validator属性添加一个required的校验器对象,等等。

就这样,这个特定领域的ViewModel就实现了,它的作用是:

  • 对于底层的View,它需要实现的仅仅是对ViewModel属性的首次读取后渲染,以及当ViewModel属性倾听,当发生变化后重新触发View。另外,对用户输入的行为,调用固定的ViewModel方法,例如是onInput,onFoucs,onBlur。
  • 对于顶层的Model,它仅仅需要以属性的方法来配置ViewModel的行为。对于那些业务固定流的行为,ViewModel默认就已经帮Model实现了,不需要再做那些重复的逻辑。

这个core库的想法真的很棒!

相关文章