react 项目实战(四)组件化表单/表单控件 高阶组件 高阶组件:formProvider 表单控件组件

高阶组件就是返回组件的组件(函数)

为什么要通过一个组件去返回另一个组件?

使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能。

我们现在已经有了带有表单校验功能的添加用户的表单,这里的表单有3个字段:name、age、gender,并且每个字段都有它自己的校验规则和对应的错误信息。

要做一个添加图书的功能,图书的表单有name、price、owner_id三个字段,一样地,每个字段有它自己的校验规则和错误信息。

仔细想想,每当我们需要写一个表单的时候,都需要有一个地方来保存表单字段的值(state),有一个函数来处理表单值的更新和校验(handleValueChange),这些东西我们可以用高阶组件来封装。

而添加用户的表单和添加图书的表单之间的不同之处仅仅是表单字段以及字段的默认值、校验规则和错误信息

那么我们的高阶组件模型就出来了:

function formProvider (fields) {
  return function (Comp) {
    constructor (props) {
      super(props);
      this.state = {
        form: {...},
        formValid: false // 加了一个formValid用来保存整个表单的校验状态
      };
    }
    handleValueChange (field, value) {...}
    class FormComponent extends React.Component {
      render () {
        const {form, formValid} = this.state;
        return (
          <Comp {...this.props} form={form} formValid={formValid} onFormChange={this.handleValueChange}/>
        );
      }
    }

    return FormComponent;
  }
}

formProvider接收一个fields参数,并返回一个函数,这个函数接收一个组件作为参数并返回一个组件,所以它的用法是这样的:

UserAdd = formProvider(fields)(UserAdd);

经过formProvider处理后的UserAdd组件会得到额外的props:

  • form
  • formValid
  • onFormChange

/src下新建一个目录utils,新建formProvider.js文件,写入具体的代码实现:

/**
 * 高阶组件 formProvider
 * 返回组件的组件(函数)
 * 使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能
 */
import React from 'react';

function formProvider (fields) { // fields 对象
  return function(Comp) { // Comp
    /**
     * 定义常量
     * 初始表单状态
     */
    const initialFormState = {};
    // 循环
    for(const key in fields){
      initialFormState[key] = {
        value: fields[key].defaultValue,
        error: ''
      };
    }

    // 创建组件
    class FormComponent extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始状态
        this.state = {
          form: initialFormState,
          formValid: false // 加了一个formValid用来保存整个表单的校验状态
        };
        // 绑定this
        this.handleValueChange = this.handleValueChange.bind(this);
      }
      // 输入框改变事件
      handleValueChange(fieldName, value){
        // 定义常量
        const { form } = this.state;

        const newFieldState = {value, valid: true, error: ''};

        const fieldRules = fields[fieldName].rules;
        // 循环
        for(let i=0; i<fieldRules.length; i++){
          const {pattern, error} = fieldRules[i];
          let valid = false;
          if(typeof pattern === 'function'){
            valid = pattern(value);
          }else{
            valid = pattern.test(value);
          }

          if(!valid){
            newFieldState.valid = false;
            newFieldState.error = error;
            break;
          }
        }
        /**
         * ... 扩展运算符
         * 将一个数组转为用逗号分隔的参数序列
         */
        const newForm = {...form, [fieldName]: newFieldState};
        /**
         * every
         * 对数组中的每个元素都执行一次指定的函数,直到此函数返回 false
         * 如果发现这个元素,every 将返回 false
         * 如果回调函数对每个元素执行后都返回 true,every 将返回 true
         */
        const formValid = Object.values(newForm).every(f => f.valid);
        // 设置状态
        this.setState({
          form: newForm,
          formValid
        });
      }
      render(){
        const { form, formValid } = this.state;
        return (
          <Comp
            {...this.props}
            form={form}
            formValid={formValid}
            onFormChange={this.handleValueChange} />
        );
      }
    }
    // 返回组件
    return FormComponent;
  }
}

export default formProvider;

formProvider的第一个参数fields是一个对象,其结构为:

// 表示表单中有name、age、gender3个字段
const fields = {
  name: {
    defaultValue: '',
    rules: [
      {
        // pattern用于对值进行校验,可以为方法或一个RegExp对象
        // 若方法的返回值为一个真值或RegExp.test(value)返回true则校验通过
        pattern: function (value) {
          return value.length > 0;
        },
        // 每个pattern对应一个error信息
        error: '请输入用户名'
      },
      {
        pattern: /^.{1,4}$/,
        error: '用户名最多4个字符'
      }
    ]
  },
  age: {...},
  gender: {...}
}

然后UserAdd.js就可以改成这个样子了:

import React from 'react';
// 高阶组件 formProvider表单验证
import formProvider from '../utils/formProvider';

// 添加用户组件
class UserAdd extends React.Component {
  // 按钮提交事件
  handleSubmit(e){
    // 阻止表单submit事件自动跳转页面的动作
    e.preventDefault();
    // 定义常量
    const { form: { name, age, gender }, formValid} = this.props; // 组件传值
    // 验证
    if(!formValid){
      alert('请填写正确的信息后重试');
      return;
    }
    // 发送请求
    fetch('http://localhost:8000/user', {
      method: 'post',
      // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
      body: JSON.stringify({
        name: name.value,
        age: age.value,
        gender: gender.value
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    // 强制回调的数据格式为json
    .then((res) => res.json())
    // 成功的回调
    .then((res) => {
      // 当添加成功时,返回的json对象中应包含一个有效的id字段
      // 所以可以使用res.id来判断添加是否成功
      if(res.id){
        alert('添加用户成功!');
      }else{
        alert('添加用户失败!');
      }
    })
    // 失败的回调
    .catch((err) => console.error(err));
  }
  
  render() {
    // 定义常量
    const {form: {name, age, gender}, onFormChange} = this.props;
    return (
      <div>
        <header>
          <div>添加用户</div>
        </header>

        <main>
          <form onSubmit={(e) => this.handleSubmit(e)}>
            <label>用户名:</label>
            <input
              type="text"
              value={name.value}
              onChange={(e) => onFormChange('name', e.target.value)} />
            {!name.valid && <span>{name.error}</span>}
            <br />
            <label>年龄:</label>
            <input
              type="number"
              value={age.value || ''}
              onChange={(e) => onFormChange('age', e.target.value)} />
            {!age.valid && <span>{age.error}</span>}
            <br />
            <label>性别:</label>
            <select
              value={gender.value}
              onChange={(e) => onFormChange('gender', e.target.value)}>
              <option value="">请选择</option>
              <option value="male">男</option>
              <option value="female">女</option>
            </select>
            {!gender.valid && <span>{gender.error}</span>}
            <br />
            <br />
            <input type="submit" value="提交" />
          </form>
        </main>
      </div>
    );
  }
}

// 实例化
UserAdd = formProvider({ // field 对象
  // 姓名
  name: {
    defaultValue: '',
    rules: [
      {
        pattern: function (value) {
          return value.length > 0;
        },
        error: '请输入用户名'
      },
      {
        pattern: /^.{1,4}$/,
        error: '用户名最多4个字符'
      }
    ]
  },
  // 年龄
  age: {
    defaultValue: 0,
    rules: [
      {
        pattern: function(value){
          return value >= 1 && value <= 100;
        },
        error: '请输入1~100的年龄'
      }
    ]
  },
  // 性别
  gender: {
    defaultValue: '',
    rules: [
      {
        pattern: function(value) {
          return !!value;
        },
        error: '请选择性别'
      }
    ]
  }
})(UserAdd);

export default UserAdd;

表单控件组件

上面我们抽离了表单的状态的维护和更新逻辑,但这并不够完美。

UserAdd.js里的render方法中,我们可以看到还存在着一些重复的代码:

...
<label>用户名:</label>
<input
  type="text"
  value={name.value}
  onChange={(e) => onFormChange('name', e.target.value)}
/>
{!name.valid && <span>{name.error}</span>}
<br/>
<label>年龄:</label>
<input
  type="number"
  value={age.value || ''}
  onChange={(e) => onFormChange('age', +e.target.value)}
/>
{!age.valid && <span>{age.error}</span>}
<br/>
<label>性别:</label>
<select
  value={gender.value}
  onChange={(e) => onFormChange('gender', e.target.value)}
>
  <option value="">请选择</option>
  <option value="male">男</option>
  <option value="female">女</option>
</select>
{!gender.valid && <span>{gender.error}</span>}
<br/>
...

每一个表单控件都包含一个label、一个具体的控件元素、一个根据valid来控制显示的span元素。

我们可以将其封装成一个FormItem组件,新建/src/components目录和FormItem.js文件,写入以下代码:

import React from 'react';

class FormItem extends React.Component {
  render () {
    const {label, children, valid, error} = this.props;
    return (
      <div>
        <label>{label}</label>
        {children}
        {!valid && <span>{error}</span>}
      </div>
    );
  }
}

export default FormItem;

UserAdd.js中使用FormItem组件:

import React from 'react';
import FormItem from '../components/FormItem';
// 高阶组件 formProvider表单验证
import formProvider from '../utils/formProvider';

// 添加用户组件
class UserAdd extends React.Component {
  // 按钮提交事件
  handleSubmit(e){
    // 阻止表单submit事件自动跳转页面的动作
    e.preventDefault();
    // 定义常量
    const { form: { name, age, gender }, formValid} = this.props; // 组件传值
    // 验证
    if(!formValid){
      alert('请填写正确的信息后重试');
      return;
    }
    // 发送请求
    fetch('http://localhost:8000/user', {
      method: 'post',
      // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
      body: JSON.stringify({
        name: name.value,
        age: age.value,
        gender: gender.value
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    // 强制回调的数据格式为json
    .then((res) => res.json())
    // 成功的回调
    .then((res) => {
      // 当添加成功时,返回的json对象中应包含一个有效的id字段
      // 所以可以使用res.id来判断添加是否成功
      if(res.id){
        alert('添加用户成功!');
      }else{
        alert('添加用户失败!');
      }
    })
    // 失败的回调
    .catch((err) => console.error(err));
  }
  
  render() {
    // 定义常量
    const {form: {name, age, gender}, onFormChange} = this.props;
    return (
      <div>
        <header>
          <div>添加用户</div>
        </header>

        <main>
          <form onSubmit={(e) => this.handleSubmit(e)}>
            <FormItem label="用户名:" valid={name.valid} error={name.error}>
              <input
                type="text"
                value={name.value}
                onChange={(e) => onFormChange('name', e.target.value)}/>
            </FormItem>

            <FormItem label="年龄:" valid={age.valid} error={age.error}>
              <input
                type="number"
                value={age.value || ''}
                onChange={(e) => onFormChange('age', e.target.value)}/>
            </FormItem>

            <FormItem label="性别:" valid={gender.valid} error={gender.error}>
              <select
                value={gender.value}
                onChange={(e) => onFormChange('gender', e.target.value)}>
                <option value="">请选择</option>
                <option value="male">男</option>
                <option value="female">女</option>
              </select>
            </FormItem>
            <br />
            <input type="submit" value="提交" />
          </form>
        </main>
      </div>
    );
  }
}

// 实例化
UserAdd = formProvider({ // field 对象
  // 姓名
  name: {
    defaultValue: '',
    rules: [
      {
        pattern: function (value) {
          return value.length > 0;
        },
        error: '请输入用户名'
      },
      {
        pattern: /^.{1,4}$/,
        error: '用户名最多4个字符'
      }
    ]
  },
  // 年龄
  age: {
    defaultValue: 0,
    rules: [
      {
        pattern: function(value){
          return value >= 1 && value <= 100;
        },
        error: '请输入1~100的年龄'
      }
    ]
  },
  // 性别
  gender: {
    defaultValue: '',
    rules: [
      {
        pattern: function(value) {
          return !!value;
        },
        error: '请选择性别'
      }
    ]
  }
})(UserAdd);

export default UserAdd;

项目结构:

react 项目实战(四)组件化表单/表单控件  高阶组件
高阶组件:formProvider
表单控件组件