0 概述
我们进入了Formily的下一步,core库,其实有了reactive组件以后,我们开发表单已经比原来要简直快捷多了,但是为什么还需要core库呢?
让我们先看一下,只用Formily的Reactive库的情况,开发一个表单会遇到什么问题。
0.1 为什么
代码在这里
0.1.1 目标
我们目标在只用Formily的Reactive库情况下,建立这个有4个表单项的表单。要求为:
表单有required的校验项,首次未输入的时候不触发,而后的每次input变动的时候,当value为空的时候弹出错误
可以通过按钮,来回切换“名字”项下的input的placeholder属性
可以通过按钮,来更改“年龄”项下的title与input组件之间的距离
可以通过按钮,来回切换“密码”项的Input或者Password组件
“名字长度”是不可修改属性项,它的值由“名字”项来自动计算出来的
最后,我们要求框架开发者足够的抽象性,让使用者轻松实现直观地实现以上功能
0.1.2 context
import React, {
createContext,
ReactElement,
useContext,
FunctionComponent,
ReactNode,
} from 'react';
import { observer } from '@formily/reactive-react';
export type ValidatorType = (data: any) => string;
export type FieldType = {
title: string;
value: any;
errors: string[];
visible: boolean;
validator: ValidatorType[];
component: (props: any) => JSX.Element;
componentProps: object;
decorator: React.FunctionComponent;
decoratorProps: object;
onInput: (data: any) => void;
};
export type FormType = {
[key in string]: FieldType;
};
export function validate(data: any, validator: ValidatorType[]): string[] {
let errors = [];
for (let i in validator) {
let singleValidator = validator[i];
let error = singleValidator(data);
if (error != '') {
errors.push(error);
}
}
return errors;
}
//创建上下文,方便Field消费
const FormContext = createContext<FormType>({});
//创建上下文,方便FormItem消费
const FieldContext = createContext<FieldType>({} as FieldType);
export { FormContext };
export { FieldContext };
//表单管理入口
type FormProviderProps = {
form: FormType;
children: ReactNode;
};
export const FormProvider = (props: FormProviderProps) => {
return (
<FormContext.Provider value={props.form}>
{props.children}
</FormContext.Provider>
);
};
//状态桥接器组件
type FieldWrapperType = {
name: string;
};
export const Field = observer((props: FieldWrapperType) => {
console.log('Child Component Field: ' + props.name + ' Render');
const form = useContext(FormContext);
const field = form[props.name];
if (!field.visible) return null;
//渲染字段,将字段状态与UI组件关联
const component = React.createElement(field.component, {
...field.componentProps,
value: field.value,
onChange: field.onInput,
} as React.Attributes);
//渲染字段包装器
const decorator = React.createElement(
field.decorator,
field.decoratorProps,
component,
);
return (
<FieldContext.Provider value={field}>{decorator}</FieldContext.Provider>
);
});
我们首先建立FormContext与FieldContext组件,这是借鉴了core库的实现,我们用了useContext,这能在不破坏实现的程度,轻松在多层组件中传递当前的field与form。同时,我们进一步创建了FormProvider与Field组件
0.1.3 FormItem
import { observer } from '@formily/reactive-react';
import { FieldContext } from './Context';
import { useContext } from 'react';
// FormItem UI组件
export default observer(({ children }) => {
const field = useContext(FieldContext);
const decoratorProps = field.decoratorProps as any;
let style = {};
if (decoratorProps.style) {
style = decoratorProps.style;
}
return (
<div>
<div style={{ height: 20, ...style }}>{field.title}:</div>
{children}
<div style={{ height: 20, fontSize: 12, color: 'red' }}>
{field.errors.join(',')}
</div>
</div>
);
});
我们创建了FormItem,作为Form表单项的包围组件,它用来显示标题,以及错误
0.1.4 RequireValidator
export default function () {
return function (data: any): string {
if (typeof data === 'string') {
return data == '' ? '缺少参数' : '';
} else if (typeof data === 'undefined') {
return '缺少参数';
} else {
return '';
}
};
}
这是必填项的校验器
0.1.5 可复用组件
import React from 'react'
import {FieldType} from './Context'
// Input UI组件
export default (props:FieldType) => {
return (
<input
{...props}
value={props.value || ''}
style={{
border: '2px solid rgb(186 203 255)',
borderRadius: 6,
width: '100%',
height: 28,
padding: '0 5px',
}}
/>
)
}
Input组件
import React from 'react'
import {FieldType} from './Context'
// Input UI组件
export default (props:FieldType) => {
return (
<input
type="number"
{...props}
value={props.value || ''}
style={{
border: '2px solid rgb(186 203 255)',
borderRadius: 6,
width: '100%',
height: 28,
padding: '0 5px',
}}
/>
)
}
Input Digit 组件
import React from 'react';
import { FieldType } from './Context';
// Input UI组件
export default (props: FieldType) => {
return (
<input
type="password"
{...props}
value={props.value || ''}
style={{
border: '2px solid rgb(186 203 255)',
borderRadius: 6,
width: '100%',
height: 28,
padding: '0 5px',
}}
/>
);
};
Password组件
import React from 'react';
import { FieldType } from './Context';
// Label UI组件
export default (props: FieldType) => {
return (
<div
{...props}
style={{
border: '2px solid rgb(186 203 255)',
borderRadius: 6,
width: '100%',
height: 28,
padding: '0 5px',
}}
>
{props.value ? props.value : ''}
</div>
);
};
纯展示组件
0.1.6 主页面
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormProvider, Field, FieldType, validate } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';
const data = observable({
name: {
title: '名字',
value: '',
errors: [] as string[],
visible: true,
component: Input,
componentProps: {
placeholder: '你是谁',
},
decorator: FormItem,
decoratorProps: {},
validator: [RequireValidator()],
onInput: function (e: any) {
data.name.value = e.target.value;
data.name.errors = validate(data.name.value, data.name.validator);
},
},
nameLength: {
title: '名字长度',
value: 0,
errors: [] as string[],
visible: true,
component: Label,
componentProps: {
disabled: true,
},
decorator: FormItem,
decoratorProps: {},
validator: [RequireValidator()],
onInput: function (e: any) {},
},
age: {
title: '年龄',
value: undefined,
errors: [] as string[],
visible: true,
component: InputDigit,
componentProps: {},
decorator: FormItem,
decoratorProps: {
style: { height: 20 },
},
validator: [RequireValidator()],
onInput: function (e: any) {
data.age.value = e.target.value;
data.age.errors = validate(data.age.value, data.age.validator);
},
},
password: {
title: '密码',
value: undefined,
errors: [] as string[],
visible: true,
component: Password,
componentProps: {},
decorator: FormItem,
decoratorProps: {},
validator: [RequireValidator()],
onInput: function (e: any) {
data.password.value = e.target.value;
data.password.errors = validate(
data.password.value,
data.password.validator,
);
},
},
});
//派生属性
autorun(() => {
data.nameLength.value = data.name.value.length;
});
export default () => {
console.log('Top Render');
return (
<FormProvider form={data}>
<button
onClick={() => {
let componentProps = data.name.componentProps as any;
if (componentProps.placeholder.indexOf('你是谁') != -1) {
componentProps.placeholder = '我是我';
} else {
componentProps.placeholder = '你是谁';
}
}}
>
切换name组件的componentProps[placeholder]
</button>
<Field name="name" />
<Field name="nameLength" />
<button
onClick={() => {
let decoratorProps = data.age.decoratorProps as any;
decoratorProps.style.height += 5;
}}
>
切换age组件的decoratorProps[style.height]
</button>
<Field name="age" />
<button
onClick={() => {
let field = data.password;
if (field.component == Password) {
field.component = Input;
} else {
field.component = Password;
}
}}
>
切换password组件的Component
</button>
<Field name="password" />
</FormProvider>
);
};
最后,这是主页面。思路主要是:
- 将表单的errors,component,componentProps,decorator,decoratorProps全部纳入Observable的管理中。这使得我们可以轻松地通过更改Observable的字段,来更新页面。
- 将自定义组件,用value与onChange,设计为受控组件,而且是表单项UI的约定接口,这使得复用不同的UI组件,或者自定义UI组件更加简单。
- Field组件,通过FormContext来获取当前的Form,使用自身的title属性获取自己所属的Field字段。并且,FormItem组件使用FieldContext来获取自己所属的Field信息。这使得Field组件即使在被多层div嵌套底下依然能正常使用,换句话说,使用FormProvider,并没有限制Field组件的排版。
- 在onInput的输入中,进行各个字段的校验
- 使用autorun来实现,自动化的数据联动
MVVM的原理让业务逻辑变得直观和轻松。
性能也不错,当”名字”项变动的时候,只有“名字”项,以及派生的”名字长度”项会重新render。顶层页面,以及其他组件都不需要重新render。而且,重点是,这种性能是不需要写任何的componentShouldUpdate的基础上的,纯粹地铺业务逻辑就可以得到较好的性能优化。
0.1.7 问题
经过上面的试验,我们也看出了一点其他的问题:
- 每个表单的字段都需要重复写title,Component,errors,visible,onInput真的好麻烦。作为表单这个特定的领域下,应该有一个通用的领域描述对象,而不是每次都需要用户自己手写每一个字段。
- 如果某个组件,需要同时控制age与name两个字段,显然现在这种做法还做不到。(目前我的项目基本上都遇到这个需求,这真的不是奇怪的需求)
- 当页面onInput的时候,需要用户手动去调用Validator的函数,这不应该是自动触发的吗,麻烦。
- Validator的触发方式,应该包括有onInput,onFocus,onBlur等不同情况的实现。而不是,只有onInput就触发Validator的实现。
- autorun能实现被动的数据联动,但是当数据是循环联动的时候,这种被动联动就会造成无穷循环触发。所以,应该还需要有主动的数据联动的支持。(认真思考一下,为什么会出现无穷循环触发)
- 如果对表单的状态进行持久化(读与写)的话,应该有提供这样的方法。毕竟你不能直接对Observable持久化,里面有React组件,还有function。
- 表单里面应该还支持嵌套表单,嵌套对象编辑的情况
- 表单的初始值怎样导入
- 缺少表单对象的反馈,例如当表单任意一个字段的变化时,希望得到回调通知
0.2 有什么
好了,现在core库出来了,官方文档在这里。Core库的定位是MVVM中的ViewModel层,它是针对表单这个特定的领域,创建的一个通用的Observable对象。这样会大幅降低我们的开发成本,但是有一定的门槛。如果说,Reactive仅仅是对Mobx的一种性能优化版本而已,那么Core库就是站在海量的表单业务下抽象出来的通用组件,它的设计的确很棒!
Reactive作为一个领域对象,它的核心内容是:
- 字段值的获取与更新,包括value与initalValue
- 显示与隐藏,visbile与hidden
- 校验器与消息反馈,校验器(validator)是对字段变化的响应,消息反馈(feedback)是校验器执行后的结果
- 交互模式,表单是以展示,还是编辑,还是不可编辑的样式。
- 生命周期,表单与字段级别的Init,Mount,Unmount,Change,onInput,onValidate的通知
- 依赖追踪器,更轻松地实现主动与被动联动
- 对Object与List支持,ObjectField与ArrayField。
- 对装饰组件的支持,VoidField没有字段值,它仅仅是布局与装饰的描述
- 字段路径,对字段地址的DSL描述。这个设计实在巧妙
最后,Reactive的设计是UI无关的,业务逻辑与UI组件是彻底解耦的。换句话说,底层的UI组件任意变动情况下,业务逻辑的代码一行都不用改!
由于代码是UI无关的,所以在后续的测试代码中,生命周期的事件都是需要手动触发的。在实际的使用环境中,组件的生命周期事件是自动触发的,这点要注意。
1 字段值的获取与更新
代码在这里
表单中最重要的属性当然是value,每个表单项的值,但是core库提供了多个可以对表单项进行set与get操作的API,它们的区别是什么?难点在于:
- 当要对表单项A进行set操作的时候,表单项A可能还没有创建起来,如何对一个还没创建的表单项进行set与get的操作?
- 表单项除了有value,还有initialValue,它们之间的区别是什么,什么时候一个值会覆盖另外一个值,什么时候不会?
1.1 当前值
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
initialValues: {
name: 'fish',
},
effects() {
onFieldMount('name', (field) => {
console.log('field : [name] mount!');
});
onFieldChange('name', (field, form) => {
//当Field是VoidField的时候,没有value
let field2 = field as Field;
console.log('field : [name] change!', field2.value);
});
onFieldChange('name', ['component'], (field, form) => {
console.log(
'field : [name] component change!',
field.component,
);
});
//测试已创建field,但没初始化的字段,get操作,
onFieldChange('name', (field, form) => {
//获取value的方法1,直接以本field为基点查找其他field
console.log(field.query('.age').value());
//获取value的方法2,query以后使用take转换为GeneralField
let ageField = field.query('.age').take() as Field;
console.log(ageField.value);
//获取value的方法3,通过form获取value,需要field在initialValues已经配置好了
console.log(form.values.age);
//获取value的方法4,通过form获取value
console.log(form.getValuesIn('age'));
//获取value的方法5,通过form获取value
console.log(form.getFieldState('age').value);
});
/*
对于未创建field,以及还没初始化的字段,要进行get操作的话
field.query('xxx').value(),相当方便的相对位置查找
field.query('xxx').take().value,相当方便的相对位置查找,可以获取多种属性
form.getValuesIn,获取value,只能从顶层开始查找
form.getFieldState(),获取多种属性,只能从顶层开始查找
*/
//测试对未创建field,以及还没初始化的字段,get操作,
onFieldChange('name', (field, form) => {
//获取value的方法1,直接以本field为基点查找其他field
console.log(field.query('.age1').value());
//获取value的方法2,通过form获取value
console.log(form.getValuesIn('age2'));
//获取value的方法3,通过form获取value,需要field在initialValues已经配置好了
//但是当字段是深层嵌套的时候就会报错
console.log(form.values.age3);
//获取value的方法4,query以后使用take转换为GeneralField
//这个方法不行,因为ageField还没通过createField创建出来
//let ageField = field.query('.age4').take() as Field;
//console.log(ageField.value);
//获取value的方法5,通过form获取value
//这个方法也不行,因为ageField还没通过createField创建出来
//console.log(form.getFieldState('age5').value);
});
/*
对于未创建field,以及还没初始化的字段,要进行get操作的话
field.query('xxx').value(),相当方便的相对位置查找
form.getValuesIn,获取value,只能从顶层开始查找
*/
//测试对未创建field,以及还没初始化的字段,set操作,
onFieldChange('name', (field, form) => {
//设置value的方法1,直接以本field为基点查找其他field
//这个方法不行
//let field2 = field.query('.age1').take() as Field;
//field2.value = 10;
//设置value的方法2,通过form获取value
form.setValuesIn('age2', 11);
//设置value的方法3,通过form获取value
//但是当字段是深层嵌套的时候就会报错
form.values.age3 = 12;
//获取value的方法4,直接以本field为基点查找其他field
//如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
form.setFieldState('age4', (state) => {
console.log('age4 set');
state.value = 13;
//这个方法可以设置field的其他属性
state.componentProps = { a: 3 };
});
});
/*
对于未创建field,以及还没初始化的字段,要进行set操作的话
form.setValuesIn,只设置value
form.setFieldState,设置value,component等其他任意属性
*/
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createField的时候就会触发onFieldChange,包括value与component
let ageField = form.createField({ name: 'age' });
console.log('create Field');
//createField的时候就会触发onFieldChange,包括value与component
let field = form.createField({ name: 'name' });
//手动触发field的onMount方法
console.log('mount');
field.onMount();
let setHasCrateFieldOfValue = async () => {
//手动更改field的value的方法
console.log('change value');
field.value = '123';
//手动更改field的value的方法2
await sleep(1000);
field.setValue('1234');
//手动更改field的value的方法3,通过form,需要有initalValue
await sleep(1000);
form.values.name = '12345';
//手动更改field的value的方法4,通过form
await sleep(1000);
form.setValuesIn('name', '123456');
//手动更改field的value的方法5
//这种方法会覆盖其他的字段,像setState一样与其他字段的原有值合并后一起更新
await sleep(1000);
form.setValues({
name: '1234567',
age: 11,
});
//手动更改field的value的方法6
form.setFieldState('name', (state) => {
state.value = '12345678';
});
};
await setHasCrateFieldOfValue();
}}
>
点击,更改[fish]field的值
</button>
</div>
);
};
对于未创建field,并初始化的字段,要进行get操作的话:
- field.query(‘xxx’).value(),相当方便的相对位置查找
- field.query(‘xxx’).take().value,相当方便的相对位置查找,可以获取多种属性
- form.getValuesIn,获取value,只能从顶层开始查找
- form.getFieldState(),获取多种属性,只能从顶层开始查找
对于未创建field,但还没初始化的字段,要进行get操作的话:
- field.query(‘xxx’).value(),相当方便的相对位置查找
- form.getValuesIn,获取value,只能从顶层开始查找
对于未创建field,以及还没初始化的字段,要进行set操作的话:
- form.setValuesIn,只设置value
- form.setFieldState,设置value,component等其他任意属性
1.2 未赋值时,设置初始值
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldChange('name', (field, form) => {
//当Field是VoidField的时候,没有value
let field2 = field as Field;
console.log('field : [name] value = ', field2.value);
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//初始化的时候,值为undefined
let field = form.createField({ name: 'name' });
//数据未赋值过的情况下,设置initialValue,会自动赋值到value
await sleep(1000);
form.setInitialValues({
name: '213',
});
}}
>
点击,初始化name的value值
</button>
</div>
);
};
数据未赋值过的情况下,设置initialValue,会自动赋值到value。覆盖更新
1.3 已赋值时,设置初始值
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldChange('name', (field, form) => {
//当Field是VoidField的时候,没有value
let field2 = field as Field;
console.log('field : [name] value = ', field2.value);
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//初始化的时候,值为undefined
let field = form.createField({ name: 'name' });
//设置了一次value
await sleep(1000);
field.setValue('1');
//后续再触发initialValues,依然会触发设置value
await sleep(1000);
form.setInitialValues({
name: '2',
});
}}
>
点击,初始化name的value值
</button>
</div>
);
};
数据已赋值过的情况下,设置initialValue,会自动赋值到value。覆盖更新
1.4 已用户输入时,设置初始值
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldChange('name', (field, form) => {
//当Field是VoidField的时候,没有value
let field2 = field as Field;
console.log('field : [name] value = ', field2.value);
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//初始化的时候,值为undefined
let field = form.createField({ name: 'name' });
//使用onInput,来模拟用户输入
await sleep(1000);
field.onInput('1');
//onInput以后,使用form来设置initialValues是会失败的,无法覆盖进去
await sleep(1000);
form.setInitialValues({
name: '2',
});
}}
>
点击,初始化name的value值
</button>
</div>
);
};
数据已被用户输入的情况下,设置initialValue,无法赋值到value。阻止更新
2 显示与隐藏
代码在这里
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createField的时候就会触发onFieldChange,包括value与component
let field = form.createField({ name: 'name' });
//模拟用户输入
field.onInput('fish');
console.log(field.value);
//使用display为none,会导致value被清空为undefined
await sleep(1000);
field.display = 'none';
console.log(field.value);
}}
>
点击,更改[name]的display为none
</button>
<button
onClick={async () => {
console.log('create Field');
//createField的时候就会触发onFieldChange,包括value与component
let field = form.createField({ name: 'name2' });
//模拟用户输入
field.onInput('cat');
console.log(field.value);
//使用display为hidden,value依然不会被清空
await sleep(1000);
field.display = 'hidden';
console.log(field.value);
}}
>
点击,更改[name2]的display为hidden
</button>
</div>
);
};
display有三种状态,visible,none和hidden。
- visible是正常的显示状态
- hidden是隐藏状态,组件会消失,但数据不会清空
- none是去掉状态,组件会消失,而且数据会清空
3 校验器与消息反馈
代码在这里
校验器是数据变更的自动触发的行为,消息反馈是校验器计算的结果。表单的校验器的业务难点在于:
- 要区分什么时候进行校验,值变更,onInput,onFoucs,onBlur?
- 校验的过程可以是异步进行的
- 消息反馈可能是多个校验器的结果
- 校验的提示可以是
3.1 触发机制
3.1.1 非值变更触发
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createField,并且设置required为true
let field = form.createField({
name: 'name',
required: true,
});
//模拟用户输入
field.onInput('f');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为空数组
//feedbacks是空数组
console.log(field.feedbacks, field.errors);
//模拟用户输入
field.onInput('');
await sleep(100);
//field.errors为1个元素的数组,数据为"该字段是必填字段"
//feedbacks是3个元素的数组
/*
[{
code: "ValidateError"
messages: ["该字段是必填字段"]
triggerType: "onInput"
type: "error"
},
{
code: "ValidateSuccess"
messages: []
triggerType: "onInput"
type: "success"
},{
code: "ValidateWarning"
messages: []
triggerType: "onInput"
type: "warning"
}]
*/
console.log(field.feedbacks, field.errors);
}}
>
onInput触发校验,required的校验,校验的结果放在feedbacks上
</button>
<button
onClick={async () => {
console.log('create Field');
//createField,并且设置required为true
let field = form.createField({
name: 'name2',
required: true,
});
//直接修改数据
field.value = 'f';
await sleep(100);
//field.errors为空数组
//feedbacks是空数组
console.log(field.feedbacks, field.errors);
//直接修改数据
field.value = '';
await sleep(100);
//errors依然为空,因为validator是基于onInput,onFocus或者onBlur来触发的,直接修改value是不会触发校验的
console.log(field.feedbacks, field.errors);
}}
>
直接修改value是没有触发校验的
</button>
</div>
);
};
在实验中可以看到,onInput能触发消息反馈,仅仅的值变更不会触发消息反馈。这样的设计是经过考虑的,因为程序员设置的数据不应该触发校验器,而只有用户输入的数据才应该触发校验器。而且,经过onInput触发校验器,能避免首次校验出错的问题。试想一下,刚开始创建的表单项,值当然为undefined,这个时候就触发required校验器,意味着所有的表单项都有错误提示的,这样当然是不好的用户体验。
3.1.2 onBlur与onFocus触发
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name',
});
field.setValidator({
triggerType: 'onBlur',
validator: (value) => {
console.log('value:', value);
if (value != '123') {
return '输入的字符串不是123';
}
return '';
},
});
//直接设置value
field.onInput('f');
await sleep(500);
//field.errors依然为空数组,因为triggerType为onBlur
console.log(field.feedbacks, field.errors);
//模拟控件blur
field.onBlur();
await sleep(500);
//field.errors现在不是为空数组了,因为blur的时候触发了校验
//它的值为1个元素的数组
console.log(field.feedbacks, field.errors);
}}
>
onBlur触发校验
</button>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name2',
});
field.setValidator({
triggerType: 'onFocus',
validator: (value) => {
console.log('value:', value);
if (value != '123') {
return '输入的字符串不是123';
}
return '';
},
});
//直接设置value
field.onInput('f');
await sleep(500);
//field.errors依然为空数组,因为triggerType为onBlur
console.log(field.feedbacks, field.errors);
//模拟控件focus
field.onFocus();
await sleep(500);
//field.errors现在不是为空数组了,因为blur的时候触发了校验
//它的值为1个元素的数组
console.log(field.feedbacks, field.errors);
}}
>
onFocus触发校验
</button>
</div>
);
};
core库除了支持onInput的时候触发,还支持onBlur与onFocus的校验触发。当然,组件自身需要配置core库进行回调onBlur与onFocus方法才能成功。自定义校验器在没有配置triggerType的情况下,默认就是onInput的触发校验。
3.2 校验方式
3.2.1 规则校验
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'age',
validator: {
required: true, //必填项
minimum: 5, //最小值为5
},
});
//模拟用户输入,错误输入
field.onInput(1);
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput(7);
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为为空
console.log(field.feedbacks, field.errors);
}}
>
validator的属性方式校验,minimum,最小值
</button>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'name',
validator: {
required: true, //必填项
min: 5, //字符串长度最小为5
},
});
//模拟用户输入,错误输入
field.onInput('abc');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('abcdef');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为为空
console.log(field.feedbacks, field.errors);
}}
>
validator的属性方式校验,min,最小字符串长度
</button>
</div>
);
};
像required,min,minimum都是规则校验的方式,具体可以看这里
3.2.2 自定义规则校验
import {
createForm,
onFieldChange,
onFieldMount,
registerValidateRules,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
registerValidateRules({
GlobalPropertyFormat(value) {
if (!value) return '';
return value !== '123' ? '错误了❎' : '';
},
});
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'age',
validator: {
required: true, //必填项
GlobalPropertyFormat: true, //自定义的format
},
});
//模拟用户输入,错误输入
field.onInput('137');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('123');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为为空
console.log(field.feedbacks, field.errors);
}}
>
validator的自定义属性方式校验,123格式
</button>
</div>
);
};
core库支持自定义规则校验,通过registerValidateRules注册一个全局的校验规则,就可以在validator中使用这个规则项了
3.2.3 格式校验
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'age',
validator: {
required: true, //必填项
format: 'phone', //格式必须为电话格式
},
});
//模拟用户输入,错误输入
field.onInput('137');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('13712345678');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为空
console.log(field.feedbacks, field.errors);
}}
>
validator的format方式校验,phone格式
</button>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'name',
validator: {
required: true, //必填项
format: 'email', //格式必须为url格式
},
});
//模拟用户输入,错误输入
field.onInput('abc');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('abc@qq.com');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为空
console.log(field.feedbacks, field.errors);
}}
>
validator的format方式校验,email格式
</button>
</div>
);
};
格式校验,就是用validator的format项进行校验,这种校验方式都是以正则表达式为依据的校验。core库默认就写好了很多格式的校验,具体看这里
3.2.4 自定义格式校验
import {
createForm,
onFieldChange,
onFieldMount,
registerValidateFormats,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
//注册一个自定义的format
registerValidateFormats({
MyFormat: /123/,
});
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
let field = form.createField({
name: 'age',
validator: {
required: true, //必填项
format: 'MyFormat', //格式必须为电话格式
},
});
//模拟用户输入,错误输入
field.onInput('137');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为不为空
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('123');
//校验过程是异步的,所以需要一个await
await sleep(100);
//field.errors为空
console.log(field.feedbacks, field.errors);
}}
>
validator的自定义format方式校验,123格式
</button>
</div>
);
};
我们可以用registerValidateFormats来注册一个全局的自定义格式,然后在validator的format中指定我们的格式名称就可以了。
3.2.5 闭包校验
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name',
});
field.setValidator((value) => {
if (value != '123') {
return '非123格式';
}
return '';
});
//模拟用户输入,错误输入
field.onInput('897');
await sleep(500);
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('123');
await sleep(500);
console.log(field.feedbacks, field.errors);
}}
>
自定义闭包的校验方式,123格式
</button>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name2',
});
field.setValidator({
triggerType: 'onFocus',
validator: (value) => {
if (value != '123') {
return '输入的字符串不是123';
}
return '';
},
});
//模拟用户输入,错误输入
field.value = '897';
field.onFocus();
await sleep(500);
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.value = '123';
field.onFocus();
await sleep(500);
console.log(field.feedbacks, field.errors);
}}
>
自定义闭包的校验方式,123格式,带triggerType
</button>
</div>
);
};
闭包校验的方式,就是一种局部的校验方式了。在validator中直接传入闭包函数,或者一个对象,含有triggerType与validator属性就可以了。注意一下,自定义闭包校验的时候,是如何返回错误描述的。
3.2.6 异步校验
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name',
});
field.setValidator(async (value) => {
await sleep(1000);
if (value != '123') {
return '非123格式';
}
return '';
});
//模拟用户输入,错误输入
field.onInput('897');
await sleep(1500);
console.log(field.feedbacks, field.errors);
//模拟用户输入,正确输入
field.onInput('123');
await sleep(1500);
console.log(field.feedbacks, field.errors);
}}
>
自定义闭包的异步校验方式,支持返回的是Promise的对象
</button>
</div>
);
};
core库的设计中本来就支持异步的校验,只需要在闭包中返回一个Promise就可以了。返回Promise容易呀,我们完全可以用async来包装一下函数就可以了。
3.2.7 组合校验
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//我们使用自定义的校验器,并且设置只有blur的时候才触发校验
let field = form.createField({
name: 'name',
});
field.setValidator([
{ required: true },
{ format: 'email' },
{
triggerType: 'onInput',
validator: (value) => {
let str = value as string;
if (str.startsWith('fish@') == false) {
return '只支持fish的发件人';
}
return '';
},
},
]);
//直接设置value
field.onInput('');
await sleep(500);
//2个错误,非必填,不是fish开头
console.log(field.feedbacks, field.errors);
//直接设置value
field.onInput('fish@123');
await sleep(500);
//1个错误,非email格式
console.log(field.feedbacks, field.errors);
//直接设置value
field.onInput('123@163.com');
await sleep(500);
//1个错误,非fish开头
console.log(field.feedbacks, field.errors);
}}
>
组合多种方式的校验
</button>
</div>
);
};
validator也支持多种校验方式的组合,直接传递一个数组进去就可以了,然后数组的每一项可以是规则校验,格式校验,和闭包校验的其中一种都可以。
4 交互模式
代码在这里
表单的交互模式有四种,core库在这点考虑得比较细致,分别是:
- editable,可以编辑的正常状态
- disabled,不可以编辑,保留输入框,输入框是不可激活的
- readOnly,不可以编辑,保留输入框,输入框是可激活的
- readPretty,不可以编辑,不保留输入框
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { Field } from '@formily/react';
import { Form, FormItem, Input, Password, Submit } from '@formily/antd';
export default () => {
const form = useMemo(() => {
return createForm({
initialValues: {
username: 'a',
username2: 'b',
username3: 'c',
username4: 'd',
},
effects() {},
});
}, []);
return (
<Form
form={form}
layout="vertical"
size="small"
onAutoSubmit={console.log}
>
<Field
name="username"
title="用户名"
//交互模式,可编辑,框是深灰
pattern="editable"
required
decorator={[FormItem]}
component={[Input, {}]}
/>
<Field
name="username2"
title="用户名2"
//交互模式,不可编辑,框是浅灰色的,像被禁用了
pattern="disabled"
required
decorator={[FormItem]}
component={[Input, {}]}
/>
<Field
name="username3"
title="用户名3"
//交互模式,不可编辑,框是深灰
pattern="readOnly"
required
decorator={[FormItem]}
component={[Input, {}]}
/>
<Field
//交互模式,不可编辑,没有框
name="username4"
title="用户名4"
pattern="readPretty"
required
decorator={[FormItem]}
component={[Input, {}]}
/>
</Form>
);
};
代码还是比较简单的,这里我们直接用了antd的Formily包装组件
分别是editable,disabled,readOnly,readPretty的交互模式
5 生命周期
代码在这里
生命周期分两种,表单的生命周期,与字段的生命周期。生命周期包括:
- 挂载时与卸载时
- 表单或字段的,value和state变更的触发
- 校验时的触发
- onInput,onFocus,onBlur的触发
要注意,触发的时候,是否包括首次触发
5.1 字段生命周期
import {
createForm,
onFieldChange,
onFieldInit,
onFieldInputValueChange,
onFieldMount,
onFieldUnmount,
onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
//createField的时候自动触发
onFieldInit('name', (field) => {
console.log('字段[name]初始化');
});
//onMount的时候手动触发
onFieldMount('name', (field) => {
console.log('字段[name] mount');
});
//onUnMount的时候手动触发
onFieldUnmount('name', (field) => {
console.log('字段[name] unmount');
});
//onChange的时候自动触发,默认就是只有value变化的时候触发
//注意,在首次createField也会自动触发
onFieldChange('name', (field) => {
console.log('字段[name] change');
});
//onChange的时候自动触发,默认就是只有value变化的时候触发
//注意,在首次createField也会自动触发,即使是非value的属性
onFieldChange('name', 'componentProps', (field) => {
console.log('字段[name] comonentProps change');
});
//onValueChange的时候自动触发,只有value变化的时候触发
//注意,首次不会自动触发
onFieldValueChange('name', (field) => {
console.log('字段[name] value change');
});
//onInput的时候手动触发,这个注意与onFieldValueChange的不同
onFieldInputValueChange('name', (field) => {
console.log('字段[name] value input change');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
//触发onFieldInit,首次会触发onFieldChange
console.log('createField');
let field = form.createField({ name: 'name' });
//触发onFieldMount
await sleep(100);
console.log('onMount');
field.onMount();
//触发onFieldChange
await sleep(100);
console.log('set componentProps');
field.componentProps = { size: 10 };
//触发onFieldChange与onFieldValueChange
await sleep(100);
console.log('set value');
field.value = 10;
//触发onFieldChange与onFieldValueChange,onFieldInputValueChange
await sleep(100);
console.log('set input');
field.onInput('cat');
//触发onUnmount
await sleep(100);
console.log('set onUnmount');
field.onUnmount();
}}
>
触发field的生命周期
</button>
</div>
);
};
注意点如下:
- onFieldChange,首次会触发,默认为value字段变更触发,也可以设置为其他属性变更时触发。这个用得比较多,而且能同时倾听多个字段的触发,后续会有介绍。
- onFieldValueChange,首次不会触发,仅value字段变更时触发
- onFieldInputValueChange,仅字段onInput的时候触发
5.2 表单生命周期
import {
createForm,
onFormInit,
onFormInputChange,
onFormMount,
onFormSubmit,
onFormUnmount,
onFormValuesChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
//createForm的时候触发
onFormInit(() => {
console.log('表单初始化');
});
//onMount的时候手动触发
onFormMount(() => {
console.log('表单 mount');
});
//onUnMount的时候手动触发
onFormUnmount(() => {
console.log('表单 unmount');
});
//onChange的时候自动触发,默认就是只有value变化的时候触发
//注意,首次不会触发
onFormValuesChange(() => {
console.log('form value change');
});
//onInput的时候手动触发,这个注意与onFieldValueChange的不同
onFormInputChange(() => {
console.log('form value input change');
});
//submit的时候自动触发
onFormSubmit(() => {
console.log('form submit');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
//触发onFieldInit
console.log('createField');
let field = form.createField({ name: 'name' });
//触发form.onMount
await sleep(100);
console.log('onMount');
form.onMount();
//不会触发onFormValuesChange
await sleep(100);
console.log('set componentProps');
field.componentProps = { size: 10 };
//触发onFormValuesChange
await sleep(100);
console.log('set value');
field.value = 10;
//触发onFormInputChange与onFormValuesChange
await sleep(100);
console.log('set input');
field.onInput('cat');
//触发onSubmit
await sleep(100);
console.log('set submit');
form.submit();
//触发onUnmount
await sleep(100);
console.log('set onUnmount');
form.onUnmount();
}}
>
触发form的生命周期
</button>
</div>
);
};
注意点如下:
- onFormValuesChange,如何onFieldValueChange的设计,首次不会触发,只有value变更的时候会触发
- onFormInputChange,只有Form的任何一个字段回调onInput函数的时候触发
- onFormSubmit,表单提交的时候触发
6 依赖追踪器
代码在这里
依赖跟踪,是数据联动与逻辑联动的关键实现。在core库中,主要分为主动联动,与被动联动两种。
6.1 主动联动
import { createForm, onFieldChange, onFieldMount } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
initialValues: {
count: 10,
price: 1,
total: 0,
},
effects() {
//主动联动,对A数据主动倾听,然后设置B数据
//对count数据倾听,然后设置total数据
//首次createField(count) 会自动触发
onFieldChange('count', (field) => {
console.log('Field[count] change');
const count = (field as Field).value;
const price = field.query('.price').value();
form.setFieldState('total', (state) => {
state.value = count * price;
});
});
//主动联动
//对price数据倾听,然后设置total数据
//首次createField(price) 会自动触发
onFieldChange('price', (field) => {
console.log('Field[price] change');
const count = (field as Field).value;
const price = field.query('.count').value();
form.setFieldState('total', (state) => {
state.value = count * price;
});
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
let countField = form.createField({ name: 'count' });
let priceField = form.createField({ name: 'price' });
//只有totalField创建了以后,form的setFieldState才会生效
let totalField = form.createField({ name: 'total' });
//虽然初始值为0,但是这里已经被自动计算为10
let total = form.getValuesIn('total');
console.log(total);
//更改count的数值,total也会更新
countField.value = 20;
let total2 = form.getValuesIn('total');
console.log(total2);
//更改price的数值,total也会更新
priceField.value = 2;
let total3 = form.getValuesIn('total');
console.log(total3);
}}
>
主动联动
</button>
</div>
);
};
主动联动,就是指定数据变更以后,触发指定的回调方法,然后在回调方法里面更新字段的值。主动联动的实现,依赖于reactive的observe方法。
6.2 被动联动
import {
createForm,
onFieldChange,
onFieldMount,
onFieldReact,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
initialValues: {
count: 10,
price: 1,
total: 0,
},
effects() {
//被动联动,指定A数据受到B或者C数据的影响,当B数据或者C数据变化的时候,要自动触发
//利用Reactive的能力,core库是自动收集依赖的,不需要显式指定依赖哪些数据,当依赖变化时,自动触发重新计算
//首次createField(total) 会自动触发
onFieldReact('total', (field) => {
console.log('total recompute');
const count = field.query('.count').value();
const price = field.query('.price').value();
const myField = field as Field;
myField.value = count * price;
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
let countField = form.createField({ name: 'count' });
let priceField = form.createField({ name: 'price' });
//只有totalField创建了以后,form的setFieldState才会生效
let totalField = form.createField({ name: 'total' });
//虽然初始值为0,但是这里已经被自动计算为10
let total = form.getValuesIn('total');
console.log(total);
//更改count的数值,total也会更新
countField.value = 20;
let total2 = form.getValuesIn('total');
console.log(total2);
//更改price的数值,total也会更新
priceField.value = 2;
let total3 = form.getValuesIn('total');
console.log(total3);
}}
>
被动联动
</button>
</div>
);
};
onFieldReact是core库中创新性的联动方法,大大方便了联动逻辑的实现。它会自动收集数据的依赖,当依赖数据发生变动以后,自动触发新数据的计算来赋值字段。被动联动的实现,依赖于reactive库的autorun方法。
7 其他Field类型
代码在这里
对于,表单中嵌套的数据类型,如数组,对象,core库也有支持。另外一方面,core库对于某些仅仅为了提供布局,和装饰用的虚拟字段提供支持。
7.1 ArrayField
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldInputValueChange('items', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
const form2 = useMemo(() => {
return createForm({
effects() {
onFieldInputValueChange('items', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
const form3 = useMemo(() => {
return createForm({
effects() {
onFieldInputValueChange('items', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createArrayField
let field = form.createArrayField({ name: 'items' });
//push操作会触发onInuput
field.push('fish2');
field.push('cat2');
//可以通过value获取到数组的值
console.log(field.value.length);
//通过path,我们可以获取到下一级的field
let fieldItem = form.createField({ name: 'items.0' });
console.log(fieldItem.value);
//通过path,我们可以获取到下一级的field
let fieldItem2 = form.createField({ name: 'items.1' });
console.log(fieldItem2.value);
//这个field没有push进去,所以是undefined的
let fieldItem3 = form.createField({ name: 'items.2' });
console.log(fieldItem3.value);
}}
>
ArrayField,通过官方ArrayField操作
</button>
<button
onClick={async () => {
console.log('create Field');
//createArrayField
let field = form2.createArrayField({ name: 'items' });
//可以push数据,但是触发不了onInput
field.value.push('fish2');
field.value.push('cat2');
//可以通过value获取到数组的值
console.log(field.value.length);
//通过path,我们可以获取到下一级的field
let fieldItem = form2.createField({ name: 'items.0' });
console.log(fieldItem.value);
//通过path,我们可以获取到下一级的field
let fieldItem2 = form2.createField({ name: 'items.1' });
console.log(fieldItem2.value);
//这个field没有push进去,所以是undefined的
let fieldItem3 = form2.createField({ name: 'items.2' });
console.log(fieldItem3.value);
}}
>
ArrayField,通过Field的value来操作,触发不了onInput,因此无法实现校验等操作
</button>
<button
onClick={async () => {
console.log('create Field');
//createArrayField
let field = form3.createField({ name: 'items', value: [] });
//可以push数据,但是触发不了onInput
field.value.push('fish3');
field.value.push('cat3');
//可以通过value获取到数组的值
console.log(field.value.length);
//通过path,我们可以获取到下一级的field
let fieldItem = form3.createField({ name: 'items.0' });
console.log(fieldItem.value);
//通过path,我们可以获取到下一级的field
let fieldItem2 = form3.createField({ name: 'items.1' });
console.log(fieldItem2.value);
//这个field没有push进去,所以是undefined的
let fieldItem3 = form3.createField({ name: 'items.2' });
console.log(fieldItem3.value);
}}
>
用普通的Field来模拟Array,,触发不了onInput,因此无法实现校验等操作,可以获取到path
</button>
</div>
);
};
注意点如下:
- 使用createArrayField就可以创建一个数组类型的Field字段了
- 使用createField,填写好路径,就能操控到数组下的子元素的Field了
- 尽量避免直接对Field的value进行支持,因为这样会丢掉onInput的回调(没有validator和feedback的内置支持)
- 使用普通的createField,也能创建数组类型的Field字段,不过也会失去onInput的回调支持
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldInputValueChange('items', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createArrayField
let field = form.createArrayField({ name: 'items' });
//push操作会触发onInuput
field.push(undefined);
field.push(undefined);
//可以通过value获取到数组的值
console.log(field.value.length);
//通过path,我们可以获取到下一级的field
let fieldItem = form.createField({
name: 'items.0',
value: 'hello',
});
console.log(fieldItem.value);
//通过path,我们可以获取到下一级的field
let fieldItem2 = form.createField({
name: 'items.1',
value: 123,
});
console.log(fieldItem2.value);
//这个field没有push进去,所以是undefined的
let fieldItem3 = form.createField({
name: 'items.2',
});
console.log(fieldItem3.value);
}}
>
ArrayField,可以通过push一个undefined来让后续来配置格式
</button>
</div>
);
};
对于数组下元素的不确定类型,可以直接push一个undefined的值
7.2 ObjectField
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldInputValueChange('person', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createObjectField
let field = form.createObjectField({ name: 'person' });
//push操作会触发onInuput
field.addProperty('name', 'fish');
field.addProperty('age', 123);
field.addProperty('sex', 'male');
//通过path,我们可以获取到下一级的field
let fieldItem = form.createField({
name: 'person.name',
});
console.log(fieldItem.value);
//通过path,我们可以获取到下一级的field
let fieldItem2 = form.createField({
name: 'person.age',
value: 123,
});
console.log(fieldItem2.value);
//这个field没有addProperty进去,所以是undefined的
let fieldItem3 = form.createField({
name: 'person.qq',
});
console.log(fieldItem3.value);
//bashPath的用法,实际取的字段是person.sex
let fieldItem4 = form.createField({
name: 'sex',
basePath: 'person',
});
console.log(fieldItem4.value);
}}
>
ObjectField
</button>
</div>
);
};
ObjectField的使用与ArrayField相当类似了,也是用createObjectField创建Field,而后用addProperty来添加属性。注意,basePath的用法
7.3 VoidField
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldMount,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
import { sleep } from '@/utils';
export default () => {
const form = useMemo(() => {
return createForm({
effects() {
onFieldChange('layout', () => {
console.log('Field [items] onInput');
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
console.log('create Field');
//createVoidField
//void field是布局和装饰用的组件,没有value的,也没有onInput,也没有validator,其他的属性基本都有
let field = form.createVoidField({ name: 'layout' });
field.setComponentProps({ size: 10 });
//可以通过query的语法,来查询节点,这里返回了{size:10}的值
console.log(form.query('layout').take().componentProps);
console.log('void field son!');
let field2 = form.createField({ name: 'layout.name' });
field2.value = 20;
field2.setComponentProps({ width: 30 });
//VoidField的路径,在query的时候,可以不省略
console.log(
form.query('layout.name').take().componentProps,
);
let field3 = form.query('layout.name').take() as Field;
console.log(field3.value);
//VoidField的路径,在query的时候,也可以省略
console.log(form.query('name').take().componentProps);
let field4 = form.query('name').take() as Field;
console.log(field4.value);
//但是,直接取value的话,必须省略,这里的API设计有点奇怪
console.log(form.query('name').value());
}}
>
VoidField
</button>
</div>
);
};
VoidField是虚拟字段,它的作用就是为了布局与装饰用的,没有value,也没有onInput,也没有validator。注意的是,VoidField在query的时候是可以忽略的,这是为了嵌套多层VoidField的时候,依然不需要更改业务逻辑,这个设计很棒。
8 字段路径
代码在这里
对于单个组件需要操控多个字段,或者onFieldChange需要同时倾听多个字段的情况,core库提供了独特的path表达式支持,它以特殊的语法来对多个字段的指定支持。
8.1 字段匹配
8.1.1 同级字段
import { createForm, onFieldChange } from '@formily/core';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldChange('person.age', (field) => {
//注意有一个点号
console.log(field.query('.name').value());
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
let ageField = form.createField({
name: 'person.age',
value: 123,
});
let nameField = form.createField({
name: 'person.name',
value: 'fish',
});
ageField.value = 12;
}}
>
选择同级的path
</button>
</div>
);
};
一个点号就能获取同级下的其他字段
8.1.2 子级字段
import {
createForm,
onFieldChange,
onFieldInputValueChange,
} from '@formily/core';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldInputValueChange('person.fish', (field) => {
//注意,没有点号
console.log(field.path + '.name');
console.log(form.query(field.path + '.name').value());
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
let ageField = form.createObjectField({
name: 'person.fish',
});
ageField.addProperty('name', 'fish');
ageField.addProperty('age', 123);
}}
>
选择子级的path
</button>
</div>
);
};
子级字段,就要在本级后面,加上后续的路径字段了
8.1.3 父级字段
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldValueChange,
} from '@formily/core';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldValueChange('person.name', (field) => {
console.log(field.parent.path.toString());
//注意,两个点号
console.log(field.query('..').value());
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
let ageField = form.createObjectField({
name: 'person',
});
ageField.addProperty('name', 'fish');
ageField.addProperty('age', 123);
let field = form.createField({ name: 'person.name' });
field.setValue('cat');
}}
>
选择父级的path
</button>
</div>
);
};
直接用parent,也可以用两个点号
8.1.4 多key指定字段
import { createForm, onFieldChange } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
//同时匹配,person的age或者name字段
onFieldChange('person.*(age,name)', (field) => {
//注意有一个点号
console.log('Field [' + field.path + '] change');
let field2 = field as Field;
console.log(field2.value);
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
let ageField = form.createField({
name: 'person.age',
value: 123,
});
let nameField = form.createField({
name: 'person.name',
value: 'fish',
});
ageField.value = 12;
nameField.value = 'cat';
}}
>
选择多个key的path
</button>
</div>
);
};
使用星号,加上括号下的多个字段名就可以了。注意,这个语法可以嵌套的
8.1.5 多index指定字段
import { createForm, onFieldChange } from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
//同时匹配,person的age或者name字段
//下标从0开始
onFieldChange('persons.*[1:2]', (field) => {
//注意有一个点号
let field2 = field as Field;
console.log(
'Field [' + field.path + '] change ' + field2.value,
);
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
let personsField = form.createArrayField({
name: 'persons',
});
personsField.push('1');
personsField.push('2');
personsField.push('3');
personsField.push('4');
for (var i = 0; i != 5; i++) {
let person = form.createArrayField({
name: 'persons.' + i,
});
person.value = 'MM' + person.value;
}
}}
>
选择一段range的path
</button>
</div>
);
};
多index指定字段,与多key指定字段是类似的,只是将圆括号,换成方括号而已。
8.2 字段解构赋值
8.2.1 批量get与set
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldChange('name', (field) => {
let field2 = field as Field;
console.log(
'Field [' + field.path + '] change ' + field2.value,
);
});
onFieldChange('age', (field) => {
let field2 = field as Field;
console.log(
'Field [' + field.path + '] change ' + field2.value,
);
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
//解构赋值,将两个字段合并在一起操作
let nameField = form.createField({
name: 'name',
});
let ageField = form.createField({
name: 'age',
});
console.log('setValuesIn');
//同时赋值多个字段
form.setValuesIn('[name,age]', ['fish', 123]);
console.log(form.getValuesIn('[name,age]'));
console.log(form.getValuesIn('name'));
console.log(form.getValuesIn('age'));
}}
>
解构赋值
</button>
</div>
);
};
我们可以创建多个字段,然后在setValuesIn,或者getValuesIn里面,使用方括号的解构赋值与取值的语法,注意,不支持ES6解构的剩余参数的写法。
8.2.2 多字段组合Field
import {
createForm,
onFieldChange,
onFieldInputValueChange,
onFieldValueChange,
} from '@formily/core';
import { Field } from '@formily/core/esm/models';
import { useMemo } from 'react';
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldChange('name', (field) => {
let field2 = field as Field;
console.log(
'Field [' + field.path + '] change ' + field2.value,
);
});
onFieldChange('age', (field) => {
let field2 = field as Field;
console.log(
'Field [' + field.path + '] change ' + field2.value,
);
});
},
});
}, []);
return (
<div>
<button
onClick={() => {
//解构赋值,将两个字段合并在一起操作
let allField = form.createField({
name: '[name,age]',
});
console.log('value', allField.getState().value);
allField.onInput(['fish', 123]);
console.log(form.getValuesIn('[name,age]'));
console.log(form.getValuesIn('name'));
console.log(form.getValuesIn('age'));
allField.value = ['cat', 456];
console.log(form.getValuesIn('[name,age]'));
console.log(form.getValuesIn('name'));
console.log(form.getValuesIn('age'));
}}
>
解构赋值
</button>
</div>
);
};
我们创建field的时候也可以同时指定多个key,那么赋值的时候就能写入到多个key中。同时,也能用getValuesIn与setValuesIn来存取其中一个key的数据。
9 表单受控
代码在这里
在大部分的业务场景中,我们的数据都是存放在单个form里面的。但是,在某些场景里面,我们希望外部数据能同步到form数据里面,或者反过来,form数据同步到外部数据去。
9.1 表单受控
import {
createForm,
onFieldChange,
onFieldMount,
onFieldReact,
} from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { autorun, observable } from '@formily/reactive';
import { Field } from '@formily/core/esm/models';
//注意,这里的实验core库版本要在2.0.0-beta.79以后。在78版本以前的试过不行
export default () => {
const obs = useMemo(() => {
let result = observable({
name: 'kk',
});
autorun(() => {
console.log('autorun:', result.name);
});
return result;
}, []);
const form = useMemo(() => {
return createForm({
//将可观察数据,在createForm的时候,就注入value中,两者就能保持同步
//这称为,响应式表单受控
//可以做双向同步
values: obs, //需要整个结构到复制过去
effects: () => {},
});
}, []);
return (
<div>
<button
onClick={async () => {
let field = form.createField({ name: 'name' });
obs.name = '123';
await sleep(100);
//这里field value变为123
console.log('field value:', field.value);
field.value = '789';
await sleep(100);
//这里obs value也变为789
console.log('obs value:', obs.name);
}}
>
createForm的双向整个表单同步
</button>
</div>
);
};
表单受控的写法较为简单,就是将整个外部的obserable对象赋值到form的values就可以了。而且,这个写法性能超好,还是双向同步的。
9.2 字段受控
import {
createForm,
onFieldChange,
onFieldMount,
onFieldReact,
} from '@formily/core';
import { useMemo } from 'react';
import { sleep } from '@/utils';
import { autorun, observable } from '@formily/reactive';
import { Field } from '@formily/core/esm/models';
export default () => {
const obs = useMemo(() => {
let result = observable({
name: 'kk',
});
autorun(() => {
console.log('autorun:', result.name);
});
return result;
}, []);
const form = useMemo(() => {
return createForm({
//将可观察数据,通过onFieldReact进行同步
//这称为,响应式字段级受控
//但是,这样只能做单向同步,从observable到form的字段,不能从form的字段同步到observable
effects: () => {
onFieldReact('age', (field) => {
let field2 = field as Field;
field2.value = obs.name;
});
},
});
}, []);
return (
<div>
<button
onClick={async () => {
let field = form.createField({ name: 'age' });
obs.name = '123';
await sleep(100);
//这里field value变为123
console.log('field value:', field.value);
field.value = '789';
await sleep(100);
//这里的obs value依然为旧值,123
console.log('obs value:', obs.name);
}}
>
createForm的字段单向同步
</button>
</div>
);
};
当我们仅仅是希望表单的部分字段受控于外部数据的时候,就要使用onFieldReact来做外部数据的同步,注意,这种方式只能实现单向同步。
10 集成core库到自定义UI
代码在这里
我们在第0章,我们介绍了如何仅使用reative库就能构造一个表单,但是效果不尽如意,那么我们就整合core库来进一步优化这个例子吧
10.1 Context
import React, {
createContext,
ReactElement,
useContext,
FunctionComponent,
ReactNode,
Component,
} from 'react';
import { observer } from '@formily/reactive-react';
import { Field, Form, IFieldFactoryProps } from '@formily/core';
//创建上下文,方便Field消费
const FormContext = createContext<Form>({} as Form);
//创建上下文,方便FormItem消费
const FieldContext = createContext<Field>({} as Field);
export { FormContext };
export { FieldContext };
//表单管理入口
type FormProviderProps = {
form: Form;
children: ReactNode;
};
export const FormProvider = (props: FormProviderProps) => {
return (
<FormContext.Provider value={props.form}>
{props.children}
</FormContext.Provider>
);
};
//状态桥接器组件
export const MyField = observer(
(props: IFieldFactoryProps<any, any, any, any>) => {
console.log('Child Component Field: ' + props.name + ' Render');
const form = useContext(FormContext);
const field = form.createField(props);
if (!field.visible) return null;
//渲染字段,将字段状态与UI组件关联
const component = React.createElement(
(field.component[0] as unknown) as string,
{
...field.componentProps,
value: field.value,
onChange: field.onInput,
} as React.Attributes,
);
//渲染字段包装器
const decorator = React.createElement(
(field.decorator[0] as unknown) as string,
field.decoratorProps,
component,
);
return (
<FieldContext.Provider value={field}>
{decorator}
</FieldContext.Provider>
);
},
);
Context代码直接引用core库的类型就可以了,更加简单
10.2 FormItem
import { observer } from '@formily/reactive-react';
import { FieldContext } from './Context';
import { useContext } from 'react';
// FormItem UI组件
export default observer(({ children }) => {
const field = useContext(FieldContext);
const decoratorProps = field.decoratorProps as any;
let style = {};
if (decoratorProps.style) {
style = decoratorProps.style;
}
return (
<div>
<div style={{ height: 20, ...style }}>{field.title}:</div>
{children}
<div style={{ height: 20, fontSize: 12, color: 'red' }}>
{field.errors.join(',')}
</div>
</div>
);
});
FormItem基本没变
10.3 Input
import { Field } from '@formily/core';
import React from 'react';
// Input UI组件
export default (props: Field) => {
return (
<input
{...props}
value={props.value || ''}
style={{
border: '2px solid rgb(186 203 255)',
borderRadius: 6,
width: '100%',
height: 28,
padding: '0 5px',
}}
/>
);
};
Input自定义组件也基本没变
10.4 主代码
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormProvider, MyField } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';
import { useMemo } from 'react';
import { createForm, Field, onFieldReact } from '@formily/core';
export default () => {
console.log('Top Render');
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldReact('nameLength', (field) => {
let field2 = field as Field;
field2.value = field2.query('.name').value()?.length;
});
},
});
}, []);
return (
<FormProvider form={form}>
<button
onClick={() => {
form.getFieldState('name', (state) => {
if (
state.componentProps?.placeholder?.indexOf(
'你是谁',
) != -1
) {
state.componentProps = { placeholder: '我是我' };
} else {
state.componentProps = { placeholder: '你是谁' };
}
});
}}
>
切换name组件的componentProps[placeholder]
</button>
<MyField
title="名称"
name="name"
required
component={[Input, {}]}
decorator={[FormItem]}
/>
<MyField
title="名称长度"
name="nameLength"
component={[Label]}
decorator={[FormItem]}
/>
<button
onClick={() => {
form.getFieldState('age', (state) => {
let decoratorProps = state.decorator[1] as any;
decoratorProps.style.height += 5;
});
}}
>
切换age组件的decoratorProps[style.height]
</button>
<MyField
title="年龄"
name="age"
required
component={[InputDigit, {}]}
decorator={[FormItem, { style: { height: 30 } }]}
/>
<button
onClick={() => {
form.getFieldState('password', (state) => {
let components = state.component as any;
if (components[0] === Password) {
state.component = [Input];
} else {
state.component = [Password];
}
});
}}
>
切换password组件的Component
</button>
<MyField
title="密码"
name="password"
required
component={[Password]}
decorator={[FormItem, {}]}
/>
</FormProvider>
);
};
整合了core库的主代码,可读性好得多了。
- 我们不再需要在头部声明表单的所有字段了,我们只需要在MyField组件里面声明我们用到的哪些属性就可以了。
- 完整使用core库的其他特性,丰富的生命周期,校验器,依赖追踪与联动,复杂结构的字段类型,灵活的path语法。
11 总结
一个设计良好的领域组件,值得学习。而且,另一方面,它可以看成是对MVVM架构的一种模范使用。在已经存在一个类似Reactive或者MobX组件的情况,如何进一步创建一个特定领域的通用ViewModel组件。
- 建立一个ViewModel,ViewModel里面包含了业务需要用到的所有渲染数据,例如表单中的component,decorator,validator,feedback等等。然后将这些数据包装起来放在一个observable,提供外部接口来访问这些数据,View层直接读取这个ViewModel的数据来渲染。(在core库中,这个大observable数据可以通过form.getFormGraph方法来获取)
- ViewModel提供了额外的生命周期(onMount,onInit,onUnmount,onSubmit,onFieldChange,onFieldReact),虚拟属性访问(实际是get与set方法),属性设置与获取的方法(setValuesIn,getValuesIn),公开的可调用方法(onInput,onFocus,onBlur)。这些方法的底层其实都在操控底层的obserable对象,同时嵌入业务固定的业务数据流。例如,onInput的调用总是会触发validator的检查,submit的调用总是会触发validator的检查,对visible设置为none总是会对value设置为undefined,对required这个虚拟属性设置为true,就会对validator属性添加一个required的校验器对象,等等。
就这样,这个特定领域的ViewModel就实现了,它的作用是:
- 对于底层的View,它需要实现的仅仅是对ViewModel属性的首次读取后渲染,以及当ViewModel属性倾听,当发生变化后重新触发View。另外,对用户输入的行为,调用固定的ViewModel方法,例如是onInput,onFoucs,onBlur。
- 对于顶层的Model,它仅仅需要以属性的方法来配置ViewModel的行为。对于那些业务固定流的行为,ViewModel默认就已经帮Model实现了,不需要再做那些重复的逻辑。
这个core库的想法真的很棒!
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!