小而美的 React Form 组件
背景之间在一篇介绍过 Table 组件《React 实现一个漂亮的 Table》 的文章中讲到过,在企业级后台产品中,用的最多且复杂的组件主要包括 Table、 Form、 Chart,在处理 Table 的时候我们遇到了很多问题。今天我们这篇文章主要是分享一下 Form 组件,在业务开发中, 相对 Table 来说,Form 处理起来更麻烦,不是所有表单都像注册页面那样简单,它往往需要处理非常多的逻辑,比如:
Form 作为一个功能型组件,它需要解决的问题无非就是两个:
大家可以通过以上图片看到,我想输入自己的中文名字都不能正常输入,这里主要存在两个问题:
我在想这两个问题不在组件上处理,在哪里处理的呢?访问地址: https://ant.design/components... 可以试一下,也希望他们可以解决掉这个问题。这个问题应该怎么解决,我之前做过记录: React 中,在 Controlled(受控制)的文本框中输入中文 onChange 会触发多次。 我们在设计的时候自然是解决了这些问题,预览效果: https://rsuitejs.com/form-lib/ ,接下来看一下具体的设计。 设计针对前面提到 Form 需要解决的两个问题(数据校验和数据获取),在设计的时候,我们把这个两个问题作为两个功能,独立在不同的库处理。
这两个库可以独立使用,如果你有自己一套自己的 Form 组件,只是缺少一个数据验证的工具,那你可以单独把 Form 定义一个表单分别看一下它们是怎么工作的,
看一个示例: 安装首先需要安装 npm i form-lib --save 示例代码import { Form,Field,createFormControl } from 'form-lib'; const SelectField = createFormControl('select'); const user = { name:'root',status:1 }; <Form data={user}> <Field name="name" /> <Field name="status" accepter={SelectField} > <option value={1}>启用</option> <option value={0}>禁用</option> </Field> </Form> 在默认情况下 Field 是一个文本输入组件,如果你需要使用 HTML 表单中其他的标签,你可以像上面示例一样 通过 自定义布局这里存在一个疑问, <Form data={user}> <div className="row"> <Field name="name" /> </div> <div className="row"> <Field name="status" accepter={SelectField} > <option value={1}>启用</option> <option value={0}>禁用</option> </Field> </div> </Form> Form Props
Schema 定义一个数据模型安装npm i rsuite-schema --save 在
示例代码一个示例: const userModel = SchemaModel( username: StringType().isRequired('用户名不能为空'),email: StringType().isEmail('请输入正确的邮箱'),age: NumberType('年龄应该是一个数字').range(18,30,'年应该在 18 到 30 岁') }); 这里定义了一个 const checkResult = userModel.check({ username: 'foobar',email: 'foo@bar.com',age: 40 }) // checkResult 结果: /** { username: { hasError: false },email: { hasError: false },age: { hasError: true,errorMessage: '年应该在 18 到 30 岁' } } **/ 多重验证StringType() .minLength(6,'不能少于 6 个字符') .maxLength(30,'不能大于 30 个字符') .isRequired('该字段不能为空'); 自定义验证通过 addRule 函数自定义一个规则。 const myModel = SchemaModel({ field1: StringType().addRule((value) => { return /^[1-9][0-9]{3}s?[a-zA-Z]{2}$/.test(value); },'请输入合法字符'),field2: StringType().pattern(/^[1-9][0-9]{3}s?[a-zA-Z]{2}$/,'请输入合法字符') }); 自定义动态错误信息例如,要通过值的不同情况,返回不同的错误信息,参考以下 const myModel = SchemaModel({ field1: StringType().addRule((value) => { if(value==='root'){ return { hasError: true,errorMessage:'不能是关键字 root' } }else if(!/^[a-zA-Z]+$/.test(value)){ return { hasError: true,errorMessage:'只能是英文字符' } } return { hasError: false } }) }); 复杂结构数据验证const userModel = SchemaModel({ username:StringType().isEmail('正确的邮箱地址').isRequired('该字段不能为空'),tag: ArrayType().of(StringType().rangeLength(6,'字符个数只能在 6 - 30 之间')),profile: ObjectType().shape({ email: StringType().isEmail('应该是一个 email'),age: NumberType().min(18,'年龄应该大于18岁') }) })
Form 与 Schema 的结合const userModel = SchemaModel({ username: StringType().isRequired('用户名不能为空'),'年应该在 18 到 30 岁') }); <Form model={userModel}> <Field name="username" /> <Field name="email" /> <Field name="age" /> </Form> 把定义的的 以上的示例代码是不完整的,没有处理错误信息和获取数据,只是为了方便大家理解。完整的示例,可以参考接下来的实践与解决方案。 实践与解决方案一个完整的示例import React from 'react'; import { Form,createFormControl } from 'form-lib'; import { SchemaModel,StringType } from 'rsuite-schema'; const TextareaField = createFormControl('textarea'); const SelectField = createFormControl('select'); const model = SchemaModel({ name: StringType().isEmail('请输入正确的邮箱') }); class DefaultForm extends React.Component { constructor(props) { super(props); this.state = { values: { name: 'abc',status: 0 },errors: {} }; this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit() { const { values } = this.state; if (!this.form.check()) { console.error('数据格式有错误'); return; } console.log(values,'提交数据'); } render() { const { errors,values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { console.log(values); this.setState({ values }); // 清除表单所有的错误信息 this.form.cleanErrors(); }} onCheck={(errors) => { this.setState({ errors }); }} values={values} model={model} > <div className="form-group"> <label>邮箱: </label> <Field name="name" className="form-control" /> <span className="help-block error" style={{ color: '#ff0000' }}> {errors.name} </span> </div> <div className="form-group"> <label>状态: </label> <Field name="status" className="form-control" accepter={SelectField} > <option value={1}>启用</option> <option value={0}>禁用</option> </Field> </div> <div className="form-group"> <label>描述 </label> <Field name="description" className="form-control" accepter={TextareaField} /> </div> <button onClick={this.handleSubmit}> 提交 </button> </Form> </div> ); } } export default DefaultForm; 在 rsuite 中的应用在 通过上一个例子中我们可以看到,没有个 import React from 'react'; import { Form,StringType,ArrayType } from 'rsuite-schema'; import { FormControl,Button,FormGroup,ControlLabel,HelpBlock,CheckboxGroup,Checkbox } from 'rsuite'; const model = SchemaModel({ name: StringType().isEmail('请输入正确的邮箱'),skills: ArrayType().minLength(1,'至少应该会一个技能') }); const CustomField = ({ name,label,accepter,error,...props }) => ( <FormGroup className={error ? 'has-error' : ''}> <ControlLabel>{label} </ControlLabel> <Field name={name} accepter={accepter} {...props} /> <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock> </FormGroup> ); class DefaultForm extends React.Component { constructor(props) { super(props); this.state = { values: { name: 'abc',skills: [2,3],gender: 0,errors: {} }; this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit() { const { values } = this.state; if (!this.form.check()) { console.error('数据格式有错误'); return; } console.log(values,'提交数据'); } render() { const { errors,values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { this.setState({ values }); console.log(values); }} onCheck={errors => this.setState({ errors })} defaultValues={values} model={model} > <CustomField name="name" label="邮箱" accepter={FormControl} error={errors.name} /> <CustomField name="status" label="状态" accepter={FormControl} error={errors.status} componentClass="select" > <option value={1}>启用</option> <option value={0}>禁用</option> </CustomField> <CustomField name="skills" label="技能" accepter={CheckboxGroup} error={errors.skills} > <Checkbox value={1}>Node.js</Checkbox> <Checkbox value={2}>Javascript</Checkbox> <Checkbox value={3}>CSS 3</Checkbox> </CustomField> <CustomField name="gender" label="性别" accepter={RadioGroup} error={errors.gender} > <Radio value={0}>男</Radio> <Radio value={1}>女</Radio> <Radio value={2}>未知</Radio> </CustomField> <CustomField name="bio" label="简介" accepter={FormControl} componentClass="textarea" error={errors.bio} /> <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button> </Form> </div> ); } } export default DefaultForm; 自定义 Field如果一个组件不是原生表单控件,也不是 RSuite 库中提供的基础组件,要在 form-lib 中使用,应该怎么处理呢?
接下来我们使用 rsuite-selectpicker 作为示例,在 rsuite-selectpicker 内部已经实现了这些 API。 import React from 'react'; import { SchemaModel,NumberType } from 'rsuite-schema'; import { Button,HelpBlock } from 'rsuite'; import Selectpicker from 'rsuite-selectpicker'; import { Form,Field } from 'form-lib'; const model = SchemaModel({ skill: NumberType().isRequired('该字段不能为空') }); const CustomField = ({ name,...props }) => ( <FormGroup className={error ? 'has-error' : ''}> <ControlLabel>{label} </ControlLabel> <Field name={name} accepter={accepter} {...props} /> <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock> </FormGroup> ); class CustomFieldForm extends React.Component { constructor(props) { super(props); this.state = { values: { skill: 3,},values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { this.setState({ values }); console.log(values); }} onCheck={errors => this.setState({ errors })} defaultValues={values} model={model} > <CustomField name="skill" label="技能" accepter={Selectpicker} error={errors.skill} data={[ { label: 'Node.js',value: 1 },{ label: 'CSS3',value: 2 },{ label: 'Javascript',value: 3 },{ label: 'HTML5',value: 4 } ]} /> <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button> </Form> </div> ); } } export default CustomFieldForm; 更多示例:参考 如果你在使用中存在任何问题,可以提交 issues,如果你有什么好的想法欢迎你 pull request,GitHub地址:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |