Happy to build the forms in React ^_^
react-formutil 定义了一种表单状态的收集、分发、同步模型。基于此,你可以很方便的使用 react-formutil 来创建、管理你的页面表单。
- 一切都是状态,$value、$diry/$pristine、$touched/$untouched、$valid/$invalid、$error 等都是状态
- 非侵入性,只提供了对表单状态收集的抽象接口,不渲染任何 dom 结构
- 采用受控组件和 context,对组件嵌套层级没有限制,支持数据双向同步(
model<->view)- 支持高阶组件和函数式子组件(function as child)式调用,更灵活
- 具备灵活的表单校验方式,支持同步和异步校验
- 规范的 jsx 语法调用,更符合 react 理念
- 对流行的 react 组件库做了适配优化,现已支持:
ant-designmaterial-uireact-bootstrapreact-md
- 安装 Installation
- 使用 Usage
<Field />rendercomponentname$defaultValue$defaultState$validators$asyncValidators$onFieldChange$state$value$dirty | $pristine | $touched | $untouched | $invalid | $valid | $focused | $pending$error$picker()$reset()$getComponent()$setState($newState)$render() | $setValue()$setDirty($dirty) | $setTouched($touched) | $setFocused($focused) | $setValidity(errKey, result)$setError($error)$validate()$getFirstError()$$formutil
withField(Component)<EasyField /><Form />render|component$defaultValues$defaultStates$onFormChange$new()$getField(name)$validate(name)$validates()$render(callback)$setStates($stateTree)$setValues($valueTree)$setErrors($errorTree)$reset($stateTree)$setDirts($dirtyTree) | $setTouches($touchedTree) | $setFocuses($focusedTree)$batchState($newState) | $batchDirty($dirty) | $batchTouched($touched) | $batchFocused($focused)$getFirstError()$states | $weakStates$params | $weakParams$errors | $weakErrors$dirts | $weakDirts$touches | $weakTouches$focuses | $weakFocuses$valid | $invalid$dirty | $pristine$touched | $untouched$focused
withForm(Component)connect(Component)
- FAQ & 常见问题解答
# npm npm install react-formutil --save # yarn yarn add react-formutil了解如何在
ant-design、Material-UI等流行 react 组件库项目中使用 react-formutil?
先看一个简单的示例:
如果上方地址无法访问或者较慢,也可以查看:Demo on github pages
上面的示例简单展示了 react-formutil 的基本用法。当然这只是很简单的示例,更复杂的状态渲染,例如$dirty、表单验证等后面会具体讲到。这里想简单说下 react-formutil 的设计思路:
react-formutil 主要提供了一个 Field 组件和一个 Form 组件,另外还有几个基于此的高阶组件:
Field组件主要用来负责和具体的表单控件做状态的同步,并向顶层的Form注册自身。虽然它是一个标准的 react 组件,但是可以把它理解成单个表单控件的 Provider。Form组件通过context提供了一些方法给Field组件,并且它增强了传递过来的子组件,向其传递了整个表单的状态。Form 可以理解为整个表单页面的 Provider。withField是基于Field包装成高阶组件,方便习惯高阶方式的调用withForm是基于Form包装成高阶组件,方便习惯高阶方式的调用EasyField是基于Field进行的组件封装,方便直接调用浏览器原生控件去生成表单(可以参考 demo 中的例子)connect是个高阶组件,用来给被包装的组件传递$formutil对象,以供调用,返回的新组件必须位于某个 Form 组件的孙子辈才可以拿到$formutil
react-formutil 不像很多你能看到的其它的 react 表单库,它是非侵入性的。即它并不要求、也并不会强制渲染某种固定的 dom 结构。它只需要提供 name 值以及绑定好 $render 用来更新输入值,然后一切就会自动同步、更新。
需要强调,当使用 Field 和 Form 时,我们建议以函数作为子节点方式调用: function as child
当然,你也可以通过
render属性来调用:render props也可以传递
component来指定直接渲染一个组件。
//一个函数式子组件书写示例 <Form> {$formutil => { return <Field name="username">{props => <input />}</Field>; }} </Form> //或者使用children属性 <Form children={$formutil => <Field name="username" children={props => <input />} />} /> //或者使用render属性 <Form render={$formutil => <Field name="username" render={props => <input />} />} /> //或者使用component属性 <Form component={MyForm} /> //当然也可以传递普通组件作为子节点 //Field组件写在loginForm这个组件中 <Form> <LoginForm /> </Form>对于
<Form /><Field /><EasyField />三个组件,其相关属性的优先级为:
component>render>children
Field 是一个标准的 react 组件。它可以理解为表单控件的顶层组件,它可以同步表单控件的状态。每一个表单控件应该总是当作 Field 组件的子组件嵌套。
Field 可以以函数、或者 React 组件当作子组件调用,推荐使用函数。
Field 可以接收以下几个属性参数:
这两个属性为可选,并且不能同时存在(component 会优先于 render,而将其覆盖)。
当使用function as child方式时,可以不传该属性。
如果设置了该属性,则其会覆盖掉function as child方式。
<Field name="username" render={$fieldutil => <input />} /> // 或 <Field name="username" component={MyField} />该项必填,name 可以是一个简单的字符串,也可以是一个字符串表达式(该表达式执行没有 scope, 所以表达式中不能存在变量)
<Field name="username" /><Field name="list[0]" /><Field name="list[1].name" /><Field name="list[2]['test' + 124]" />
以上都是合法的 name 值。对于多层级的 name 值,生成的表单参数对象,也会基于该对象层级创建。例如,上面的示例,将会生成以下格式的表单参数对象:
{ "username": "", "list": ["", { "name": "" }, { "test124": "" }] }该属性可以设置表单控件的默认值/初始值。如过不传递该参数,则默认值都为空字符串。通过该属性,你可以指定某个表单控件的默认值或初始值。
<Field $defaultValue="username" /><Field $defaultValue={{name: 'dog'}} />
$defaultValue 可以是任意类型值。
该属性可以覆盖表单控件的的默认状态,类型必需是key: value简单对象:
<Field $defaultState={{ $value: 'username' }} /> <Field $defaultValue="username" />上面两者等效,其实表单控件的值只是状态里的一个字段$value
该属性可以设置表单控件的校验方式,同时支持同步和异步校验。它是 key: value 的对象形式,key 为校验类型标识,value 为校验函数。仅当校验函数返回 true 时,表示该项校验通过,否则其他值将会被当作错误信息保存到状态中。
异步校验:如果校验函数返回一个
promise对象,则resolved表示校验通过,rejected则校验不通过,同时rejected返回的reason将会被当作错误信息保存到$error对象中。
异步校验时,状态里会有
$pending用来表示正在异步校验。
特别注意: 仅仅设置了
$validators,并不会触发校验,还需要设置匹配$validators中每一项的属性标识符,该属性的值会作为第二个参数传递给校验函数。
校验被调用,会传入三个值:value、attr、props
value为当前 Field 的值attr为校验标识值props为当前传给 Field 的所有 props,还包括当前 Field 所属 Fom 的$formutil
<Field required maxLength="5" disableChar="z" asyncCheck $validators={{ required: value => !!value || '该项必填', maxLength: (value, len) => value.length <= parseInt(len) || '最少长度:' + len, disableChar: (value, char) => value.indexOf(char) === -1 || '禁止输入字符:' + char, /* 注意:下面这条规则将不会触发校验,因为我们没有给Field传递 minNumber 属性来表示需要去校验该条规则 */ minNumber: (value, limit) => value > parseFloat(limit) || '输入值必需大于:' + limit, /* 异步校验 */ asyncCheck: value => axios.post('/api/v1/check_account', { account: value }).catch(error => Promise.reject(error.message)) }}> {$fieldutil => ( <div className="form-group"> <label>密码</label> <input type="number" onChange={ev => $fieldutil.$render(ev.target.value.trim())} value={$fieldutil.$value} /> {$fieldutil.$invalid && <div className="error">{object.values($fieldutil.$error)[0]}</div>} </div> )} </Field>在这个例子中,我们通过$validators 设置了 required 、 maxLength 以及 disabledChar 的校验规则。同时通过属性 props 表示了需要校验这三个字段。然后我们可以通过状态判断将错误信息展示出来。
当然,也可以只在一个校验函数里校验多个规则,甚至混合异步校验:
<Field baseCheck $validators={{ baseCheck(value) { //校验非空 if (!value) { return '该项必填'; } //校验输入长度 if (value.length < 5) { return '最小输入五个字符'; } //异步校验 return axios .post('/api/v1/check_account', { account: value }) .catch(error => Promise.reject(error.message)); } }} />
v0.2.22起,建议直接使用$validators即可,$validators也支持了异步校验。不建议单独使用$asyncValidators。
该属性可以设置表单项的异步校验规则,设置方式与$validators类似。但是不同的是,异步校验函数需要返回promise对象,该promise被resolve表示校验成功,reject表示校验失败,并且reject的reason会被当作失败原因保存到状态的$error对象。
异步校验时,状态里会有$pending用来表示正在异步校验。
由于 react 的渲染是异步的,所以如果存在交叉验证,例如 A 控件依赖于 B 控件的值去校验自身,那么这种情况下,B 的值变更并不会导致 A 立即去应用新的值去校验。所以这种情况下,可以通过该属性设置回调,主动去触发校验 A 控件。
注意:
- 该回调并不会在调用
$render$setValues等更新表单值的方法后立即触发,它会随着最新的一次 react 渲染执行。也正因为此,所以才能拿到变更后的表单的值和状态。- 仅当当前
Field的值(状态里的$value)有变动时才会触发,其他状态例如$diry$touched等变化不会触发。- 如果需要访问 DOM Event,请使用
onChange即可。- 不要在该回调里再次修改当前 Field 的值,否则会陷入死循环(修改该 Field 的其它状态或者修改其它 Field 的值是安全的)。
//在B的值变更并且渲染完毕后,主动再次要求A组件进行一次校验 <Field name="B" $onFieldChange={(newValue, preValue) => $formutil.$getField('A').$validate()}> //... </Field>Field 会维护一个状态树,以及一些方法,并且会将状态和方法合并一起通过参数或者组件传递给 Field 的子组件:
{ $value: "", //表单值 $dirty: false, //是否修改过表单项 $pristine: true, //与$dirty相反 $touched: false, //是否接触过表单 $untouched: true, //与$touched相反 $focused: false, //是否聚焦到当前输入 $valid: true, //表单项校验结果是否通过 $invalid: false, //与$valid相反 $error: {}, //表单校验错误信息 $pending: false, //异步校验时该值将为true /*** 上面是状态,下面是可用方法 ***/ $pickr: () => $state, //返回当前状态树 $reset: ($newState) => $state, //重置为初始状态, $newState存在的话,会做一个合并 $getComponent: (name) => FieldComponent, //返回Field组件实例 $render: (value, callback) => {}, //更新表单值,callback可选,会在组件更新后回调 $setValue: value => {}, //同$render,只是个别名 $setDirty: $dirty => {}, //设置$dirty装态 $setTouched: $touched => {}, //设置$touched装态 $setFocused: $focused => {}, //设置$focused装态 $setState: $newState => {} //直接更新状态,其实上面的几个方法都是基于$setState $setValidity: ($key, $valid) => {} //设置校验, $valid为true代表校验通过,其它值表示校验失败,并当作错误原因 $setError: ($error) => {} //直接设置错误状态 $validate: () => {} //触发再次校验该对象会传递给子组件,子组件可以利用其中的方法来同步、修改表单状态:
- 用户输入时需要通过调用
$render来更新新值到状态中 - 渲染表单项时,应该使用受控组件,根据
$value来渲染 - 错误信息和校验状态可以通过
$dirty$invalid$error来渲染
需要强调的是,Field 默认不同步
$touched/$untouched、$focused状态,只有$dirty/$pristine会自动同步(首次调用$render会自动同步$dirty状态) 如果你需要其它状态,需要自己去绑定相关事件来更新状态:
<Field name="username"> {$fieldutil => ( <input onChange={ev => $fieldutil.$render(ev.target.value)} onFocus={ev => $fieldutil.$setFocused(true)} onBlur={ev => $fieldutil.$setTouched(true) && $fieldutil.$setFocused(false)} /> )} </Field>更多解释
Field 的值实际是保存在状态里的该字段中,
Field 的一组状态:
- $dirty 控件被修改过
- $pristine 控件没有被修改过,与$dirty 互斥
- $touched 控件失去过焦点
- $untouched 控件没有失去过焦点
- $focused 焦点是否在当前控件
- $pending 是否正在进行异步检查
- $valid 表单所有控件均校验通过
- $invalid 表单中有至少一个控件校验不通过
Field 的错误信息
返回 Field 的纯粹状态(不包含任何下方的方法)
重制当前 Field 为初始状态
获取 Field 的实例对象(虚拟 dom)
$setState({ $dirty: true, $value: '124' });设置新的$state,$newState 会与当前$state 合并
设置渲染 Field 的值(保存到$value 中)
$setDirty(true); $setTouched(true); $setFocused(true); $setValidity('required', '必需填写'); //第二个参数不为true,则表示校验失败,并当作错误描述 $setValidity('required', true); //表示校验通过设置$dirty $touched $error 等状态
替换$error
$setError({ required: '必需填写', maxLength: '不能超过10个字符' });重新校验当前 Field
获取该 Field 的错误项中的首个错误描述
<Field> {$fieldutil => ( <div> <input value={$fieldutil.$value} onChange={ev => $fieldutil.$render(ev.target.value)} /> {$fieldutil.$invalid && <p className="error">{$fieldutil.$getFirstError()}</p>} </div> )} </Field>当前 Field 所属的 Form 的$formutil 对象包含了整个表单的状态以及一些操作方法,具体可以参考下方 Form 说明。
特别注意,这里
$$formutil是双$符号打头
<Field name="username"> { $fieldutil => <input onChange={ev => $fieldutil.$render(ev.target.value)} onFocus={ev => $fieldutil.$$formutil.$validates()} /> </Field>特别注意:v0.4.0 版本起,
withField将会把状态和方法都放到$fieldutil对象中传递给被装饰的组件!!这与之前的方式有所区别,请留意。
withField 是一个高阶组件,与 Field 的区别是调用方式的不同。withField 的第二个参数为可选配置,如过定义了该参数,会将配置传递给 Field 组件。一般情况下建议通过 Field 组件去构造表单。如果你需要自定义一个复杂的表单项控件,则可以使用该高阶组件:
import React from 'react'; import { withField } from 'react-formutil'; class FieldCustom extends React.Component { onChange = ev => this.props.$fieldutil.$render(ev.target.value); render() { return <input onChange={this.onChange} value={this.props.$fieldutil.$value} />; } } export default withField(FieldCustom, { $defaultValue: '' //该项将传递给Field组件 });withField同样支持装饰器语法
@withField class MyField extends Component {} //or pass some default props @withField({ $defaultValue: '123' }) class MyField extends Component {}EasyField 是对Field的二次封装,向下提供了 onChange onFocus onBlur 三个方法用来同步值的变动以及相关$dirty $touched等状态。
并且也也内置了一些常用的校验方法,例如:
required必填,如果是 group.checkbox,则必需至少选中一项requiredmaxLength。最大输入长度,支持 group.checkbox。有效输入时才会校验maxLength="100"minLength最小输入长度,支持 group.checkbox。有效输入时才会校验minLength="10"max最大输入数值,仅支持 Number 比较。有效输入时才会校验max="100"min最小输入数值,仅支持 Number 比较。有效输入时才会校验min="10"pattern正则匹配。有效输入时才会校验pattern={/^\d+$/}enum枚举值检测。有效输入时才会校验enum={[1,2,3]}checker自定义校验函数。checker={value => value > 10 && value < 100 || '输入比如大于10小与100'}
注:校验属性的值为
null时表示不进行该校验
小技巧:你可以利用checker很便捷的完成自定义校验,不需要validMessage $validators:
<EasyField checker={value => { if (!value) { return 'Required!'; } if (value.length < 6) { return 'minlength: 6'; } return true; // no error }}它接收以下属性参数:
当设置了 type 时,EasyField 将会尝试直接渲染浏览器表单元素。它支持以下类型:
input[type=text]input[type=number]input[type=search]input[type=password]input[type=checkbox]input[type=radio]selecttextareagroup.radiogroup.checkbox
EasyField 对亚洲语言(中文、韩文、日文)输入法在输入过程中的的字母合成做了处理
一些调用示例:
事实上 type 值只要不是 selct textarea checkbox radio group.xxx 时都是渲染普通 input 输入框,并且 type 值会传给该 input。
<EasyField name="name" type="text" /> <EasyField name="pwd" type="password" /> <EasyField name="email" type="email" /> <EasyField name="search" type="search" /> <EasyField name="number" type="number" /> <EasyField name="comment" type="textarea" cols="8" rows="10" />下拉列表可以将后选项当作子节点直接传递就行,就像普通的 select 标签一样!
<EasyField name="age" type="select"> <option value="20">20</option> <option value="30">30</option> </EasyField>单选/多选还可以传递 checked、unchekced 属性,用来覆盖选中/未选中状态下所对应的值
<EasyField name="agree" checked="yes" unchecked="no" type="checkbox" /> <EasyField name="agree" type="raido" />当 type 值为 group.xxx 为渲染输入控件组,当前仅支持group.checkbox group.radio。它会向函数式子节点传递 GroupOption 属性,用来渲染单个后选项。每个后选项的值通过 $value 属性指定。
此时支持额外的属性groupNode,默认为'div',渲染一个空的 div 标签。react@16以上版本可以设置groupNode={null}来禁止渲染空的 div 节点
<EasyField type="group.checkbox" name="targets" required validMessage={{ required: '请至少选择一项' }}> {({ GroupOption }) => this.targets.map(item => ( <label key={item.id} className="checkbox-inline"> <GroupOption $value={item.id} className="checkbox" /> {item.name} </label> )) } </EasyField>当 type 属性没有指定时,会根据这三个属性来进行渲染,并且将 EasyField 定义的同步回调方法(onChange onFocus onBlur)和当前值(value)传递下去。
也支持浏览器原生控件
普通文本输入
<EasyField name="username"> <input type="text" /> </EasyField> <EasyField name="pwd"> <input type="password" placeholder="Password" /> </EasyField>渲染复选框
<EasyField name="username" valuePropName="checked"> <input type="chekcbox" /> </EasyField> /* 自定义复选框对应的值,等同于:<EasyField type="checkbox" checked="yes" unchecked="no" /> */ <EasyField name="username"> {({ onChange, value }) => <input type="checkbox" checked={value === 'yes'} onChange={ev => onChange(ev.target.checked ? 'yes' : 'no')} />} </EasyField>上面例子中渲染复选框最后一种示例,就是使用了自定义组件渲染。更多场景是和第三方输入组件进行交互:
与 ant-design 进行交互:
import { Input, Switch } from 'antd'; <EasyField name="username"> <Input /> </EasyField>; <EasyField name="switch" $defaultValue={true}> <Switch /> </EasyField>;与 react-select 进行交互:
import Select from 'react-select'; <EasyField name="react-select" $defaultValue={undefined}> <Select options={options} /> </EasyField>;同Field的name
同Field的$defaultValue
同Field的$validators。它会与内置的校验方法进行合并后,可以覆盖同名的默认校验方法。
v0.2.22起,建议直接使用$validators即可,$validators也支持了异步校验。不建议单独使用$asyncValidators。
在输入值更新到 state 中时会传递给 $parser 处理函数。默认为 value => value
在 state 中的值渲染给表单控件时会传递给 $formatter 处理函数。默认为 value => value
<EasyField name="age" $parser={value => Number(value)} $formatter={value => String(value)} />注意,这个是省略前面的$符号。如果与$defaultValue同时存在,则会被后者覆盖。
可以通过该属性,设置内置的校验方法的错误信息展示:
<EasyField name="useraname" required maxLength="10" validMessage={{ required: '必需填写', maxLength: '最多输入十个字符' }} />如果是 checkbox 或 radio,则可以设置该属性,表示选中/未选中所代表的值。默认为 true 和 false。
//这里可以设置选中、未选中用yes和no表示 <label> <EasyField type="checkbox" name="remember" checked="yes" unchecked="no" /> 是否同意用户协议 </label>当不设置 type 属性,而使用自定义渲染时,如果组件的值以及值变动触发的更新回调方法不是默认的 value、onChange、onFocus、onBlur,可以通过这些参数更改:
function MyComponent({ current, onUpdate }) { return <button onClick={() => onUpdate(124)}>更新</button>; } <label> <EasyField component={MyComponent} valuePropName="current" changePropName="onUpdate" /> 是否同意用户协议 </label>;但使用自定义组件时,如果需要访问当前 Field 的状态,可以通过设置该参数,传入一个字符串,EasyField 会将状态通过该参数值传递给自定义组件:
<EasyField name="custom" passUtil="$fieldutil"> {({ $fieldutil, onChange, value }) => { return <input className={$fieldutil.$invalid ? 'has-error' : ''} onChange={onChange} value={value} />; }} </EasyField>Form 也是一个标准的 react 组件,它类似 Field,同样可以以函数、或者普通组件当作子组件调用。它可以增强子组件,收集子 dom 树中的 Field 组件状态,并通过$formutil 传递给被调用组件。
经过 Form 增强的组件,会在其 props 中接收到一个$formutil对象。例如
- 你可以通过
$formutil.$params拿到整个表单的输入值 - 你可以通过
$formutil.$invalid或$formutil.$valid来判断表单是否有误 - 你可以通过
$formutil.$errors来获取表单的错误输入信息
Form 可以接收以下可选属性参数:
该属性为可选,当使用function as child方式时,可以不传该属性。如果设置了该属性,则其会覆盖掉function as child方式。
如果render 和 component 同时存在,则后者会覆盖前者。
<Form render={$formutil => {/* ... */} />} /> <Form component={MyForm} />$defaultValues 可以通过这里批量设置表单的默认值,格式为 { name: value }(如果设置对应的值,会覆盖 Field 中的 defautlValue 设置)
<Form $defaultValues={{ username: 'qiqiboy' }}> {$formutil => ( /* const { $params, $invalid, $errors, ...others } = $formutil; */ <div> <Field name="username">{props => <input />}</Field> <Field name="password">{props => <input />}</Field> </div> )} </Form>$defaultStates 可以通过这里批量设置表单的默认状态,格式为 { name: $state }(如果设置对应的值,会覆盖 Field 中的 defautlValue 设置)
<Form $defaultStates={{ username: { $dirty: true } }}> {$formutil => ( /* const { $params, $invalid, $errors, ...others } = $formutil; */ <div> <Field name="username">{props => <input />}</Field> <Field name="password">{props => <input />}</Field> </div> )} </Form>该属性可传入一个函数,当表单的值有变动时,会触发该回调,新的$formutil 对象和本次变动的新旧值的集合会依次以参数形式传递:
注意:
- 该回调不是随用户修改同步触发,它随 react 的最新的一次渲染完成触发。
- 请避免在该回调里不加条件的一直去变更表单项的值,否则可能陷入死循环(因为表单值变更即会导致该回调重新触发)。
<Form $onFormChange={($formutil, newValues, preValues) => console.log($formutil, newValues, preValues)}>//...</Form>; //当表单值有变更时,将会打印: //$formutil { $params: {}, $states: {}, $invalid: false, $valid: true, //... $setStates: () => {}, $getField: () => {}, //... } //newValues { username: 'new value'; } //preValues { username: 'pre value'; }$formutil的属性方法详解:
获取最新的表单$formutil。这里可能会产生一个疑问:为什么已经拿到了$formutil,还要再通过$new()再获取一次呢?
这是因为$formutil是随着渲染,每次都时时生成的新对象,即 react 组件的前后两次渲染,拿到的$formutil其实都是所属渲染帧的快照!
当使用withForm高阶组件时,我们如果通过this.props.$formutil来访问,都是安全的,因为最新的$formutil都会通过组件的props传递过去。
但是,当我们通过render props方式(即通过Form的 render、children 属性传递渲染函数),异步回调里获取的上下文中$fomutil则可能是之前的某个快照,并不是最新的,所以你获得的表单状态和值都可能是不正确的。
错误的调用
<Form> {$formutil => { const onChange = ev => { // 延迟2s执行 setTimeout(() => { const { $invalid, $params } = $formutil; // 这里的$formutil来自于回调函数所在作用域上下文中的$formutil // 它是`onChange`事件触发时的最后一次渲染的快照 // 如果`onChange`触发,到延迟2s回调函数执行,表单又有变化的话,那么这里拿到的$formutil有可能就是和最新的表单状态不一致 }, 2000); }; return <EasyField name="user" onChange={onChange} required />; }} </Form>正确的用法
<Form> {$formutil => { const onChange = ev => { // 延迟2s执行 setTimeout(() => { const { $invalid, $params } = $formutil.$new(); // 注意,这里通过 $formutil.$new() 获取即时的最新的 $formutil,这样子是绝对安全的用法。 // 如果不确定该不该用 $formutil.$new(),那么请记住,总是使用$new()总是没错的! // ... }, 2000); }; return <EasyField name="user" onChange={onChange} required />; }} </Form>获取对 name 对应的表单项的操作引用,可以获取到包含以下方法的对象:
const { $picker(){}, //返回当前$state $validate(){}, //重新校验 $reset($state){}, //重置表单项状态 $getComponent(){}, //获取Field组件的引用 $setState, $setValue, $setDirty,$setTouched,$setValidity,$setError } = $formutil.$getField('list[0].name'); //name支持表达式字符串立即校验对应 name 的表单项
重新校验所有的表单项
强制重新渲染表单组件,可以通过该方法的回调,在当前的渲染完成后回调
可以用来更新表单项的状态:
$formutil.$setStates({ username: { $dirty: true, $pristine: false }, 'list[0].name': { //也可以像下方一样传入结构化对象 $dirty: true, $pristine: false }, list: [ { name: { $dirty: true, $pristine: false } } ] });可以用来更新表单项的值:
$formutil.$setValues({ username: 'jack', 'list[0].id': '123456', //也可以像下方一样传入结构化对象 list: [ { id: '123456' } ] });可以用来设置表单的校验结果:
$formutil.$setErrors({ username: { required: '必填' }, 'list[0].id': {} //代表校验通过 });可以用来重置表单,会讲表单重置为初始状态(不会改变组件设置的默认状态和默认值)。如过传递了$stateTree,则会重置为合并了$stateTree 后的状态
$formutil.$reset();可以用来更新表单控件的$dirty、$touched、$focused状态,类似$setValues
$formutil.$setDirts({ username: true, 'list[0].id': false }); $formutil.$setFocuses({ username: true, 'list[0].id': false });批量更改所有表单项的状态
$formutil.$batchState({ $dirty: true, $pristine: false }); $formutil.$batchDirty(true); //同上效果 $formutil.$batchTouched(true);从表单的所有错误项中取出第一个错误描述
$formutil.$getFirstError(); //例如 const { $invalid, $getFirstError } = this.props.$formutil; if ($invalid) { alert($getFirstError()); } else { // ...submit data }所有表单项的状态集合。$formutl.$state 是以 Fieldi 的 name 值经过路径解析后的对象,$formutil.$weakState 是以 Field 的 name 字符串当 key 的对象。
所有表单项的 值$value 集合。$formutil.$params 是以 Field 的 name 值经过路径解析后的对象,$formutil.$weakParams 是以 Field 的 name 字符串当 key 的对象。
$params = { username: 'qiqiboy', list: [{ name: 'apple' }, { name: 'banana' }] }; $weakParams = { username: 'qiqiboy', 'list[0].name': 'apple', 'list[1].name': 'banana' };所有表单项的 $error 集合。$formutil.$errors 是以 Field 的 name 值经过路径解析后的对象,$formutil.$weakErrors 是以 Field 的 name 字符串当 key 的对象。
$errors = { username: { required: '必填' }, list: [ { name: { required: '必填' } }, { name: { required: '必填' } } ] }; $weakErrors = { username: { required: '必填' }, 'list[0].name': { required: '必填' }, 'list[1].name': { required: '必填' } };所有表单项的 $dirty 集合。$formutil.$dirts 是以 Field 的 name 值经过路径解析后的对象,$formutil.$weakDirts 是以 Field 的 name 字符串当 key 的对象。
所有表单项的 $touched 集合。$formutil.$touches 是以 Field 的 name 值经过路径解析后的对象,$formutil.$weakTouches 是以 Field 的 name 字符串当 key 的对象。
所有表单项的 $focused 集合。$formutil.$focuses 是以 Field 的 name 值经过路径解析后的对象,$formutil.$weakFocuses 是以 Field 的 name 字符串当 key 的对象。
表单项中所有 Field 的$valid 均为 true 时,$formutil.$valid 为 true, $formutil.$invalid 为 false。表单项中有任意 Field 的$valid 为 false 时,$formutil.$valid 为 false, $formutil.$invalid 为 True。
表单项中所有 Field 的$dirty 均为 false 时,$formutil.$dirty 为 false, $formutil.$pristine 为 true。表单项中有任意 Field 的$dirty 为 true 时,$formutil.$dirty 为 true, $formutil.$pristine 为 false。
表单项中所有 Field 的$touched 均为 false 时,$formutil.$touched 为 false, $formutil.$untouched 为 true。表单项中有任意 Field 的$touched 为 true 时,$formutil.$touched 为 true, $formutil.$untouched 为 false。
表单项中所有 Field 的$focused 均为 false 时,$formutil.$focused 为 false。表单项中有任意 Field 的$focused 为 true 时,$formutil.$focused 为 true。
withForm 是基于 Form 封装的高阶组件,withForm 的第二个参数为可选配置,如过定义了该参数,会将配置传递给 Form 组件。
class LoginForm extends Component { // ... } export default withForm(LoginForm, { $defaultValues: {} //该项将传递给Form组件 });withForm同样支持装饰器语法
@withForm class MyField extends Component {} //or pass some default props @withForm({ $defaultValues: {} }) class MyField extends Component {}connect 是一个高阶组件,它可以增强当前组件,并获取其最近的父辈级中的 Form 组件的 $formutil 对象,并以 props 传递给当前组件。
在大表单拆分多个小组件的时候很有用,不用将$formutil 再传来传去:
import { connect } from 'react-formutil'; class Submit extends Component { submit = () => { //通过connect可以拿到 $formutil const { $formutil } = this.props; // ... }; render() { return <button onClick={this.submit} />; } } export default connect(Submit);<Form> <div className=""> <EasyField name="username" /> <Submit /> </div> </Form>Field 是抽象的底层,它本身不会渲染任何 dom 结构出来,它仅提供了同步、渲染表单控件的接口。要实现具体的表单,需要通过 Field,使用它提供的接口,手动实现监听用户输入、同步数据等工作(例如不会主动同步$touched $focused 状态)
EasyField 则是基于 Field 封装的另一个组件,它针对浏览器原生的表单控件,封装实现了数据同步、表单校验,可以简化调用。EasyField 会自动绑定 change、focus、blur 事件,并主动同步$touched $untouched $focused状态
可以直接 Field 实现,也可以使用 EasyField 实现(demo 都中有示例):
const hobbiesItems = [ { id: 'music', name: '音乐' }, { id: 'movie', name: '电影' }, { id: 'ps4', name: 'ps4' } ]; <EasyField name="hobbies" type="group.checkbox"> {props => ( <div> {hobbies.map(item => ( <label className="checkbox-inline" key={item.id}> {/* props.GroupOption是每个候选项对应的input[checkbox],必须渲染出来,并传递 $value */} <props.GroupOption $value={item.id} /> {item.name} </label> ))} </div> )} </EasyField>;假如我们需要在表单中插入一个按钮,用户需要点击按钮上传图片后,将图片地址同步到表单中
import React from 'react'; import { Field } from 'react-formutil'; import uploadFile from './uplaodFile'; //上传文件的方法 //定义我们自己的表单控件组件 export default function FieldFile(props) { return ( <Field {...props}> {$props => { const selectFile = function() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.onchange = function() { /* get file &upload */ const files = fileInput.files; uploadFile(files).then( fileUrl => { //将文件地址更新到Field的状态中 $props.$render(fileUrl); }, error => { alert('upload fail'); } ); }; fileInput.click(); }; return ( <div className="upload-image"> {$props.$value && <img src={$props.$value} className="preview" />} <button onClick={selectFile}>{$props.$value ? '更改图片' : '上传图片'}</button> </div> ); }} </Field> ); } /* ---------------------- 使用 -------------------- */ <div className="form-group"> <label>点击上传头像</label> <FieldFile name="avatar" /> </div>;可以通过 $getField 获取到一组 handler 方法,其中有 $getComponent 方法,可以获取到组件对象,然后再通过 react-dom 提供的 findDOMNode 来获取到对应的实际 dom 元素节点
import { findDOMNode } from 'react-dom'; <Form> {$formutil => { function getNode(name) { return findDOMNode($formutil.$getField(name).$getComponent()); } return <Field name="username">{/*...*/}</Field>; }} </Form>;对于一个具有很多表单项、导致页面很大的表单,如果全部在一个组件里维护,会比较痛苦。幸运的事,使用 react-formutl 你可以很方便将大表单拆分成多个模块,既能减小大组件带来的维护难题,还能复用表单模块。
比如同时要收集用户的个人信息和工作信息,我们可以将其拆分为三个模块:
Userinfo.js用户基本信心的字段Workinfo.js用户工作信息的字段Submit.js提交区域(因为只有在 Form 组件下级才能拿到$formutil 信息)
注: Submit.js 和 Workinfo.js 合并到一起也是可以的。
// Userinfo.js import React from 'react'; import { EasyField } from 'react-formutil'; export default function Userinfo({ $formutil }) { //可以从props中获取$formutil return ( <div className="userinfo-form"> <h3>基本信息</h3> <EasyField name="name" placeholder="姓名" /> <EasyField name="age" placeholder="年龄" /> <EasyField name="sex" placeholder="性别" /> <EasyField name="phone" placeholder="手机" /> </div> ); }// Workinfo.js import React from 'react'; import { EasyField } from 'react-formutil'; export default function Workinfo({ $formutil }) { //可以从props中获取$formutil return ( <div className="workinfo-form"> <h3>工作信息</h3> <EasyField name="company" placeholder="公司名称" /> <EasyField name="job" placeholder="行业" /> <EasyField name="work_address" placeholder="公司地址" /> </div> ); }//Submit.js export default function Submit({ $formutil }) { //可以从props中获取$formutil const postData = () => { const { $params, $invalid, $erros } = $formutil; // ... 更多处理 }; return ( <div className="submit-area"> <button disabled={$formutil.$invlid} onClick={postData}> 提交 </button> </div> ); }// EditInfoPage.js import React from 'react'; import Userinfo from './Userinfo'; import Workinfo from './Workinfo'; import Submit from './Submit'; import { Form } from 'react-formutl'; export default function EditInfoPage() { //可以直接将拆分的模块以子组件放置在<Form />组件下(直接子组件,不可嵌套其它组件,否则可以使用下方的写法) return ( <div className="editinfo-page"> <Form> <Userinfo /> <Workinfo /> <Submit /> </Form> </div> ); /* 与下方写法等效 */ return ( <Form> {({ $formutil }) => ( <div className="editinfo-page"> <Userinfo $formutil={$formutil} /> <Workinfo $formutil={$formutil} /> <Submit $formutil={$formutil} /> </div> )} </Form> ); /* 也可以使用 connect 高阶组件包装分拆的组件,然后就不必显式的传 $formutil */ /** * import { connect } from 'react-formutil' * class Submit extends Component { * submit = () => { * //通过connect可以拿到 $formutil * const { $formutil } = this.props; * // ... * }; * * render() { * return <button onClick={this.submit} />; * } * } * export default connect(Submit); */ return ( <Form> <div className="editinfo-page"> <Userinfo /> <Workinfo /> <Submit /> </div> </Form> ); }在ant-design或Material-UI项目中使用 react-formutil 也非常简单,以 ant-design 为例:
import React, { Component } from 'react'; import { EasyField, Field, withForm } from 'react-formutil'; import { Form, Input, Checkbox, DatePicker, Button } from 'antd'; //@decorator @withForm class MyForm extends Component { onSubmit = ev => { ev.preventDefault(); const { $invalid } = this.props.$formutil; if ($invalid) { // some error } else { // submit data } }; render() { return ( <Form onSubmit={this.onSubmit}> {/* use <Field /> */} <Field name="title"> {props => <Input value={props.value} onChange={ev => props.$render(ev.target.value)} />} </Field> {/* use <EasyField />, not need to sync data by set 'onChange' manual */} <EasyField name="username"> <Input placeholder="Username" /> </EasyField> <EasyField name="password"> <Input type="password" placeholder="Password" /> </EasyField> <EasyField name="remeber" $defaultValue={true} valuePropName="checked"> <Checkbox>remember me</Checkbox> </EasyField> {/* use <Form.Item /> */} <Form.Item label="Date"> <EasyField name="date" $defaultValue={null}> <DatePicker /> </EasyField> </Form.Item> <Button block type="primary"> Submit </Button> </Form> ); } }是的,你可以使用Field来手动绑定 onChange 来同步数据,也可以直接使用EasyField嵌套 antd 的组件即可,EasyField会自动绑定好相关数据同步。
为了更便捷的在各大流程组件库项目中使用react-formutil,我们也提供了针对各个组件库的优化封装组件:
你可以点击上方链接来了解更多。
如果你还觉得有其它优秀的组件库也需要提供针对性的组件优化,也可以提 issues。
react-formutil@0.3.0 起提供了针对typescript的DefinitionTypes声明文件,在开发中可能会用到的主要是以下几个:
$Formutil<Field, Validators, WeakFields>整个表单的 $formtutil 类型声明$Fieldutil<T, Validators, Fields, WeakFields>单个表单项的 $fieldutil 类型声明Field<T, Validators, Fields, WeakFields>Field 组件的类型声明EasyField<T, Validators, Fields, WeakFields>EasyField 组件的类型声明Form<Fields, Validators, WeakFields>EasyField 组件的类型声明
除了以上列出的,还有很多其它的类型定义,可以自行查看类型声明文件。
T是指值类型;Validators是指表单的校验项结构;Fields是指表单的参数域结构;WeakFields是指扁平的Fields结构,默认等同于Fields。如果你的表单不使用深层结构,那么只需要提供Fields即可。
let IErrors: Validators = { required: true, maxLength: string }
let fields: Fields = { user: { name: string, age: number }, price: number }
let weakFields: WeakFields = { 'user.name': string, 'user.age': number, price: number }
import React, { Component } from 'react'; import { withForm, EasyField, $Formutil } from 'react-formutil'; // 定义整个表单的参数结构 interface IFields { name: string; age: number; } // 定义整个表单的校验结构 interface IErrors { required: string; max: string; } // 定义表单组件的props // 因为我们使用了withForm高阶组件,所以我们需要声明$formutil这个对象 // 并且通过给 $Formutil 传递泛型参数,来明确整个$formutil对象中可以获取的表单相关结构信息 interface IProps { $formutil: $Formutil<IFields, IErrors>; } // @ts-ignore @withForm class UserForm extends Component<IProps> { componentDidMount() { // 可以调用$formutil对象 this.props.$formutil.$setValues({ name: 'xiao hong' }); // 甚至可以访问错误信息结构 console.log(this.props.$formutil.$errors.age.required); } render() { return ( <form> {/* 这里类似上面声明IProps时传递了泛型参数,如果我们需要在EasyField属性配置中访问其对象信息,也需要提供泛型参数定义 */} <EasyField<string, IErrors, IFields> name="name" $onFieldChange={(newValue, oldValue, $formutil) => { // 可以正常访问$formutil对象 }} /> {/* 这里我们定义该项的值为number类型,所以在渲染该值是需要做类型转换 */} <Field<number> name="age"> { $fieldutil => { // console.log($fieldutil.$value) return <input onChange={ev => $fieldutil.$render(Number(ev.target.value))} value={$fieldutil.$value} /> } } </Field> </form> ); } }