React-联合组件

前言

本文讲的如何利用context,将多个组件串联起来,实现一个更大的联合组件。最具有这个特性的就是表单组件,所以本文例子就是一个表单组件。本文例子参考 Ant Design 。本次不讲 context 知识,需要的话等到下一次分享。

准备

或者直接使用本文 demo Gitee地址

基本代码

<Form onSubmit={(e, v) => {
  console.log(e, 'error');
  console.log(v, 'value');
}}>
  <Form.Item label={'手机号'}>
    <Form.Input name={'phone'} rules={[{validator: (e) => /^1[3-9]\d+$/.test(e), message: '手机号格式错误'}]}/>
  </Form.Item>
  <Form.Item label={'年龄'}>
    <Form.Input name={'age'} rules={[{validator: (e) => /^\d+$/.test(e), message: '只允许输入数字'}]}/>
  </Form.Item>
    <Form.Button>提交</Form.Button>
  <Form.Button type={'reset'}>重置</Form.Button>
</Form>

需求

  • 自定义校验规则
  • 表单内容组件不限组合方式
  • 点击提交按钮就可以提交
  • 提交时候可以校验值并且可以自动拦截,然后将错误信息下发给 FormItem 组件并且显示出来
  • 通过传入 Form 组件的 onSubmit 参数就可以获取到内容

实现

明白自己所需要的内容后,我们创建基本代码中的几个组件,Form , FormItem ,Input , 以及 Button。
具体内容看代码中的注释

Form

首先我们要知道 Form 组件在联合组件中的负责的内容

  • 数据收集
  • 数据校验
  • 提交、重置动作处理

代码如下

import React, {Component} from 'React';
import PropTypes from 'prop-types';
import {Item} from './Item';
import {Button} from './Button';
import {Input} from './Input';

export class Form extends Component{
  static propTypes = {
    onSubmit: PropTypes.func.isRequired, // 需要该参数因为,如果没有该参数,整个组件就没有意义
    defaultValues: PropTypes.object, // 如果有些需要默认参数的,就需要该参数
    children: PropTypes.any,
  };
  
  static defaultProps = {
    defaultValues: {},
  };

  static childContextTypes = {
    form: PropTypes.any, // 定义上下文参数名称和格式,格式太麻烦,直接any了或者 object也可以。
  };

  state = {
    validates: {},
    change: 0,
  };
  
  // 为什么不将数据全部放在 state 里面,在本文最后会讲到
  registerState = {
    form: {},
    rules: {},
    label: {},
  };

  getChildContext() {
    // 定义上下文返回内容
    const {validates} = this.state;
    const {form} = this.registerState;
    return {
      form: {
        submit: this.submit.bind(this),
        reset: this.reset.bind(this),
        register: this.register.bind(this),
        registerLabel: this.registerLabel.bind(this),
        setFieldValue: this.setFieldValue.bind(this),
        data: form,
        validates,
      },
    };
  }

  submit() {
    // 提交动作
    const {onSubmit} = this.props;
    if (onSubmit) {
      const validates = [];
      const {form, rules, label} = this.registerState;
      Object.keys(form).forEach(key => {
        const item = form[key];
        const itemRules = rules[key];

        itemRules.forEach(rule => {
          //To do something validator 简单列出几种基本校验方法,可自行添加
          let res = true;
          // 如果校验规则里面有基本规则时候,使用基本规则
          if (rule.hasOwnProperty('type')) {
            switch (rule) {
              case 'phone':
                /^1[3-9]\d+$/.test(item);
                res = false;
                break;
              default:
                break;
            }
          }
          // 如果校验规则里面有 校验函数时候,使用它
          if (rule.hasOwnProperty('validator')) {
            res = rule.validator(item);
          }
          // 校验不通过,向校验结果数组里面增加,并且结束本次校验
          if (!res) {
            validates.push({key, message: rule.message, label: label.hasOwnProperty(key) ? label[key] : ''});
            return false;
          }
        });
      });
        
      if (validates.length > 0) {
        // 在控制台打印出来
        validates.forEach(item => {
          console.warn(`item: ${item.label ? item.label : item.key}; message: ${item.message}`);
        });
        // 将错误信息返回到 state 并且由 context 向下文传递内容,例如 FormItem 收集到该信息,就可以显示出错误内容和样式
        this.setState({
          validates,
        });
      }
      // 最后触发 onSubmit 参数,将错误信息和数据返回
      onSubmit(validates, this.registerState.form);
    }
  }

  reset() {
    // 重置表单内容
    const {form} = this.registerState;
    const {defaultValues} = this.props;
    this.registerState.form = Object.keys(form).reduce((t, c) => {
      t[c] = defaultValues.hasOwnProperty(c) ? defaultValues[c] : '';
      return t;
    }, {});
    // 因为值不在 state 中,需要刷新一下state,完成值在 context 中的更新
    this.change();
  }
  
  //更新某一个值
  setFieldValue(name, value) {
    this.registerState.form[name] = value;
    this.change();
  }

  // 值和规则都不在state中,需要借助次方法更新内容
  change() {
    this.setState({
      change: this.state.change + 1,
    });
  }
  
  // 注册参数,最后数据收集和规则校验都是通过该方法向里面添加的内容完成
  register(name, itemRules) {
    if (this.registerFields.indexOf(name) === -1) {
      this.registerFields.push(name);
      const {defaultValues} = this.props;
      this.registerState.form[name] = defaultValues.hasOwnProperty(name) ? defaultValues[name] : '';
      this.registerState.rules[name] = itemRules;
    } else {
      // 重复的话提示错误
      console.warn(`\`${name}\` has repeat`);
    }
  }
  
  // 添加 字段名称,优化体验
  registerLabel(name, label) {
    this.registerState.label[name] = label;
  }

  render() {
    return (
      <div className="form">
        {this.props.children}
      </div>
    ); // 这里使用括号因为在 webStrom 下格式化代码后的格式看起来更舒服。
  }
}

// 将子组件加入到 Form 中 表示关联关系
Form.Item = Item;
Form.Button = Button;
Form.Input = Input;

FormItem

它的功能不多

  • 向 Form 中注册 输入框的关联名称
  • 从 Form 中获取 校验结果并且展示出来

代码如下

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export class Item extends Component {
  // 这个值在 FormItem 组件 被包裹在 Form 组件中时,必须有
  name;

  static propTypes = {
    label: PropTypes.string,
  };

  static childContextTypes = {
    formItem: PropTypes.any,
    children: PropTypes.any,
  };

  static contextTypes = {
    form: PropTypes.object,
  };

  // 防止重复覆盖 name 的值
  lock = false;

  // 获取到 包裹的输入组件的 name值,如果在存在 Form 中,则向 Form 注册name值相对的label值
  setName(name) {
    if (!this.lock) {
      this.lock = true;
      this.name = name;

      const {form} = this.context;
      if (form) {
        form.registerLabel(name, this.props.label);
      }
    } else {
      // 同样,一个 FormItem 只允许操作一个值
      console.warn('Allows only once `setName`');
    }

  }

  getChildContext() {
    return {
      formItem: {
        setName: this.setName.bind(this),
      },
    };
  }

  render() {
    const {label} = this.props;
    const {form} = this.context;

    let className = 'form-item';
    let help = false;
    if (form) {
      const error = form.validates.find(err => err.key === this.name);
      // 如果有找到属于自己错误,就修改状态
      if (error) {
        className += ' form-item-warning';
        help = error.message;
        return false;
      }
    }

    return (
        <div className={className}>
          <div className="label">
            {label}
          </div>
          <div className="input">
            {this.props.children}
          </div>
          {help ? (
              <div className="help">
                {help}
              </div>
          ) : ''}
        </div>
    );
  }
}

Input

暂时演示输入组件为 Input ,后面可以按照该组件内容,继续增加其他操作组件
该类型组件负责的东西很多

  • 唯一name,通知 FormItem 它所包裹的是谁
  • Form 组件里面,收集的数据
  • 校验规则

代码如下

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export class Input extends Component {
  constructor(props, context) {
    super(props);
    // 如果在 Form 中,或者在 FormItem 中,name值为必填
    if ((context.form || context.formItem) && !props.name) {
      throw new Error('You should set the `name` props');
    }
    // 如果在 Form 中,不在 FormItem 中,提示一下,不在 FormItem 中不影响最后的值
    if (context.form && !context.formItem) {
      console.warn('Maybe used `Input` in `FormItem` can be better');
    }

    // 在 FormItem 中,就要通知它自己是谁
    if (context.formItem) {
      context.formItem.setName(props.name);
    }
    // 在 Form 中,就向 Form 注册自己的 name 和 校验规则
    if (context.form) {
      context.form.register(props.name, props.rules);
    }
  }

  shouldComponentUpdate(nextProps) {
    const {form} = this.context;
    const {name} = this.props;
    // 当 有 onChange 事件 或者外部使用组件,强行更改了 Input 值,就需要通知 Form 更新值
    if (form && this.changeLock && form.data[name] !== nextProps.value) {
      form.setFieldValue(name, nextProps.value);
      return false;
    }
    return true;
  }

  static propTypes = {
    name: PropTypes.string,
    value: PropTypes.string,
    onChange: PropTypes.func,
    rules: PropTypes.arrayOf(PropTypes.shape({
      type: PropTypes.oneOf(['phone']),
      validator: PropTypes.func,
      message: PropTypes.string.isRequired,
    })),
    type: PropTypes.oneOf(['text', 'tel', 'number', 'color', 'date']),
  };

  static defaultProps = {
    value: '',
    rules: [],
  };

  static contextTypes = {
    form: PropTypes.object,
    formItem: PropTypes.object,
  };

  onChange(e) {
    const val = e.currentTarget.value;
    const {onChange, name} = this.props;
    const {form} = this.context;
    if (onChange) {
      this.changeLock = true;
      onChange(val);
    } else {
      if (form) {
        form.setFieldValue(name, val);
      }
    }
  }

  render() {
    let {value, name, type} = this.props;
    const {form} = this.context;
    if (form) {
      value = form.data[name] || '';
    }
    return (
        <input onChange={this.onChange.bind(this)} type={type} value={value}/>
    );
  }
}

Button

负责内容很简单

  • 提交,触发 submit
  • 重置,触发 reset

代码如下

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export class Button extends Component {

  componentWillMount() {
    const {form} = this.context;
    // 该组件只能用于 Form
    if (!form) {
      throw new Error('You should used `FormButton` in the `Form`');
    }
  }

  static propTypes = {
    children: PropTypes.any,
    type: PropTypes.oneOf(['submit', 'reset']),
  };

  static defaultProps = {
    type: 'submit',
  };

  static contextTypes = {
    form: PropTypes.any,
  };

  onClick() {
    const {form} = this.context;
    const {type} = this.props;
    if (type === 'reset') {
      form.reset();
    } else {
      form.submit();
    }
  }

  render() {
    return (
        <button onClick={this.onClick.bind(this)} className={'form-button'}>
          {this.props.children}
        </button>
    );
  }
}

后言

首先先讲明为何 不将label 和数据不放在state 里面因为多个组件同时注册时候,state更新来不及,会导致部分值初始化不成功,所以最后将值收集在 另外的 object 里面,并且是直接赋值
看了上面几个组件的代码,应该有所明确,这些组件组合起来使用就是一个大的组件。同时又可以单独使用,知道该如何使用后,又可以按照规则,更新整个各个组件,而不会说,一个巨大无比的单独组件,无法拆分,累赘又复杂。通过联合组件,可以达成很多奇妙的组合方式。上文的例子中,如果没有 Form 组件, 单独的 FormInput 加 Input,这两个组合起来,也可以是一个单独的验证器。

    原文作者:inight
    原文地址: https://segmentfault.com/a/1190000014851189
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞