Formily的React库经验汇总

2021-07-21 fishedee 前端

0 概述

Formily的React库经验汇总,官方文档在这里

我们在Core库中看到了,Formily是如何实现一个ViewModel层的。我们也看到了我们使用自定义的UI如何接入的Core库,在接入的过程中,我们也发现了一点问题:

  • Field组件重复,每次自定义UI都需要用到
  • Context组件重复,总是需要建立Form与Field的Context,这点也麻烦
  • 自定义UI组件暂时无法实现嵌套的组件,例如一个ObjectField组件下面优雅地嵌入一个Field组件

Formily的React库的目标就是解决以上的这些问题,让我们自定义的UI组件能更快地接入Formily体系之中,它将自己定义为ViewModel层与View层之间的胶水层,它包括的内容有:

  • 胶水组件,它定义好了Field,ArrayField,ObjectField与VoidField,能快速地让我们嵌入自己的自定义UI组件。
  • Schema组件,Formily漂亮的地方在于,它更进一步地扩展胶水组件的含义。它允许开发者动态传入一个JSON Schema或者Markup Schema,然后由它来统一渲染组件。聪明的你已经知道,这其实是在为后续的低代码开发做了铺垫。而且,这种方法带来的代码可维护性与可读性也更好。

1 胶水组件

代码在这里

1.1 Field组件

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import { FormProvider, Field, FormConsumer } from '@formily/react';
import { Input } from 'antd';

const form = createForm({
    effects: () => {},
});

export default () => {
    return (
        <FormProvider form={form}>
            <Field
                name="input"
                component={[Input, { placeholder: 'Please Input' }]}
            />
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

Field组件就像我们原来的用法,在component属性写入组件,就能实现自定绑定组件了。

1.2 ObjectField组件

import { Field, ObjectField } from '@formily/core';
import { useField } from '@formily/react';
import React, { ReactNode, useContext } from 'react';

export default (props: { children: ReactNode }) => {
    const field = useField();
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <h2>{field.title}</h2>
            <div style={{ padding: '10px' }}>{props.children}</div>
        </div>
    );
};

我们先定义一个Card组件,在于ObjectField的容器。useField是react自带的属性,它会拉取当前父节点中最接近的Field属性(Core库里面的Field,不是React库的Field)。

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
    FormProvider,
    Field,
    FormConsumer,
    ObjectField,
    VoidField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, NumberPicker } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

export default () => {
    return (
        <FormProvider form={form}>
            <ObjectField name="person" title="个人信息" component={[Card, {}]}>
                <Field
                    name="name"
                    title="姓名"
                    required={true}
                    component={[Input, {}]}
                    decorator={[FormItem, {}]}
                />
                <Field
                    name="age"
                    title="年龄"
                    required={true}
                    component={[NumberPicker, {}]}
                    decorator={[FormItem, {}]}
                />
            </ObjectField>
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

然后我们建立一个ObjectField组件,compnent是Card就可以了。最后,在ObjectField组件里面嵌套Field组件就可以了。另外,Formily在ObjectField组件的实现中,它会自动将ObjectField组件嵌套的children组件传递给Card这个组件,所以,你看到了Card组件中用到了props.children这个变量。

{
    "person":{
        "name":"123",
        "age":12
    }
}

注意,最后生成表单的格式是以上的这种格式,数据嵌套在person的子属性里面。而且,我们的Field组件只需要写name,而不是person.name就可以了。Field组件自己会知道自己嵌套在什么容器组件的下面。

1.3 VoidField组件

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
    FormProvider,
    Field,
    FormConsumer,
    ObjectField,
    VoidField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

export default () => {
    return (
        <FormProvider form={form}>
            <VoidField
                name="layout"
                component={[FormLayout, { labelCol: 6, wrapperCol: 10 }]}
            >
                <ObjectField
                    name="person"
                    title="个人信息"
                    component={[Card, {}]}
                >
                    <Field
                        name="name"
                        title="姓名"
                        required={true}
                        component={[Input, {}]}
                        decorator={[FormItem, {}]}
                    />
                    <Field
                        name="age"
                        title="年龄"
                        required={true}
                        component={[NumberPicker, {}]}
                        decorator={[FormItem, {}]}
                    />
                </ObjectField>
            </VoidField>
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

VoidField组件的用法与ObjectField是类似的,只不过它是没有value的而已。我们这里在VoidField组件中使用了FormLayout作为组件。

1.4 ArrayField组件

ArrayField组件是最为复杂的组件,一方面,它想ObjectField一样,作为容器组件可以嵌套其他的基础组件。另外一方面,ArrayField都是像表格与TabPane的组件,除了展示底层的基础组件,还需要自己显示列头,列宽,添加按钮等的组件。因此,在Formily里面,ArrayField组件它不会自动解析children组件再交给ArrayField组件来渲染,它只会直接将整个schema交给ArrayField,由ArrayField来确定怎样渲染。

import { ArrayField, Field } from '@formily/core';
import { useField } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';

type PropsType = Field & {
    children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
    const field = useField<ArrayField>();
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <div style={{ padding: '10px' }}>
                {field.value?.map((item, index) => {
                    return (
                        <div key={index}>
                            <div>
                                {field.componentProps.childrenRender(index)}
                            </div>
                            <button
                                onClick={() => {
                                    field.remove(index);
                                }}
                            >
                                删除
                            </button>
                        </div>
                    );
                })}
            </div>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

我们先定义一个ArrayItems组件,它使用componentProps里面的childrenRender来渲染子组件。注意,childrenRender是一个Render Props,不是ReactNode。

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
    FormProvider,
    Field,
    FormConsumer,
    ObjectField,
    VoidField,
    ArrayField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';

const form = createForm({
    effects: () => {},
});

export default () => {
    //使用ArrayField传递描述数组数据,注意不能用props.children来传递
    return (
        <FormProvider form={form}>
            <VoidField
                name="layout"
                component={[FormLayout, { labelCol: 6, wrapperCol: 10 }]}
            >
                <ObjectField
                    name="person"
                    title="个人信息"
                    component={[Card, {}]}
                >
                    <Field
                        name="name"
                        title="姓名"
                        required={true}
                        component={[Input, {}]}
                        decorator={[FormItem, {}]}
                    />
                    <Field
                        name="age"
                        title="年龄"
                        required={true}
                        component={[NumberPicker, {}]}
                        decorator={[FormItem, {}]}
                    />
                </ObjectField>
                <ArrayField
                    name="contact"
                    title="联系信息"
                    component={[
                        ArrayItems,
                        {
                            childrenRender: (index: number) => {
                                return (
                                    <ObjectField
                                        name={index + ''}
                                        title="信息"
                                        component={[Card, {}]}
                                    >
                                        <Field
                                            name="phone"
                                            title="电话"
                                            required={true}
                                            validator={{ format: 'phone' }}
                                            component={[Input, {}]}
                                            decorator={[FormItem, {}]}
                                        />
                                        <Field
                                            name="email"
                                            title="电子邮件"
                                            required={true}
                                            validator={{ format: 'email' }}
                                            component={[Input, {}]}
                                            decorator={[FormItem, {}]}
                                        />
                                    </ObjectField>
                                );
                            },
                        },
                    ]}
                ></ArrayField>
            </VoidField>
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

最后我们用ArrayField与ArrayItems来渲染了这个允许自增的表单列表组件。

2 Schema组件

schema组件是整个React库中最为漂亮的部分,在看示例代码的时候,不妨思考一下,Formily是怎样实现这个组件的。代码在这里。Formily为ant design包装的组件,全部都是使用Schema的方式包装,所以,这一部分是必须要被掌握的。

2.1 Json Schema

import { ArrayField, Field } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';

type PropsType = Field & {
    children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
    const field = useField<ArrayField>();
    const fieldSchema = useFieldSchema();
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <div style={{ padding: '10px' }}>
                {field.value?.map((item, index) => {
                    return (
                        <div key={index}>
                            <div>
                                <RecursionField
                                    name={index}
                                    schema={fieldSchema.items!}
                                />
                            </div>
                            <button
                                onClick={() => {
                                    field.remove(index);
                                }}
                            >
                                删除
                            </button>
                        </div>
                    );
                })}
            </div>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

首先,我们重写ArrayItems组件,这个时候,它不是通过componentProps来选择子组件,而是通过useFieldSchema来获取自身的Schema,然后交给RecursionField来渲染。(这里刚开始我也看不懂,直到后面的源代码才知道啥意思。)

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
    FormProvider,
    Field,
    FormConsumer,
    ObjectField,
    VoidField,
    ArrayField,
    createSchemaField,
    ISchema,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';

const form = createForm({
    effects: () => {},
});

//创建SchemaField的时候,就已经有options
const SchemaField = createSchemaField({
    components: {
        Input,
        NumberPicker,
        Card,
        FormLayout,
        FormItem,
        ArrayItems,
    },
});

const schema: ISchema = {
    type: 'void',
    'x-component': 'FormLayout',
    'x-component-props': { labelCol: 6, wrapperCol: 10 },
    properties: {
        person: {
            type: 'object',
            title: '个人信息',
            'x-component': 'Card',
            'x-decorator': 'FormItem',
            properties: {
                name: {
                    type: 'string',
                    title: '姓名',
                    required: true,
                    'x-component': 'Input',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                },
                age: {
                    type: 'number',
                    title: '年龄',
                    required: true,
                    'x-component': 'NumberPicker',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                },
            },
        },
        contact: {
            type: 'array',
            title: '联系信息',
            'x-component': 'ArrayItems',
            'x-decorator': 'FormItem',
            items: {
                type: 'object',
                title: '信息',
                'x-component': 'Card',
                properties: {
                    phone: {
                        type: 'string',
                        title: '电话',
                        format: 'phone',
                        required: true,
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                    },
                    email: {
                        type: 'string',
                        title: '电子邮件',
                        format: 'email',
                        required: true,
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                    },
                },
            },
        },
    },
};

export default () => {
    //使用schema
    return (
        <FormProvider form={form}>
            <SchemaField schema={schema} />
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

最后,我们用SchemaField组件与json schema就能渲染这个一样的页面。没有闭包,没有if语句,就是简单的json就能表达整个页面,注意这里与直接用ArrayField组件的不同。

另外,Object Schema的对象,总是含有properties属性。而Array Schema的对象,不仅含有properties属性(描述有哪些额外的按钮),还有items属性(描述数组中的每个元素应该怎么渲染)。

2.2 Markup Schema

import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
    FormProvider,
    Field,
    FormConsumer,
    ObjectField,
    VoidField,
    ArrayField,
    createSchemaField,
    ISchema,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';

const form = createForm({
    effects: () => {},
});

//创建SchemaField的时候,就已经有options
const SchemaField = createSchemaField({
    components: {
        Input,
        NumberPicker,
        Card,
        FormLayout,
        FormItem,
        ArrayItems,
    },
});

const schema: ISchema = {
    type: 'void',
    'x-component': 'FormLayout',
    'x-component-props': { labelCol: 6, wrapperCol: 10 },
    properties: {
        person: {
            type: 'object',
            title: '个人信息',
            'x-component': 'Card',
            'x-decorator': 'FormItem',
            properties: {
                name: {
                    type: 'string',
                    title: '姓名',
                    required: true,
                    'x-component': 'Input',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                },
                age: {
                    type: 'number',
                    title: '年龄',
                    required: true,
                    'x-component': 'NumberPicker',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                },
            },
        },
        contact: {
            type: 'array',
            title: '联系信息',
            'x-component': 'ArrayItems',
            'x-decorator': 'FormItem',
            items: {
                type: 'object',
                title: '信息',
                'x-component': 'Card',
                properties: {
                    phone: {
                        type: 'string',
                        title: '电话',
                        format: 'phone',
                        required: true,
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                    },
                    email: {
                        type: 'string',
                        title: '电子邮件',
                        format: 'email',
                        required: true,
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                    },
                },
            },
        },
    },
};

export default () => {
    //使用schema
    return (
        <FormProvider form={form}>
            <SchemaField>
                <SchemaField.Void
                    x-component="FormLayout"
                    x-component-props={{ labelCol: 6, wrapperCol: 10 }}
                >
                    <SchemaField.Object
                        title="个人信息"
                        name="person"
                        x-component={'Card'}
                        x-decorator={'FormItem'}
                    >
                        <SchemaField.String
                            title="姓名"
                            name="name"
                            required={true}
                            x-component={'Input'}
                            x-decorator={'FormItem'}
                        />
                        <SchemaField.Number
                            title="年龄"
                            name="age"
                            required={true}
                            x-component={'NumberPicker'}
                            x-decorator={'FormItem'}
                        />
                    </SchemaField.Object>
                    <SchemaField.Array
                        title="个人信息"
                        name="contact"
                        x-component={'ArrayItems'}
                        x-decorator={'FormItem'}
                    >
                        <SchemaField.Object
                            title="信息"
                            x-component={'Card'}
                            x-decorator={'FormItem'}
                        >
                            <SchemaField.String
                                title="电话"
                                name="phone"
                                required={true}
                                format="phone"
                                x-component={'Input'}
                                x-decorator={'FormItem'}
                            />
                            <SchemaField.Number
                                title="电子邮件2"
                                name="email"
                                required={true}
                                format="email"
                                x-component={'Input'}
                                x-decorator={'FormItem'}
                            />
                        </SchemaField.Object>
                    </SchemaField.Array>
                </SchemaField.Void>
            </SchemaField>
            <FormConsumer>
                {(form: Form) => {
                    return JSON.stringify(form.values) as ReactChild;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

JSON Schema的特点是,容易被后端服务器二次处理,也容易被其他编辑器自动生成,但是代码的可读性不好。因此,Formily提供了不需要动态化支持的Markup Schema,它的思路是,以ReactElement的语法,在SchemaField组件中写组件,这部分组件会先转换为json代码,然后再被SchemaField根据json渲染出来。这种Markup Schema的写法可读性更好,而且在typescript环境中有更好的提示。

3 胶水组件原理

我们来尝试一下,只有core库如何实现ObjectField与ArrayField组件。

3.1 ObjectField

代码在这里

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>
    );
};

let nameId = 0;

function randomName():string{
    let id = nameId++;
    return "random_"+id;
}

type FormConsumerProps = {
    children:(form:Form)=>ReactElement
}
export const FormConsumer = observer((props:FormConsumerProps)=>{
    const form = useContext(FormContext);
    return props.children(form);
})

//状态桥接器组件
const ReactiveField = 
    (props: {field:Field}&{otherProps?:object} ) => {
        let field = props.field;
        console.log('Child Component Field: ' + field.address + ' Render');
        if (!field.visible) return null;
        //渲染字段,将字段状态与UI组件关联
        //传入children
        const component = React.createElement(
            (field.component[0] as unknown) as string,
            {
                ...field.componentProps,
                ...props.otherProps,
            } as React.Attributes,
            props.children,
        );

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

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

export const MyObjectField = observer((props:IFieldFactoryProps<any, any, any, any>&{children?:ReactNode[]})=>{
    const form = useContext(FormContext);
    const parent = useContext(FieldContext);
    const name = props.name ? props.name:randomName();
    const field = form.createObjectField({
        ...props,
        name:name,
        basePath:parent?.address,
    });
    return <ReactiveField field={field}>{props.children}</ReactiveField>
})


export const MyField = observer((props:IFieldFactoryProps<any, any, any, any>)=>{
    const form = useContext(FormContext);
    const parent = useContext(FieldContext);
    const name = props.name ? props.name:randomName();
    const field = form.createField({
        ...props,
        name:name,
        basePath:parent?.address,
    });
    return <ReactiveField field={field} otherProps={{value:field.value,onChange:field.onInput}}/>
})

这里的代码与Core库的实现很相似,注意点如下:

  • MyObjectField使用createObjectField,而不是createField来创建Field,并且向component透传了children属性。这点实现了MyObjectField的children自动渲染。
  • MyField组件总是先用useContext(FieldContext)来获取上级的Field组件,然后在createField的时候,将上级的field的address填入basePath属性中。这点实现了Field在多层嵌套以后依然能知道自己在哪一级的Field下面。
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider, MyField, MyObjectField } 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';
import Card from './Card';

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}>
            <MyObjectField
                title="个人信息"
                name="person"
                component={[Card,{}]}
                decorator={[FormItem]}>
                <MyField
                    title="名称"
                    name="name"
                    required
                    component={[Input, {}]}
                    decorator={[FormItem]}
                />
                <MyField
                    title="年龄"
                    name="age"
                    required
                    component={[InputDigit, {}]}
                    decorator={[FormItem, { style: { height: 30 } }]}
                />
            </MyObjectField>
            <MyObjectField
                title="联系信息"
                name="contact"
                component={[Card,{}]}
                decorator={[FormItem]}>
                <MyField
                    title="电话"
                    name="phone"
                    validator={{
                        format:'phone'
                    }}
                    required
                    component={[Input, {}]}
                    decorator={[FormItem]}
                />
                 <MyField
                    title="邮件"
                    name="email"
                    validator={{
                        format:'email'
                    }}
                    required
                    component={[Input, {}]}
                    decorator={[FormItem]}
                />
            </MyObjectField>
            <FormConsumer>
                {(form)=>{
                    return (<div>{JSON.stringify(form.values)}</div>);
                }}
            </FormConsumer>
        </FormProvider>
    );
};

这是测试代码

3.2 ArrayField

代码在这里

export const MyArrayField = observer(
    (
        props: IFieldFactoryProps<any, any, any, any> & {
            children?: (index: number) => ReactNode;
        },
    ) => {
        const form = useContext(FormContext);
        const parent = useContext(FieldContext);
        const name = props.name;
        const field = form.createArrayField({
            ...props,
            name: name,
            basePath: parent?.address,
        });
        return (
            <ReactiveField
                field={field}
                otherProps={{
                    value: field.value,
                    onChange: field.onInput,
                }}
            >
                {props.children}
            </ReactiveField>
        );
    },
);

MyArrayField的实现与Formily的稍有不同,MyArrayField会透传children字段。MyArrayField的实现也简单,用createArrayField创建Field就可以了

import { ArrayField, Field } from '@formily/core';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import { FieldContext } from './Context';

// Input UI组件
type PropsType = Field & {
    children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
    const field = useContext(FieldContext) as ArrayField;
    console.log('render arrayitem ', field.value);
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <div style={{ padding: '10px' }}>
                {field.value.map((item, index) => {
                    console.log('render array ' + index);
                    return (
                        <div key={index}>
                            <div>{props.children(index)}</div>
                            <button
                                onClick={() => {
                                    field.remove(index);
                                }}
                            >
                                删除
                            </button>
                        </div>
                    );
                })}
            </div>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

注意,ArrayField里面的组件要用observer包围,否则array发生变动以后,容器组件不会自动渲染。这里,我们直接用props.children来渲染就可以了。

import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import {
    FormConsumer,
    FormProvider,
    MyArrayField,
    MyField,
    MyObjectField,
} 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';
import Card from './Card';
import ArrayItems from './ArrayItems';

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}>
            <MyObjectField
                title="个人信息"
                name="person"
                component={[Card, {}]}
                decorator={[FormItem]}
            >
                <MyField
                    title="名称"
                    name="name"
                    required
                    component={[Input, {}]}
                    decorator={[FormItem]}
                />
                <MyField
                    title="年龄"
                    name="age"
                    required
                    component={[InputDigit, {}]}
                    decorator={[FormItem, { style: { height: 30 } }]}
                />
            </MyObjectField>
            <MyArrayField
                title="联系信息"
                name="contact"
                component={[ArrayItems, {}]}
                decorator={[FormItem, { style: { height: 30 } }]}
            >
                {(index) => {
                    return (
                        <MyObjectField
                            title="信息"
                            name={index}
                            component={[Card, {}]}
                            decorator={[FormItem]}
                        >
                            <MyField
                                title="电话"
                                name="phone"
                                validator={{
                                    format: 'phone',
                                }}
                                required
                                component={[Input, {}]}
                                decorator={[FormItem]}
                            />
                            <MyField
                                title="邮件"
                                name="email"
                                validator={{
                                    format: 'email',
                                }}
                                required
                                component={[Input, {}]}
                                decorator={[FormItem]}
                            />
                        </MyObjectField>
                    );
                }}
            </MyArrayField>
            <FormConsumer>
                {(form) => {
                    return <div>{JSON.stringify(form.values)}</div>;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

这是测试代码,写法已经和Formily的很相似了

4 Schema组件原理

Schema组件实现原理中,最关键的一点是,使用RecursionField进行某个Schema子树的渲染,并且它也会不断递归自身,将自身Schema渲染完毕以后,计算得到子Schema,然后交给子的RecursionField来渲染。

4.1 JSON Schema

代码在这里

export type JsonSchema =
    | {
          type: 'object';
          title?: string;
          name?: string;
          required?: boolean;
          format?: string;
          properties: {
              [name in string]: JsonSchema;
          };
          'x-component': string;
          'x-component-props': any;
          'x-decorator': string;
          'x-decorator-props': any;
      }
    | {
          type: 'array';
          title?: string;
          name?: string;
          required?: boolean;
          format?: string;
          items: JsonSchema;
          properties: {
              [name in string]: JsonSchema;
          };
          'x-component': string;
          'x-component-props': any;
          'x-decorator': string;
          'x-decorator-props': any;
      }
    | {
          type: 'number';
          title?: string;
          name?: string;
          required?: boolean;
          format?: string;
          'x-component': string;
          'x-component-props': any;
          'x-decorator': string;
          'x-decorator-props': any;
      }
    | {
          type: 'string';
          title?: string;
          name?: string;
          required?: boolean;
          format?: string;
          'x-component': string;
          'x-component-props': any;
          'x-decorator': string;
          'x-decorator-props': any;
      };

首先,定义JSON Schema的格式

import { MyField, MyObjectField, MyArrayField } from './Context';
import { Fragment, ReactElement } from 'react';
import { useContext, ReactNode } from 'react';
import { createContext } from 'react';
import { JsonSchema } from './JsonSchema';

//创建上下文,方便Schema获取到Component字符串的实际指向
export type SchemaOptions = {
    [name in string]:
        | React.FunctionComponent<any>
        | React.Component<any, any, any>;
};

export const SchemaOptionsContext = createContext<SchemaOptions>(
    {} as SchemaOptions,
);

//创建上下文,方便RecusrionField获取到当前的子Schema
export const FieldSchemaContext = createContext<JsonSchema>({} as JsonSchema);

type RecursionFieldProps = {
    name: string;
    schema: JsonSchema;
    onlyRenderProperties: boolean;
};

export const RecursionField: React.FC<RecursionFieldProps> = (props) => {
    const fieldSchema = props.schema;
    //当fieldSchema的name为空的时候,使用props上面的name
    let name = fieldSchema.name ? fieldSchema.name : props.name;
    if (name === undefined) {
        name = '';
    }
    let validator: { format: string } | undefined = undefined;
    if (fieldSchema.format) {
        validator = { format: fieldSchema.format };
    }
    const options = useContext(SchemaOptionsContext);
    const renderProperties = (
        schemas: { [name in string]: JsonSchema },
    ): ReactElement => {
        let result = [];
        for (var key in schemas) {
            let subSchema = schemas[key];
            result.push(
                <RecursionField
                    key={key}
                    onlyRenderProperties={false}
                    schema={subSchema}
                    name=""
                />,
            );
        }
        return <Fragment>{result}</Fragment>;
    };

    const render = (): ReactNode => {
        if (fieldSchema.type == 'object') {
            if (props.onlyRenderProperties) {
                return renderProperties(fieldSchema.properties);
            }
            return (
                <MyObjectField
                    title={fieldSchema.title}
                    name={name}
                    required={fieldSchema.required}
                    validator={validator}
                    component={[
                        options[fieldSchema['x-component']],
                        fieldSchema['x-component-props'],
                    ]}
                    decorator={[
                        options[fieldSchema['x-decorator']],
                        fieldSchema['x-decorator-props'],
                    ]}
                >
                    {renderProperties(fieldSchema.properties)}
                </MyObjectField>
            );
        } else if (fieldSchema.type == 'array') {
            //array不渲染children,因为array的业务方案太多了
            if (props.onlyRenderProperties) {
                return renderProperties(fieldSchema.properties);
            }
            return (
                <MyArrayField
                    title={fieldSchema.title}
                    name={name}
                    required={fieldSchema.required}
                    validator={validator}
                    component={[
                        options[fieldSchema['x-component']],
                        fieldSchema['x-component-props'],
                    ]}
                    decorator={[
                        options[fieldSchema['x-decorator']],
                        fieldSchema['x-decorator-props'],
                    ]}
                />
            );
        } else if (
            fieldSchema.type == 'number' ||
            fieldSchema.type == 'string'
        ) {
            return (
                <MyField
                    title={fieldSchema.title}
                    name={name}
                    required={fieldSchema.required}
                    validator={validator}
                    component={[
                        options[fieldSchema['x-component']],
                        fieldSchema['x-component-props'],
                    ]}
                    decorator={[
                        options[fieldSchema['x-decorator']],
                        fieldSchema['x-decorator-props'],
                    ]}
                />
            );
        }
    };

    return (
        <FieldSchemaContext.Provider value={fieldSchema}>
            {render()}
        </FieldSchemaContext.Provider>
    );
};

type SchemaProps = {
    options: SchemaOptions;
    schema: JsonSchema;
};

export function Schema(props: SchemaProps) {
    return (
        <SchemaOptionsContext.Provider value={props.options}>
            <RecursionField
                onlyRenderProperties={true}
                schema={props.schema}
                name=""
            />
        </SchemaOptionsContext.Provider>
    );
}

然后,我们建立SchemaOptionsContext,传递component字符串映射。以及建立FieldSchemaContext,它负责传递下级的Schema。最后,Schema组件的渲染其实就是将schema交给了RecursionField来渲染,RecursionField主要做两件事:

  • 根据schema的当前节点,渲染出schema。
  • 只有VoidField与ObjectField,需要进一步渲染子schema,并填充到children字段里面。

注意,ArrayField组件,不会去渲染它的children字段。那么ArrayField是如何知道怎样渲染自身的元素呢?

import { ArrayField, Field } from '@formily/core';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import { FieldContext } from './Context';
import { FieldSchemaContext, RecursionField } from './Schema';

export default observer(() => {
    const fieldSchema = useContext(FieldSchemaContext);
    const field = useContext(FieldContext) as ArrayField;
    console.log('render arrayitem ', field.value);
    //在下面的RecursionField传入name,因为定义jsonSchema的时候,无法知道当前是在哪个index
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <div style={{ padding: '10px' }}>
                {field.value.map((item, index) => {
                    return (
                        <div key={index}>
                            <div>
                                <RecursionField
                                    onlyRenderProperties={false}
                                    schema={(fieldSchema as any).items}
                                    name={index + ''}
                                />
                            </div>
                            <button
                                onClick={() => {
                                    field.remove(index);
                                }}
                            >
                                删除
                            </button>
                        </div>
                    );
                })}
            </div>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

答案是使用useContext(FieldSchemaContext),拿出自己的schema,然后对着Schema的items字段,调用RecursionField来渲染。这为ArrayField的实现提供了最大的灵活度,你试试思考一下,如果RecursionField帮助了ArrayField渲染了children字段,那么ArrayField如何知道怎样渲染列头,列行等信息呢。

import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import Card from './Card';
import ArrayItems from './ArrayItemsSchema';
import { Schema, SchemaOptions } from './Schema';
import { JsonSchema } from './JsonSchema';

let options: SchemaOptions = {
    Input: Input,
    InputDigit: InputDigit,
    Password: Password,
    Label: Label,
    Card: Card,
    ArrayItems: ArrayItems,
    FormItem: FormItem,
};

let schema: JsonSchema = {
    type: 'object',
    'x-component': 'FormItem',
    'x-component-props': {},
    'x-decorator': 'FormItem',
    'x-decorator-props': {},
    properties: {
        person: {
            type: 'object',
            name: 'person',
            title: '个人信息',
            'x-component': 'Card',
            'x-component-props': {},
            'x-decorator': 'FormItem',
            'x-decorator-props': {},
            properties: {
                name: {
                    type: 'string',
                    name: 'name',
                    title: '名称',
                    required: true,
                    'x-component': 'Input',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                    'x-decorator-props': {},
                },
                age: {
                    type: 'number',
                    name: 'age',
                    title: '年龄',
                    required: true,
                    'x-component': 'InputDigit',
                    'x-component-props': {},
                    'x-decorator': 'FormItem',
                    'x-decorator-props': {},
                },
            },
        },
        contact: {
            type: 'array',
            name: 'contact',
            title: '联系信息',
            'x-component': 'ArrayItems',
            'x-component-props': {},
            'x-decorator': 'FormItem',
            'x-decorator-props': {},
            items: {
                //这里的name要保持为空,因为array下有多个行,每个行的index都是不同的,不能在定义schema的时候确定
                //这里只能由ArrayItems自身来确定下一级的name
                type: 'object',
                title: '信息',
                'x-component': 'Card',
                'x-component-props': {},
                'x-decorator': 'FormItem',
                'x-decorator-props': {},
                properties: {
                    name: {
                        type: 'string',
                        name: 'phone',
                        title: '电话',
                        required: true,
                        format: 'phone',
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                        'x-decorator-props': {},
                    },
                    age: {
                        type: 'string',
                        name: 'email',
                        title: '电子邮件',
                        required: true,
                        format: 'email',
                        'x-component': 'Input',
                        'x-component-props': {},
                        'x-decorator': 'FormItem',
                        'x-decorator-props': {},
                    },
                },
            },
            properties: {},
        },
    },
};

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

    return (
        <FormProvider form={form}>
            <Schema options={options} schema={schema} />
            <FormConsumer>
                {(form) => {
                    return <div>{JSON.stringify(form.values)}</div>;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

测试代码,与Formily的几乎是一样的了

4.2 Markup Schema

Markup Schema其实就是JSON Schema的另外一种写法而已。所以,在实现上,是先将Markup Schema转换为JSON Schema,最后再交给Schema来渲染。一个错误的思路是,将Markup Schema看成是React Node然后逐层递归来渲染。这个方法的错误在于,在ArrayField组件里面,你不能用相同引用的React Node来渲染多个行,这样会被React报错。你只能用相同的Schema,生成出不同引用但是相同内容的React Node来渲染多个行。

从另外一个角度看,RecursionField组件就是将Json转换为React Node的过程,那么Markup Schema组件就是反过来,它将React Node转换为Json,我们来看看是怎样做到的。

代码在这里

import { FunctionComponent, useContext } from 'react';
import { ReactNode } from 'react';
import { createContext } from 'react';
import { JsonSchema } from './JsonSchema';

type BasicJsxSchemaProps = {
    title?: string;
    name?: string;
    required?: boolean;
    format?: string;
    component: [string, any];
    decorator: [string, any];
};

type ObjectJsxSchemaProps = {
    children: ReactNode;
} & BasicJsxSchemaProps;

type ArrayJsxSchemaProps = {
    children: ReactNode;
} & BasicJsxSchemaProps;

export const JsxSchemaContext = createContext<JsonSchema>({} as JsonSchema);

export function JsxSchema() {
    const Common: FunctionComponent<
        BasicJsxSchemaProps & {
            type: 'string' | 'number' | 'object' | 'array';
        }
    > = (props) => {
        let parent = useContext(JsxSchemaContext);
        let data: JsonSchema = {
            type: props.type,
            title: props.title,
            name: props.name,
            required: props.required,
            format: props.format,
            'x-component': props.component[0],
            'x-component-props': props.component[1],
            'x-decorator': props.decorator[0],
            'x-decorator-props': props.decorator[1],
        };
        //添加上级的schema
        if (parent.type == 'array') {
            if (!parent.items) {
                //首次添加进入array
                parent.items = data;
            } else {
                //而后进入array
                if (!parent.properties) {
                    parent.properties = {};
                }
                parent.properties[data.name!] = data;
            }
        } else if (parent.type == 'object') {
            if (!parent.properties) {
                parent.properties = {};
            }
            parent.properties[data.name!] = data;
        } else {
            throw new Error('unknown component!');
        }

        //让子schema递归下去
        if (data.type == 'array' || data.type == 'object') {
            return (
                <JsxSchemaContext.Provider value={data}>
                    {props.children}
                </JsxSchemaContext.Provider>
            );
        } else {
            return null;
        }
    };
    const StringJsx = (props: BasicJsxSchemaProps) => {
        return <Common type="string" {...props} />;
    };
    const NumberJsx = (props: BasicJsxSchemaProps) => {
        return <Common type="number" {...props} />;
    };
    const ObjectJsx = (props: ObjectJsxSchemaProps) => {
        return <Common type="object" {...props} />;
    };
    const ArrayJsx = (props: ArrayJsxSchemaProps) => {
        return <Common type="array" {...props} />;
    };
    return {
        String: StringJsx,
        Number: NumberJsx,
        Object: ObjectJsx,
        Array: ArrayJsx,
    };
}

首先建立一个JsxSchemaContext,它获取的是当前正在生成的JSON Schema子树。然后在每层子树render的时候,把当前schema添加进去properties或者items上。注意,由于Markup Schema缺乏像JSON这样显式的指定properties与items属性,Markup Schema将所有的children组件全部放在xml的子树上。因此,我们约定,在Markup Schema中,首个Children组件放入items属性,之后的Children组件放入properties属性,Formily也是这样实现的。

export const Schema: FunctionComponent<SchemaProps> = (props) => {
    let schema: JsonSchema = props.schema
        ? props.schema
        : {
              type: 'object',
              'x-component': '',
              'x-component-props': {},
              'x-decorator': '',
              'x-decorator-props': {},
              properties: {},
          };
    return (
        <SchemaOptionsContext.Provider value={props.options}>
            <JsxSchemaContext.Provider value={schema}>
                {props.children}
            </JsxSchemaContext.Provider>
            <RecursionField
                onlyRenderProperties={true}
                schema={schema}
                name=""
            />
        </SchemaOptionsContext.Provider>
    );
};

最后,我们稍微更改一下Schema组件,添加JsxSchemaContext.Provider,并且将子树渲染出来。

import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import Card from './Card';
import ArrayItems from './ArrayItemsSchema';
import { Schema, SchemaOptions } from './Schema';
import { JsonSchema } from './JsonSchema';
import { JsxSchema, JsxSchemaContext } from './JsxSchema';

let options: SchemaOptions = {
    Input: Input,
    InputDigit: InputDigit,
    Password: Password,
    Label: Label,
    Card: Card,
    ArrayItems: ArrayItems,
    FormItem: FormItem,
};

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

    return (
        <FormProvider form={form}>
            <Schema options={options}>
                <MyJsxSchma.Object
                    name={'person'}
                    title={'个人信息'}
                    component={['Card', {}]}
                    decorator={['FormItem', {}]}
                >
                    <MyJsxSchma.String
                        name={'name'}
                        title={'名称'}
                        required={true}
                        component={['Input', {}]}
                        decorator={['FormItem', {}]}
                    />
                    <MyJsxSchma.Number
                        name={'age'}
                        title={'年龄'}
                        required={true}
                        component={['InputDigit', {}]}
                        decorator={['FormItem', {}]}
                    />
                </MyJsxSchma.Object>
                <MyJsxSchma.Array
                    name={'contact'}
                    title={'联系方式'}
                    component={['ArrayItems', {}]}
                    decorator={['FormItem', {}]}
                >
                    <MyJsxSchma.Object
                        title={'信息'}
                        component={['Card', {}]}
                        decorator={['FormItem', {}]}
                    >
                        <MyJsxSchma.String
                            name={'phone'}
                            title={'电话'}
                            required={true}
                            format={'phone'}
                            component={['Input', {}]}
                            decorator={['FormItem', {}]}
                        />
                        <MyJsxSchma.String
                            name={'email'}
                            title={'电子邮件'}
                            required={true}
                            format={'email'}
                            component={['Input', {}]}
                            decorator={['FormItem', {}]}
                        />
                    </MyJsxSchma.Object>
                </MyJsxSchma.Array>
            </Schema>
            <FormConsumer>
                {(form) => {
                    return <div>{JSON.stringify(form.values)}</div>;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

测试代码,这个时候与Formily的实现也几乎一致

5 Schema联动表达式

我们在Core库中描述过,在form写联动表达式是通过在effects里面调用onFieldReact,或者onFieldValueChange方法。但是,在JSON Schema中无法将所有联动表达式都转换为js代码放到JSON节点中,这样不方便在编辑器中写入联动表达式。

因此,在Schema中描述组件的联动逻辑,有另外的一套语法。原来在Form表单的effects属性中写入联动逻辑是可以的,这是原来的方法不方便在组件编辑器中进行读取而已。

代码在这里

5.1 主动与被动联动

5.1.1 主动联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="input"
                title="输入者"
                x-component="Input"
                x-decorator="FormItem"
                x-reactions={{
                    //主动受控,target加fulfill,只有当前value为123的时候才会展示input2
                    //注意,可以修改受控者的哪个value
                    target: 'input2',
                    fulfill: {
                        state: {
                            visible: "{{$self.value=='123'}}",
                        },
                    },
                }}
            />
            <SchemaField.String
                name="input2"
                title="受控者"
                x-component="Input"
                x-decorator="FormItem"
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

主动联动的写法,target加上fulfill,$self可以取出当前字段的值

5.1.2 被动联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="input"
                title="输入者"
                x-component="Input"
                x-decorator="FormItem"
            />
            <SchemaField.String
                name="input2"
                title="受控者"
                x-component="Input"
                x-decorator="FormItem"
                x-reactions={{
                    //被动受控,dependencies加fulfill
                    dependencies: ['input'],
                    fulfill: {
                        state: {
                            value: '{{$deps[0]}}',
                        },
                    },
                }}
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

被动联动,dependencies加上fulfill,\(deps可以取到对应的依赖项的数值,这种方式只能拿到\)deps的value值,不能拿其他值

5.2 scope联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
    scope: {
        asyncVisible(field) {
            field.loading = true;
            setTimeout(() => {
                field.loading = false;
                form.setFieldState('input', (state) => {
                    //对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
                    state.display = field.value;
                });
            }, 1000);
        },
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="select"
                title="控制者"
                default="visible"
                enum={[
                    { label: '显示', value: 'visible' },
                    { label: '隐藏', value: 'none' },
                    { label: '隐藏-保留值', value: 'hidden' },
                ]}
                x-component="Select"
                x-decorator="FormItem"
                x-reactions={{
                    //主动联动,但是当value发生变化的时候,触发asyncVisible方法
                    //注意asyncVisible需要先放在scope环境里面,触发方法用run
                    target: 'input',
                    effects: ['onFieldValueChange'],
                    fulfill: {
                        run: 'asyncVisible($self,$target)',
                    },
                }}
            />
            <SchemaField.String
                name="input"
                title="受控者"
                x-component="Input"
                x-decorator="FormItem"
                x-visible={false}
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values, null, 2)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

fulfill可以用run方法,这样就会调用对应scope上的方法了。触发方式上我们可以加上onFieldValueChange,或者onFieldInputValueChange

5.3 批量联动

5.3.1 批量主动联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="input"
                title="输入者"
                x-component="Input"
                x-decorator="FormItem"
                x-reactions={[
                    //同时操控多个受控者
                    {
                        //主动受控,target加fulfill,只有当前value为123的时候才会展示input2
                        target: 'input2',
                        fulfill: {
                            state: {
                                value:
                                    "{{'['+($self.value?$self.value:'')+']'}}",
                            },
                        },
                    },
                    {
                        //主动受控,target加fulfill,只有当前value为123的时候才会展示input2
                        target: 'input3',
                        fulfill: {
                            state: {
                                value:
                                    "{{'$'+($self.value?$self.value:'')+'$'}}",
                            },
                        },
                    },
                ]}
            />
            <SchemaField.String
                name="input2"
                title="受控者"
                x-component="Input"
                x-decorator="FormItem"
            />
            <SchemaField.String
                name="input3"
                title="受控者2"
                x-component="Input"
                x-decorator="FormItem"
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

x-reactions可以传入一个数组,这样就能实现一对多的主动联动

5.3.2 批量被动联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="price"
                title="单价"
                x-component="Input"
                x-decorator="FormItem"
            />
            <SchemaField.String
                name="count"
                title="数量"
                x-component="Input"
                x-decorator="FormItem"
            />
            <SchemaField.String
                name="total"
                title="总额"
                x-editable={false}
                x-component="Input"
                x-decorator="FormItem"
                x-reactions={{
                    //被动受控,dependencies可以是一个数组
                    //注意加入了when操作,当两者的乘积不是合法数值的时候,不进行更新,这个是可选操作
                    dependencies: ['price', 'count'],
                    when: '{{$deps[0] && $deps[1]}}',
                    fulfill: {
                        state: {
                            value: '{{$deps[0]*$deps[1]}}',
                        },
                    },
                }}
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

我们在依赖项的时候也可以传入一个数组,这样可以让一个字段同时受多个字段的被动联动。注意when的写法,主要可以指定特定情况下才去触发联动。

5.3.3 批量path联动

import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm();

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => (
    <Form form={form}>
        <SchemaField>
            <SchemaField.String
                name="input"
                title="输入者"
                x-component="Input"
                x-decorator="FormItem"
                x-reactions={
                    //同时操控多个受控者
                    {
                        //使用path的方式,同时指定多个受控者
                        //注意加入了onFieldInputValueChange的操作,仅当输入产生的value变化时才触发,开发者修改的value不触发
                        target: '*(input2,input3)',
                        effects: ['onFieldInputValueChange'],
                        fulfill: {
                            state: {
                                value:
                                    "{{'['+($self.value?$self.value:'')+']'}}",
                            },
                        },
                    }
                }
            />
            <SchemaField.String
                name="input2"
                title="受控者"
                x-component="Input"
                x-decorator="FormItem"
            />
            <SchemaField.String
                name="input3"
                title="受控者2"
                x-component="Input"
                x-decorator="FormItem"
            />
        </SchemaField>
        <FormConsumer>
            {() => (
                <code>
                    <pre>{JSON.stringify(form.values)}</pre>
                </code>
            )}
        </FormConsumer>
    </Form>
);

批量主动联动的另外一种写法是,使用path表达式的*号,可以同时指定多个target。

5.4 list条目联动

5.4.1 同级path联动

import React from 'react';
import {
    Form,
    FormItem,
    NumberPicker,
    ArrayTable,
    Editable,
    Input,
    FormButtonGroup,
    Submit,
} from '@formily/antd';
import { createForm } from '@formily/core';
import { createSchemaField } from '@formily/react';

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Editable,
        Input,
        NumberPicker,
        ArrayTable,
    },
});

const form = createForm();

export default () => {
    return (
        <Form form={form} layout="vertical">
            <SchemaField>
                <SchemaField.Array
                    name="projects"
                    title="Projects"
                    x-decorator="FormItem"
                    x-component="ArrayTable"
                >
                    <SchemaField.Object>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{
                                width: 50,
                                title: 'Sort',
                                align: 'center',
                            }}
                        >
                            <SchemaField.Void
                                x-decorator="FormItem"
                                x-component="ArrayTable.SortHandle"
                            />
                        </SchemaField.Void>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{
                                width: 80,
                                title: 'Index',
                                align: 'center',
                            }}
                        >
                            <SchemaField.String
                                x-decorator="FormItem"
                                x-component="ArrayTable.Index"
                            />
                        </SchemaField.Void>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{ title: 'Price' }}
                        >
                            <SchemaField.Number
                                name="price"
                                x-decorator="FormItem"
                                required
                                x-component="NumberPicker"
                                x-component-props={{}}
                                default={0}
                                x-reactions={{
                                    //FIXME,同级使用主动联动会失败
                                    //https://github.com/alibaba/formily/discussions/1874
                                    //主动联动,不支持相对路径,这是官方的说法
                                    target: '.count',
                                    fulfill: {
                                        state: {
                                            value: '{{$self.value}}',
                                        },
                                    },
                                }}
                            />
                        </SchemaField.Void>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{ title: 'Count' }}
                        >
                            <SchemaField.Number
                                name="count"
                                x-decorator="FormItem"
                                required
                                x-component="NumberPicker"
                                default={0}
                            />
                        </SchemaField.Void>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{ title: 'Total' }}
                        >
                            <SchemaField.Number
                                x-decorator="FormItem"
                                name="total"
                                x-component="NumberPicker"
                                x-pattern="readPretty"
                                x-component-props={{}}
                                x-reactions={{
                                    //拿出同级的price与count数据,取乘积
                                    dependencies: ['.price', '.count'],
                                    when: '{{$deps[0] && $deps[1]}}',
                                    fulfill: {
                                        state: {
                                            value: '{{$deps[0] * $deps[1]}}',
                                        },
                                    },
                                }}
                            />
                        </SchemaField.Void>
                        <SchemaField.Void
                            x-component="ArrayTable.Column"
                            x-component-props={{
                                title: 'Operations',
                                dataIndex: 'operations',
                                width: 200,
                                fixed: 'right',
                            }}
                        >
                            <SchemaField.Void x-component="FormItem">
                                <SchemaField.Void x-component="ArrayTable.Remove" />
                                <SchemaField.Void x-component="ArrayTable.MoveDown" />
                                <SchemaField.Void x-component="ArrayTable.MoveUp" />
                            </SchemaField.Void>
                        </SchemaField.Void>
                    </SchemaField.Object>
                    <SchemaField.Void
                        x-component="ArrayTable.Addition"
                        title="Add"
                    />
                </SchemaField.Array>
                <SchemaField.Number
                    name="total"
                    title="Total"
                    x-decorator="FormItem"
                    x-component="NumberPicker"
                    x-component-props={{
                        addonAfter: '$',
                    }}
                    x-pattern="readPretty"
                    x-reactions={{
                        //被动联动,拿出同级的.projects数据
                        dependencies: ['.projects'],
                        when: '{{$deps.length > 0}}',
                        fulfill: {
                            state: {
                                value:
                                    '{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}',
                            },
                        },
                    }}
                />
            </SchemaField>
            <FormButtonGroup>
                <Submit onSubmit={console.log}>提交</Submit>
            </FormButtonGroup>
        </Form>
    );
};

使用path表达式的.号语法,我们可以轻松指定同级的其他字段。但是,注意,在目前Formily的80版本下,只支持同级path表达式的被动联动,不支持同级path表达式的主动联动,这种情况下只能使用Form的effects来做。具体看这里

5.4.2 子级path联动

import React from 'react';
import {
    FormItem,
    Input,
    ArrayCards,
    FormButtonGroup,
    Submit,
} from '@formily/antd';
import { createForm, onFieldInputValueChange } from '@formily/core';
import { FormProvider, createSchemaField, FormConsumer } from '@formily/react';

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        ArrayCards,
    },
});

const form = createForm({
    effects: () => {
        /*
        //effect方式的主动联动
        onFieldInputValueChange('array.*.input', (field) => {
            field.query('..').take().setTitle(field.value);
        });
        */
    },
});

export default () => {
    return (
        <FormProvider form={form}>
            <SchemaField>
                <SchemaField.Array
                    name="array"
                    x-component="ArrayCards"
                    x-component-props={{}}
                >
                    <SchemaField.Object
                        //path表达式取出子级的数据
                        x-reactions={{
                            dependencies: ['.[].input'],
                            fulfill: {
                                state: {
                                    visible: "{{$deps[0]!='123'}}",
                                },
                            },
                        }}
                    >
                        <SchemaField.String
                            name="title"
                            x-decorator="FormItem"
                            title="标题"
                            x-component="Input"
                        />
                        <SchemaField.String
                            name="input"
                            x-decorator="FormItem"
                            title="输入框"
                            required
                            x-component="Input"
                        />
                        <SchemaField.Void x-component="ArrayCards.Remove" />
                        <SchemaField.Void x-component="ArrayCards.MoveUp" />
                        <SchemaField.Void x-component="ArrayCards.MoveDown" />
                    </SchemaField.Object>
                    <SchemaField.Void
                        x-component="ArrayCards.Addition"
                        x-reactions={{
                            //被动联动
                            dependencies: ['array'],
                            fulfill: {
                                state: {
                                    visible: '{{$deps[0].length<3}}',
                                },
                            },
                        }}
                        title="添加条目"
                    />
                </SchemaField.Array>
            </SchemaField>
            <FormButtonGroup>
                <Submit onSubmit={console.log}>提交</Submit>
            </FormButtonGroup>
            <FormConsumer>
                {(form) => {
                    return <div>{JSON.stringify(form.values)}</div>;
                }}
            </FormConsumer>
        </FormProvider>
    );
};

子级的联动,我们可以用.[].xx表达式,这种方式比较少用。

6 高级特性

有些时候我们需要自己定制的UI组件与Formily体系接入,这个时候我们就需要深入去理解Formily的内部的运行机制了

学完高级特性,我们尝试实现一个简陋版的ArrayTable

代码在这里

6.1 ObjectField的component属性为空

import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => {
    //ObjectField和VoidField的Component是可以为空的,不影响运行
    //https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/ReactiveField.tsx
    /*
    const renderComponent = () => {
        if (!field.component[0]) return <Fragment>{children}</Fragment>
        ....
    }
    * 当component为空的时候,直接返回一个children包围的fragment组件
    * VoidField与ObjectField的执行原理是类似的,只是FieldProvider提供的是VoidField,不是ObjectField,这样Void组件会导致values中没有void的名称字段。
    * ArrayField是没有children字段的,因为ArrayField接管了全部的渲染过程,Array组件没有Component是无法渲染任何组件出来的
     */
    return (
        <Form form={form}>
            <SchemaField>
                <SchemaField.Object name="object">
                    <SchemaField.String
                        name="input"
                        title="Object输入框"
                        x-component="Input"
                        x-decorator="FormItem"
                    />
                </SchemaField.Object>
                <SchemaField.Void name="void">
                    <SchemaField.String
                        name="input2"
                        title="Void输入框"
                        x-component="Input"
                        x-decorator="FormItem"
                    />
                </SchemaField.Void>
                <SchemaField.Array name="array">
                    <SchemaField.String
                        name="input3"
                        title="Array输入框"
                        x-component="Input"
                        x-decorator="FormItem"
                    />
                </SchemaField.Array>
            </SchemaField>
            <FormConsumer>
                {() => (
                    <code>
                        <pre>{JSON.stringify(form.values)}</pre>
                    </code>
                )}
            </FormConsumer>
        </Form>
    );
};

从实验中,我们得到:

  • 当ObjectField与VoidField的component为空的时候,Formily会用一个Fragment包围返回出去,渲染依然是正常的。
  • 当ArrayField的component为空的时候,Formily是无法渲染的,因为Formily是不会自动渲染ArrayField的Children,它直接将整个渲染过程丢给了ArrayField来做。

另外一点,即使ObjectField与VoidField的component为空,但是它依然能给下级组件提供了FieldProvider,产生字段树,这是这种做法最大的意义。

6.2 ObjectField的相同name

import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

const form2 = createForm({
    effects: () => {},
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => {
    //同一级的SchemaField不能有两个相同的name,否则因为properties相同而撞在一起了
    //可以用Void来包围一下放在下一级使用两个相同的name
    return (
        <div>
            <Form form={form}>
                <SchemaField>
                    <SchemaField.Object name="object">
                        <SchemaField.String
                            name="input"
                            title="Object输入框"
                            x-component="Input"
                            x-decorator="FormItem"
                        />
                    </SchemaField.Object>
                    <SchemaField.Object name="object">
                        <SchemaField.String
                            name="input2"
                            title="Object输入框"
                            x-component="Input"
                            x-decorator="FormItem"
                        />
                    </SchemaField.Object>
                </SchemaField>
                <FormConsumer>
                    {() => (
                        <code>
                            <pre>{JSON.stringify(form.values)}</pre>
                        </code>
                    )}
                </FormConsumer>
            </Form>
            <Form form={form2}>
                <SchemaField>
                    <SchemaField.Object name="object">
                        <SchemaField.String
                            name="input"
                            title="Object输入框"
                            x-component="Input"
                            x-decorator="FormItem"
                        />
                    </SchemaField.Object>
                    <SchemaField.Void name="void">
                        <SchemaField.Object name="object">
                            <SchemaField.String
                                name="input2"
                                title="Object输入框"
                                x-component="Input"
                                x-decorator="FormItem"
                            />
                        </SchemaField.Object>
                    </SchemaField.Void>
                </SchemaField>
                <FormConsumer>
                    {() => (
                        <code>
                            <pre>{JSON.stringify(form2.values)}</pre>
                        </code>
                    )}
                </FormConsumer>
            </Form>
        </div>
    );
};

在Markup Schema写代码的时候,如果同一级别的两个组件name是相同的,就是会产生异常。因为Markup Schema内部会转换为JSON Schema,两个组件的name是相同的,那么他们会合并在同一个properties的key上,导致前一个组件丢失了。当然,不同组件的name不同是没问题的。

6.3 Field的basePath属性

import { createForm } from '@formily/core';
import {
    createSchemaField,
    Field,
    FormConsumer,
    ObjectField,
} from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

const form2 = createForm({
    effects: () => {},
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

export default () => {
    //Field字段下面的BasePath可以改写Field的父级路径
    //https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/Field.tsx
    /*
    const form = useForm()
    const parent = useField()
    const field = useAttach(
        form.createField({ basePath: parent?.address, ...props })
    )
    //以上是Field的源代码,默认是取上级Field的address作为basePath,我们可以在props中传递basePath来改写这个属性
    //另外,我们也能看到,父级没有ObjectField组件,也能使用Form的createField来创建跨下级的Field。Formily会自动补充中间缺失的Field
    //注意,SchemaField上的x-basePath属性是没用的,不要使用。
     */
    return (
        <div>
            <Form form={form}>
                <Field
                    name="input"
                    basePath="mm"
                    title="Object输入框"
                    component={[Input, {}]}
                    decorator={[FormItem, {}]}
                />
                <Field
                    name="input"
                    basePath="kk"
                    title="Object输入框"
                    component={[Input, {}]}
                    decorator={[FormItem, {}]}
                />
                <FormConsumer>
                    {() => (
                        <code>
                            <pre>{JSON.stringify(form.values)}</pre>
                        </code>
                    )}
                </FormConsumer>
            </Form>
        </div>
    );
};

在Field组件的默认实现中,它会以父级Field的address作为basePath。我们可以通过改写它的basePath属性来覆盖这种默认设定。

6.4 RecursionField的name属性

import { createForm } from '@formily/core';
import {
    createSchemaField,
    Field,
    FormConsumer,
    ObjectField,
} from '@formily/react';
import ArrayList from './ArrayList';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {},
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
        ArrayList,
    },
});

export default () => {
    return (
        <div>
            <Form form={form}>
                <SchemaField>
                    <SchemaField.Array name="data" x-component="ArrayList">
                        <SchemaField.Void>
                            <SchemaField.String
                                name="input"
                                title="输入框A"
                                x-component="Input"
                                x-decorator="FormItem"
                            />
                            <SchemaField.String
                                name="input2"
                                title="输入框B"
                                x-component="Input"
                                x-decorator="FormItem"
                            />
                        </SchemaField.Void>
                    </SchemaField.Array>
                </SchemaField>
                <FormConsumer>
                    {() => (
                        <code>
                            <pre>{JSON.stringify(form.values)}</pre>
                        </code>
                    )}
                </FormConsumer>
            </Form>
        </div>
    );
};

我们先假设,我们设计一个ArrayList的组件,它的items组件下面,不是ObjectField,而是VoidField。那么每一个行之间的数据是如何知道它在哪个index的呢?例如,input字段,怎么知道自己是在data.0.input字段,还是在data.1.input字段呢?因为没有ObjectField,不能在input字段上建立一个父字段,来沿用Field的basePath的默认生成机制。

import { ArrayField, Field } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';

type PropsType = Field & {
    children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
    const field = useField<ArrayField>();
    const fieldSchema = useFieldSchema();
    //https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/RecursionField.tsx
    //https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/ReactiveField.tsx
    //RecursionField在渲染VoidField与ObjectField的时候,会将name字段传递给它们properties的Field的RecursionField的objectName+name。也各个Field的BasePath字段。
    //换句话说,RecursionField会自动覆盖本Field以及各个子Field下的basePath
    //RecursionField的name字段是必填的,basePath字段是选填的
    //而且,RecursionField自身还有basePath字段,它们的优先级是,优先使用RecursionField的basePath字段,否则就使用parentField.address字段。结果为XBasePath字段。
    //默认情况下,RecursionField渲染自身与子节点,这时候自身basePath被强行指定XBasePath,name字段就是为props.name字段,子节点通过ReactiveField来触发renderProperties,传入第一个参数是ObjectField或者VoidField本身,会自行继承父级的address作为basePath
    //当RecursionField使用onlyRenderProperties的时候,自身缺少节点,子节点被直接指定basePath为(XBasePath+name字段)。
    //无论如何,RecursionField总是保证以下:
    //* 自身为XBasePath+RecursionField的name字段
    //* 子节点为XBasePath+RecursionField的name字段+子节点自身的name字段
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <div style={{ padding: '10px' }}>
                {field.value?.map((item, index) => {
                    return (
                        <div key={index}>
                            <div>
                                <RecursionField
                                    name={index}
                                    schema={fieldSchema.items!}
                                />
                            </div>
                            <button
                                onClick={() => {
                                    field.remove(index);
                                }}
                            >
                                删除
                            </button>
                        </div>
                    );
                })}
            </div>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

RecursionField在设计的时候已经考虑了这种特殊情况,它的解决办法在于name字段。需要仔细看一下RecursionField的实现

const getBasePath = () => {
    if (props.onlyRenderProperties) {
      return props.basePath || parent?.address.concat(props.name)
    }
    return props.basePath || parent?.address
  }
  const basePath = getBasePath()
  const children =
    fieldSchema['x-content'] || fieldSchema['x-component-props']?.['children']
  const renderProperties = (field?: GeneralField) => {
    if (props.onlyRenderSelf) return
    return (
      <Fragment>
        {fieldSchema.mapProperties((item, name, index) => {
          const base = field?.address || basePath
          let schema: Schema = item
          return (
            <RecursionField
              schema={schema}
              key={`${index}-${name}`}
              name={name}
              basePath={base}
            />
          )
        })}
        {children}
      </Fragment>
    )
  }

  const render = () => {
    if (!isValid(props.name)) return renderProperties()
    if (fieldSchema.type === 'object') {
      if (props.onlyRenderProperties) return renderProperties()
      return (
        <ObjectField {...fieldProps} name={props.name} basePath={basePath}>
          {renderProperties}
        </ObjectField>
      )
    } ...

从实现中,我们可以看到:

  • RecursionField自身还有basePath字段,它们的优先级是,优先使用RecursionField的basePath字段,否则就使用parentField.address字段。结果为XBasePath字段。
  • 默认情况下,RecursionField渲染自身与子节点,这时候自身basePath被强行指定XBasePath,name字段就是为props.name字段,子节点通过ReactiveField来触发renderProperties,传入第一个参数是ObjectField或者VoidField本身,会自行继承父级的address作为basePath
  • 当RecursionField使用onlyRenderProperties的时候,自身缺少节点,子节点被直接指定basePath为(XBasePath+name字段)。

6.5 ArrayTable的列组件的占位实现

import { createForm, onFieldInputValueChange } from '@formily/core';
import {
    createSchemaField,
    Field,
    FormConsumer,
    ObjectField,
} from '@formily/react';
import MyTable from './MyTable';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {
        onFieldInputValueChange('data', (field) => {
            form.setFieldState('data.firstColumn', (state) => {
                let compontProps = state.componentProps;
                if (compontProps) {
                    console.log('change title');
                    compontProps.name = '名字:' + field.value.length + '行';
                }
            });
        });
    },
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
        MyTable,
    },
});

export default () => {
    //MyTable实现了自增,但是Column组件并没有支持effects
    return (
        <div>
            <Form form={form} feedbackLayout="terse">
                <SchemaField>
                    <SchemaField.Array name="data" x-component="MyTable">
                        <SchemaField.Void>
                            <SchemaField.Void
                                name="firstColumn"
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '名字',
                                    style: {
                                        width: '100px',
                                    },
                                }}
                            >
                                <SchemaField.String
                                    name="name"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>

                            <SchemaField.Void
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '年龄',
                                }}
                            >
                                <SchemaField.String
                                    name="age"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>
                        </SchemaField.Void>
                    </SchemaField.Array>
                </SchemaField>
                <FormConsumer>
                    {() => (
                        <code>
                            <pre>{JSON.stringify(form.values)}</pre>
                        </code>
                    )}
                </FormConsumer>
            </Form>
        </div>
    );
};

我们来模拟实现以下ArrayTable,初次实现中,我们看到ArrayTable的Schema中,每个字段都被一个Column包围住了。

但是,实际的渲染中,是以行为渲染的,每个单元格并不是都用Column来包围展示的。在这里,Column仅仅是作为一种Schema的占位符组件而已。

import { ArrayField, Field } from '@formily/core';
import {
    RecursionField,
    Schema,
    useField,
    useFieldSchema,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';

type Column = {
    style: object;
    name: string;
    schema: Schema;
};

function getColumn(schema: Schema): Column[] {
    //在当前实现中,Column层只是作为一种Schema来用,没有使用它的Field特性
    let itemsSchema: Schema['items'] = schema.items;
    const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
    const parseSource = (schema: Schema): Column[] => {
        let component = schema['x-component'];
        if (component?.indexOf('Column') != -1) {
            //获取该列的信息
            return [
                {
                    style: schema['x-component-props']?.style,
                    name: schema['x-component-props']?.title,
                    schema: schema,
                },
            ];
        }
        return [];
    };
    const reduceProperties = (schema: Schema): Column[] => {
        //对于items里面的每个schema,遍历它的Properties
        if (schema.properties) {
            return schema.reduceProperties((current, schema) => {
                return current.concat(parseSource(schema));
            }, [] as Column[]);
        } else {
            return [];
        }
    };
    return items.reduce((current, schema) => {
        //遍历每个items里面的schema
        if (schema) {
            return current.concat(reduceProperties(schema));
        }
        return current;
    }, [] as Column[]);
}
type PropsType = Field & {
    children: (index: number) => ReactElement;
};

type MyTableType = React.FC<PropsType> & {
    Column?: React.FC<any>;
};

const MyTable: MyTableType = observer((props: PropsType) => {
    const field = useField<ArrayField>();
    const fieldSchema = useFieldSchema();
    const tableColumns = getColumn(fieldSchema);
    const renderHeader = () => {
        let row = tableColumns.map((column) => {
            return (
                <td
                    style={column.style}
                    className={TableStyle.td}
                    key={column.name}
                >
                    {column.name}
                </td>
            );
        });
        return <tr>{row}</tr>;
    };
    const renderRow = (field: any, index: number) => {
        //注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
        //但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
        let row = tableColumns.map((column) => {
            return (
                <td className={TableStyle.td} key={column.name}>
                    {
                        <RecursionField
                            name={index}
                            schema={column.schema}
                            onlyRenderProperties
                        />
                    }
                </td>
            );
        });
        return (
            <tr className={TableStyle.tr} key={index}>
                {row}
            </tr>
        );
    };
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <table className={TableStyle.table}>
                <thead>{renderHeader()}</thead>
                <tbody>
                    {field.value?.map((row, index) => {
                        return renderRow(row, index);
                    })}
                </tbody>
            </table>
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

MyTable.Column = () => {
    return <Fragment></Fragment>;
};

export default MyTable;

因此,我们实现中,先分析Schema所有的Column描述,并保存下来对应的Schema。

  • 对于每个Column组件,放到header来渲染
  • 对于每个单元格,我们用RecursionField的onlyRenderProperties来渲染,避免渲染Column组件本身。另外,我们设置RecursionField的name为index。

6.6 ArrayTable的列组件的联动实现

import { createForm, onFieldInputValueChange } from '@formily/core';
import {
    createSchemaField,
    Field,
    FormConsumer,
    ObjectField,
} from '@formily/react';
import MyTable from './MyTable2';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {
        onFieldInputValueChange('data', (field) => {
            form.setFieldState('data.firstColumn', (state) => {
                let compontProps = state.componentProps;
                if (compontProps) {
                    console.log('change title');
                    compontProps.title = '名字:' + field.value.length + '行';
                }
            });
        });
    },
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
        MyTable,
    },
});

export default () => {
    //MyTable实现了自增,Column支持effects了,但没有自增列
    return (
        <div>
            <Form form={form} feedbackLayout="terse">
                <SchemaField>
                    <SchemaField.Array name="data" x-component="MyTable">
                        <SchemaField.Void>
                            <SchemaField.Void
                                name="firstColumn"
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '名字',
                                    style: {
                                        width: '100px',
                                    },
                                }}
                            >
                                <SchemaField.String
                                    name="name"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>

                            <SchemaField.Void
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '年龄',
                                }}
                            >
                                <SchemaField.String
                                    name="age"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>
                        </SchemaField.Void>
                    </SchemaField.Array>
                </SchemaField>
                <FormConsumer>
                    {() => <div>{JSON.stringify(form.values)}</div>}
                </FormConsumer>
            </Form>
        </div>
    );
};

在6.5列实现的Table的一个问题是,Column组件仅仅作为Schema描述,而不是一个实际的VoidField。当我们在effects中动态修改Column组件的component-props的时候,UI没有反应。

import { ArrayField, Field } from '@formily/core';
import {
    RecursionField,
    Schema,
    useField,
    useFieldSchema,
    useForm,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';

type Column = {
    style: object;
    title: string;
    name: string;
    schema: Schema;
};

function getColumn(schema: Schema): Column[] {
    //在当前实现中,Column层看成是Field
    let itemsSchema: Schema['items'] = schema.items;
    const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
    //获取当前array的field
    let form = useForm();
    let field = useField();
    const parseSource = (schema: Schema): Column[] => {
        //在渲染的时候,手动拿出每个Column的Field,并且将Schema作为保底逻辑
        //这里的写法,其实是先取field数据,再去createField
        //当第一次render的时候,Field不存在时,返回值为undefined
        let columnField = form.query(field.address + '.' + schema.name).take();
        console.log('field:', columnField);
        let component = schema['x-component'];
        if (component?.indexOf('Column') != -1) {
            //获取该列的信息
            return [
                {
                    name: schema.name + '',
                    style:
                        columnField?.componentProps?.stype ||
                        schema['x-component-props']?.style,
                    title:
                        columnField?.componentProps?.title ||
                        schema['x-component-props']?.title,
                    schema: schema,
                },
            ];
        }
        return [];
    };
    const reduceProperties = (schema: Schema): Column[] => {
        //对于items里面的每个schema,遍历它的Properties
        if (schema.properties) {
            return schema.reduceProperties((current, schema) => {
                return current.concat(parseSource(schema));
            }, [] as Column[]);
        } else {
            return [];
        }
    };
    return items.reduce((current, schema) => {
        //遍历每个items里面的schema
        if (schema) {
            return current.concat(reduceProperties(schema));
        }
        return current;
    }, [] as Column[]);
}
type PropsType = Field & {
    children: (index: number) => ReactElement;
};

type MyTableType = React.FC<PropsType> & {
    Column?: React.FC<any>;
};

const MyTable: MyTableType = observer((props: PropsType) => {
    const field = useField<ArrayField>();
    const fieldSchema = useFieldSchema();
    const tableColumns = getColumn(fieldSchema);
    console.log('Render Column', tableColumns);
    const renderHeader = () => {
        let row = tableColumns.map((column) => {
            return (
                <td
                    style={column.style}
                    className={TableStyle.td}
                    key={column.name}
                >
                    {column.title}
                </td>
            );
        });
        return <tr>{row}</tr>;
    };
    const renderRow = (field: any, index: number) => {
        //注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
        //但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
        let row = tableColumns.map((column) => {
            return (
                <td className={TableStyle.td} key={column.name}>
                    {
                        <RecursionField
                            name={index}
                            schema={column.schema}
                            onlyRenderProperties
                        />
                    }
                </td>
            );
        });
        return (
            <tr className={TableStyle.tr} key={index}>
                {row}
            </tr>
        );
    };
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <table className={TableStyle.table}>
                <thead>{renderHeader()}</thead>
                <tbody>
                    {field.value?.map((row, index) => {
                        return renderRow(row, index);
                    })}
                </tbody>
            </table>
            {tableColumns.map((column) => {
                //这里实际渲染每个Column,以保证Column能接收到Reaction
                //注意要使用onlyRenderSelf
                return (
                    <RecursionField
                        key={column.name}
                        name={column.name}
                        schema={column.schema}
                        onlyRenderSelf
                    />
                );
            })}
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

MyTable.Column = () => {
    return <Fragment></Fragment>;
};

export default MyTable;

解决方法就是,将读取Column的Schema的时候,不仅要考虑Schema本身,还要将Column的这个Field读取出来分析。而且,在Table的底部,我们需要用RecursionField将Column组件实际渲染出来,这是为了利用RecursionField调用它的createVoidField来创建Void字段。

这个时候,Column组件的title会自动随着Table的行数来自动变化了

6.7 ArrayTable的序号与删除组件的实现

import { createForm, onFieldInputValueChange } from '@formily/core';
import {
    createSchemaField,
    Field,
    FormConsumer,
    ObjectField,
} from '@formily/react';
import MyTable from './MyTable3';
import { Form, FormItem, Input, Select } from '@formily/antd';

const form = createForm({
    effects: () => {
        onFieldInputValueChange('data', (field) => {
            form.setFieldState('data.firstColumn', (state) => {
                let compontProps = state.componentProps;
                if (compontProps) {
                    console.log('change title');
                    compontProps.title = '名字:' + field.value.length + '行';
                }
            });
        });
    },
});

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
        MyTable,
    },
});

export default () => {
    //MyTable实现了自增,Column支持effects了,但没有自增列
    return (
        <div>
            <Form form={form} feedbackLayout="terse">
                <SchemaField>
                    <SchemaField.Array name="data" x-component="MyTable">
                        <SchemaField.Void>
                            <SchemaField.Void
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '序号',
                                }}
                            >
                                <SchemaField.Void x-component="MyTable.Index" />
                            </SchemaField.Void>
                            <SchemaField.Void
                                name="firstColumn"
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '名字',
                                    style: {
                                        width: '100px',
                                    },
                                }}
                            >
                                <SchemaField.String
                                    name="name"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>

                            <SchemaField.Void
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '年龄',
                                }}
                            >
                                <SchemaField.String
                                    name="age"
                                    x-component="Input"
                                    x-decorator="FormItem"
                                />
                            </SchemaField.Void>
                            <SchemaField.Void
                                x-component="MyTable.Column"
                                x-component-props={{
                                    title: '操作',
                                }}
                            >
                                <SchemaField.Void x-component="MyTable.Remove" />
                            </SchemaField.Void>
                        </SchemaField.Void>
                    </SchemaField.Array>
                </SchemaField>
                <FormConsumer>
                    {() => <div>{JSON.stringify(form.values)}</div>}
                </FormConsumer>
            </Form>
        </div>
    );
};

最后,我们来模拟实现一下ArrayTable的Index组件与Remove组件。

Index组件与Remove组件的特点是,它们不是一个字段组件,但是又既可以操作父级字段,以及知道自己在哪一个行。

import { ArrayField, Field } from '@formily/core';
import {
    RecursionField,
    Schema,
    useField,
    useFieldSchema,
    useForm,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { createContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';

type Column = {
    style: object;
    title: string;
    name: string;
    schema: Schema;
};

const ArrayContext = createContext({} as ArrayField);
const ArrayIndexContext = createContext(0);

function getColumn(schema: Schema): Column[] {
    //在当前实现中,Column层看成是Field
    let itemsSchema: Schema['items'] = schema.items;
    const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
    //获取当前array的field
    let form = useForm();
    let field = useField();
    const parseSource = (schema: Schema): Column[] => {
        //在渲染的时候,手动拿出每个Column的Field,并且将Schema作为保底逻辑
        //这里的写法,其实是先取field数据,再去createField
        //当第一次render的时候,Field不存在时,返回值为undefined
        let columnField = form.query(field.address + '.' + schema.name).take();
        console.log('field:', columnField);
        let component = schema['x-component'];
        if (component?.indexOf('Column') != -1) {
            //获取该列的信息
            return [
                {
                    name: schema.name + '',
                    style:
                        columnField?.componentProps?.stype ||
                        schema['x-component-props']?.style,
                    title:
                        columnField?.componentProps?.title ||
                        schema['x-component-props']?.title,
                    schema: schema,
                },
            ];
        }
        return [];
    };
    const reduceProperties = (schema: Schema): Column[] => {
        //对于items里面的每个schema,遍历它的Properties
        if (schema.properties) {
            return schema.reduceProperties((current, schema) => {
                return current.concat(parseSource(schema));
            }, [] as Column[]);
        } else {
            return [];
        }
    };
    return items.reduce((current, schema) => {
        //遍历每个items里面的schema
        if (schema) {
            return current.concat(reduceProperties(schema));
        }
        return current;
    }, [] as Column[]);
}
type PropsType = Field & {
    children: (index: number) => ReactElement;
};

type MyTableType = React.FC<PropsType> & {
    Column?: React.FC<any>;
    Index?: React.FC<any>;
    Remove?: React.FC<any>;
};

const MyTable: MyTableType = observer((props: PropsType) => {
    const field = useField<ArrayField>();
    const fieldSchema = useFieldSchema();
    const tableColumns = getColumn(fieldSchema);
    const renderHeader = () => {
        let row = tableColumns.map((column) => {
            return (
                <td
                    style={column.style}
                    className={TableStyle.td}
                    key={column.name}
                >
                    {column.title}
                </td>
            );
        });
        return <tr>{row}</tr>;
    };
    const renderRow = (field: any, index: number) => {
        //注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
        //但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
        let row = tableColumns.map((column) => {
            return (
                <td className={TableStyle.td} key={column.name}>
                    {
                        <RecursionField
                            name={index}
                            schema={column.schema}
                            onlyRenderProperties
                        />
                    }
                </td>
            );
        });
        //在这里注入index
        return (
            <ArrayIndexContext.Provider value={index}>
                <tr className={TableStyle.tr} key={index}>
                    {row}
                </tr>
            </ArrayIndexContext.Provider>
        );
    };
    return (
        <div
            style={{
                border: '2px solid rgb(186 203 255)',
            }}
        >
            <ArrayContext.Provider value={field}>
                <table className={TableStyle.table}>
                    <thead>{renderHeader()}</thead>
                    <tbody>
                        {field.value?.map((row, index) => {
                            return renderRow(row, index);
                        })}
                    </tbody>
                </table>
            </ArrayContext.Provider>
            {tableColumns.map((column) => {
                //这里实际渲染每个Column,以保证Column能接收到Reaction
                //注意要使用onlyRenderSelf
                return (
                    <RecursionField
                        key={column.name}
                        name={column.name}
                        schema={column.schema}
                        onlyRenderSelf
                    />
                );
            })}
            <button
                onClick={() => {
                    field.push({});
                }}
            >
                添加一行
            </button>
        </div>
    );
});

MyTable.Column = () => {
    return <Fragment></Fragment>;
};

MyTable.Index = () => {
    const indexContext = useContext(ArrayIndexContext);
    return <span>{indexContext + 1}</span>;
};

MyTable.Remove = () => {
    const arrayContext = useContext(ArrayContext);
    const indexContext = useContext(ArrayIndexContext);
    return (
        <a
            onClick={() => {
                arrayContext.remove(indexContext);
            }}
        >
            {'删除'}
        </a>
    );
};

export default MyTable;

解决办法就是,创建两个Context,ArrayContext来传递Array本身,ArrayIndexContext来传递哪一个行这个数据。

7 FAQ

7.1 value与onChange的隐式传递

具体看这里

Formily对于Object Field,Array Field,Void Field默认都会传递value与onChange方法。同时React默认会对onChange事件进行冒泡(这点真的恶心),所以,如果一个input组件发生了onChange操作,它上层的div组件也会收到onChange事件。同时,我们将该div组件放入Object Field的话就会出问题。

解决方法也很简单,按照语义的方式写代码,放入x-component的组件必须是输入组件,非输入组件就放入x-decorator里面就可以了

7.2 onClick方法指向scope

这点是很不推荐的,因为scope是放在SchemaField里面的,取不到组件的data数据,将onClick的方法指向到scope里面是不推荐的,还不如用onFieldReact来写。

7.3 effects里面的报错没有控制台输出

代码在这里

import {
    createForm,
    Field,
    onFieldChange,
    onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});
export default () => {
    const form = useMemo(() => {
        return createForm({
            values: {},
            effects: () => {
                onFieldInputValueChange('title', (field) => {
                    const field2 = field as Field;
                    console.log('title change to ',field2.value);
                    //这里故意写错,抛出了异常,但是控制台没有输出
                    field.doSomeErrorThing();
                    console.log('title change2',field2.value);
                });
            },
        });
    }, []);
    return (
        <Form form={form} feedbackLayout="terse">
            <SchemaField>
                <SchemaField.String
                    name="title"
                    x-component={'Input'}
                />
            </SchemaField>
            <FormConsumer>
                {() => <div>{JSON.stringify(form.values)}</div>}
            </FormConsumer>
        </Form>
    );
};

在onFieldInputValueChange或者onFieldValueChange里面的代码如果写错了,控制台是没有错误输出的,这点要注意一下,体验并不太好。

这个问题后来被修复了,看这里

7.4 dataSource等Field属性只能看成是首次赋值有效

代码看这里

import {
    createForm,
    Field,
    onFieldChange,
    onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { Button } from 'antd';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';
import { useState } from 'react';

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

type SelectType = {
    label:string;
    value:number;
}
export default () => {
    let [select,setSelect] = useState<SelectType[]>([{
        label:'啊',
        value:1,
    }]);
    const form = useMemo(() => {
        return createForm({
            values: {},
            effects: () => {
                onFieldInputValueChange('title', (field) => {
                    const field2 = field as Field;
                    console.log('title change to ',field2.value);
                });
            },
        });
    }, []);
    const toggleSelect = ()=>{
        if( select.length == 0 || select.length == 1 ){
            setSelect([{
                label:'你',
                value:2,
            },{
                label:'好',
                value:3,
            }]);
        }else{
            setSelect([{
                label:'啊',
                value:1,
            }]);
        }
    }
    console.log(' currentSelect ',select);
    return (
        <div>
            <Button onClick={toggleSelect}>切换Select</Button>
            <Form form={form} feedbackLayout="terse">
                <SchemaField>
                    <SchemaField.String
                        name="title"
                        enum={select}
                        x-decorator={'FormItem'}
                        x-component={'Select'}
                    />
                </SchemaField>
                <FormConsumer>
                    {() => <div>{JSON.stringify(form.values)}</div>}
                </FormConsumer>
            </Form>
        </div>
    );
};

在点击按钮以后,Field里面的dataSource属性是不会改动的。因为传入Field的属性仅仅是在该PATH下面的Field首次赋值的时候有效,第二次传入的时候,它会直接拿Form里面的state来操作。

这点跟React的UI = f(state)的方式是不太相同的,formily应该看成是类似Vue的框架,你要修改Form的state本身,而不是修改Field传入的属性来修改UI的。但是,formily的Field的存在与否依然是通过React的Field创建来确定的。

import {
    createForm,
    Field,
    onFieldChange,
    onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { Button } from 'antd';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';
import { useState } from 'react';
import { useRef } from 'react';
import {observer} from '@formily/reactive-react';

const SchemaField = createSchemaField({
    components: {
        FormItem,
        Input,
        Select,
    },
});

type SelectType = {
    label:string;
    value:number;
}
export default observer(() => {
    let ref = useRef<SelectType[]>([]);
    const form = useMemo(() => {
        return createForm({
            values: {},
            effects: () => {
                onFieldInputValueChange('title', (field) => {
                    const field2 = field as Field;
                    console.log('title change to ',field2.value);
                });
            },
        });
    }, []);
    const toggleSelect = ()=>{
        if( ref.current.length == 0 || ref.current.length == 1 ){
            ref.current = [{
                label:'你',
                value:2,
            },{
                label:'好',
                value:3,
            }];
        }else{
            ref.current =[{
                label:'啊',
                value:1,
            }];
        }
        form.setFieldState('title',(state)=>{
            state.dataSource = ref.current;
        })
    }
    console.log('value',form.values.title);
    return (
        <div>
            <Button onClick={toggleSelect}>切换Select</Button>
            <Form form={form} feedbackLayout="terse">
                <SchemaField>
                    <SchemaField.String
                        name="title"
                        x-decorator={'FormItem'}
                        x-component={'Select'}
                    />
                </SchemaField>
                <FormConsumer>
                    {() => <div>{JSON.stringify(form.values)}</div>}
                </FormConsumer>
            </Form>
        </div>
    );
});

要解决上面的问题,我们需要在点击按钮以后,显式地更改form的dataSource属性才能生效。

小结一下:

  • Field的动态数量显示,可以通过React的方式setState来修改。
  • Field的属性,dataSource,editable,visible,只能通过formily的方式,通过setFieldState来修改。

7.5 setFieldState与onValueChange不要用

import { createSchemaField, observer } from "@formily/react";
import { Button } from "antd";
import {
  Input,
  Select,
  FormItem,
  Submit,
  Form,
  Space,
  FormLayout,
  FormButtonGroup,
  Reset,
  NumberPicker,
  ArrayTable,
} from "@formily/antd";
import {
  Field,
  onFieldInputValueChange,
  onFieldChange,
  createForm,
} from "@formily/core";
import { useEffect, useMemo } from "react";

const SchemaField = createSchemaField({
  components: {
    Input,
    Select,
    FormItem,
    FormLayout,
    Space,
    Button,
    Submit,
    ArrayTable,
    NumberPicker,
  },
});

const UsetDetail: React.FC<any> = observer((props) => {
  const form = useMemo(() => {
    return createForm({
      values: {
        detail: {} as any,
      },
      effects: () => {
        onFieldChange("detail.type", (f) => {
          //初次type启动时,以及ajax数据返回后type的变化
          const typeField = f as Field;
          const typeValue = typeField.value;
          refreshTableColumn(typeValue);
        });
      },
    });
  }, []);
  const refreshTableColumn = (typeValue: string) => {
    form.setFieldState("detail.selectItems", (state) => {
      if (typeValue == "COMBO") {
        state.visible = true;
      } else {
        state.visible = false;
      }
    });
  };
  useEffect(() => {
    setTimeout(() => {
      //模拟ajax请求
      form.values.detail = {
        type: "COMBO",
        selectItems: [
          {
            amount: "123",
          },
          {
            amount: "456",
          },
        ],
      };
    }, 2000);
  }, []);
  const subItemSchema = (
    <SchemaField.Array
      name="selectItems"
      title="子物料"
      x-component="ArrayTable"
      x-component-props={{
        bordered: true,
      }}
      x-decorator="FormItem"
    >
      <SchemaField.Void>
        <SchemaField.Void
          name="AmountColumn"
          title="默认配量"
          x-component="Table.Column"
        >
          <SchemaField.String
            name="amount"
            default={1}
            x-component="NumberPicker"
            x-decorator="FormItem"
            required={true}
          />
        </SchemaField.Void>
      </SchemaField.Void>
    </SchemaField.Array>
  );
  const formSchema = (
    <SchemaField>
      <SchemaField.Object name="detail">
        <SchemaField.String
          name="type"
          title="类型"
          enum={[
            { label: "普通物料", value: "NORMAL" },
            { label: "属性物料", value: "PROPERTY" },
            { label: "选项物料", value: "SELECT" },
            { label: "组合物料", value: "COMBO" },
          ]}
          required={true}
          x-decorator="FormItem"
          x-component="Select"
          x-component-props={{}}
        />
        {subItemSchema}
      </SchemaField.Object>
    </SchemaField>
  );
  return (
    <Form form={form} feedbackLayout={"terse"}>
      {formSchema}
      <FormButtonGroup gutter={10}>
        <Submit onSubmit={() => {}}>提交</Submit>
        <Reset>重置</Reset>
      </FormButtonGroup>
    </Form>
  );
});

export default UsetDetail;

这里

在rc17版本下,以上的代码会出现问题,根本原因是,setFieldState会将写入请求进入堆栈,在field挂载以后,拉出来运行。这个实现对于Formily开发者以及Formily的使用者来说,都是不少的心智负担。因为,setFieldState的闭包什么时候触发是完全无概念的,它可能在ajax结果返回前,也可能在ajax结果返回后执行的。在加上,js是无栈异步模型,这种异步触发事件不确定的问题,出了问题超级难排查。

更好的方法,应该是直接用form.query查询字段模型,存在的时候就直接赋值,不存在的时候就不进行赋值。让赋值操作称为普通的同步操作,而不是带异步的操作。这样代码的执行时机更清晰,对开发者心智要求要低得多。

另外,不要使用onFieldChange和onFieldValueChange,因为它们的触发时机也是不确定的,它们会在开发者修改value,或者首次挂载组件,但其他组件仍没有挂载的时候触发。因为,onFieldValueChange会在其他组件没有挂载的时候会触发,所以Formily提供了setFieldState这样的API接口来弥补这个问题。

个人认为,更好的方法应该是:

  • Formily的开发者与使用者都不应该使用onFieldChange和onFieldValueChange方法来编写业务代码,仅仅在组件库代码中可以使用。在业务代码中,应该只使用onFieldInputValueChange来编写用户改变值后的触发操作。至于开发者代码变更值的操作,应该显式调用相关代码,而不是依赖onFieldValueChange来做隐式触发。
  • Formily的开发者与使用者都不应该使用setFieldState方法,而仅仅使用form.query,或者field.query的方法来同步修改组件状态。业务流程更加清晰,而且更少歧义。

在业务开发中,我们应该尽可能倾向于编写更难出错的,一眼就能看清的代码。而不是编写省事(行数更少,看起来优雅)但是难以理解的代码。

7.6 query.take()未创建组件的问题

import { createSchemaField, observer } from "@formily/react";
import { Button } from "antd";
import {
  Input,
  Select,
  FormItem,
  Submit,
  Form,
  Space,
  FormLayout,
  FormButtonGroup,
  Reset,
  NumberPicker,
  ArrayTable,
} from "@formily/antd";
import {
  Field,
  onFieldInputValueChange,
  onFieldChange,
  createForm,
} from "@formily/core";
import { useEffect, useMemo, useState } from "react";

const SchemaField = createSchemaField({
  components: {
    Input,
    Select,
    FormItem,
    FormLayout,
    Space,
    Button,
    Submit,
    ArrayTable,
    NumberPicker,
  },
});

const UsetDetail: React.FC<any> = observer((props) => {
  const [stateRefresh, setStateRefresh] = useState(1);

  const form = useMemo(() => {
    return createForm({
      values: {
        detail: {} as any,
      },
    });
  }, []);
  const clickMe = () => {
    const field1 = form.query("detail.items.0.amount").take();
    console.log("field1 ", field1);

    form.values.detail = {
      items: [
        {
          amount: "123",
        },
      ],
    };

    //只创建value的话,是拿不到这个Field的,只有这个Field被render出来才能拿到
    //const field2 = form.query("detail.items.0.amount").take();
    //console.log("field2 ", field2);

    //手动setState的方法并不能马上render页面
    setStateRefresh(stateRefresh + 1);
    console.log("go");

    //你需要用timeout来延迟设置,在render以后再触发后面的代码
    setTimeout(() => {
      const field2 = form.query("detail.items.0.amount").take();
      console.log("field2 ", field2);
    }, 100);

    //这个问题是Formily的弱点,因为Formily设置在field上的dataSource只能是首次有效
    //后续的都需要经过设置dataSource都需要经过formily的query再set的方法。
    //而formily的set方法又需要组件创建以后才能使用,组件的创建时需要等待React触发后创建的,这点Formily是无法控制的生命周期
    //这里只能寄望setTimeout之后,组件已经被React render出来了
    //form.setFieldState的方法能解决这个问题,有一定的其他使用风险。
  };
  console.log("pageRefresh");
  const subItemSchema = (
    <SchemaField.Array
      name="items"
      title="子物料"
      x-component="ArrayTable"
      x-component-props={{
        bordered: true,
      }}
      x-decorator="FormItem"
    >
      <SchemaField.Void>
        <SchemaField.Void
          name="AmountColumn"
          title="默认配量"
          x-component="ArrayTable.Column"
        >
          <SchemaField.String
            name="amount"
            default={1}
            x-component="NumberPicker"
            x-decorator="FormItem"
            required={true}
          />
        </SchemaField.Void>
      </SchemaField.Void>
    </SchemaField.Array>
  );
  const formSchema = (
    <SchemaField>
      <SchemaField.Object name="detail">{subItemSchema}</SchemaField.Object>
    </SchemaField>
  );
  return (
    <Form form={form} feedbackLayout={"terse"}>
      {formSchema}
      <Button onClick={clickMe}>{"点我"}</Button>
    </Form>
  );
});

export default UsetDetail;

这个问题的关键在于,Formily的属性是命令式的设置,但是Formily的组件是由React的声明式的创建造成。解决这个问题有两种方法:

  • 使用setTimeout,延迟命令式设置的时机。
  • 使用createField,而不是query.take,这样的话要注意将所有默认数据的配置好。

7.7 React方式组合Formily

代码看这里

<div>
    <Button onClick={toggleSelect}>切换Select</Button>
    <Button onClick={toggleTitle}>切换Title</Button>
    <Form form={form} feedbackLayout="terse">
    <SchemaField>
        <SchemaField.String
        title={argv.title}
        x-decorator={'FormItem'}
        x-component={'Input'}
        x-component-props={{
            placeholder: argv.placeholder,
        }}
        />
    </SchemaField>
    <FormConsumer>
        {() => <div>{JSON.stringify(form.values)}</div>}
    </FormConsumer>
    </Form>
</div>

无论是使用SchemaField

<div>
    <Button onClick={toggleSelect}>切换Select</Button>
    <Button onClick={toggleTitle}>切换Title</Button>
    <Form form={form} feedbackLayout="terse">
    <Field
        name="title"
        component={[Input, { placeholder: argv.placeholder }]}
        decorator={[FormItem, { title: '123' }]}
    />
    <FormConsumer>
        {() => <div>{JSON.stringify(form.values)}</div>}
    </FormConsumer>
    </Form>
</div>

还是使用Field字段,都无法直接修改本地数据,你总是需要使用form.query来获取field,然后修改它的data来触发变更,而不是直接修改赋值属性来修改它。

<div>
    <Button onClick={toggleSelect}>切换Select</Button>
    <Button onClick={toggleTitle}>切换Title</Button>
    <Form form={form} feedbackLayout="terse">
    <FormItem
        label={argv.title}
        asterisk={true}
        feedbackText={'错误'}
        feedbackStatus="error"
    >
        <Input placeholder={argv.placeholder} />
    </FormItem>
    <FormConsumer>
        {() => <div>{JSON.stringify(form.values)}</div>}
    </FormConsumer>
    </Form>
</div>

一个简单的方法就是仅仅使用Formily的antd与reactive组件,不使用core组件。这样就不再需要使用setFieldState,也不需要使用query方法。

8 总结

ArrayField组件,Schema的设计与实现,这两点都很漂亮,值得学习。

相关文章