React-聯合組件

前言

本文講的如何利用context,將多個組件串聯起來,實現一個更大的聯合組件。最具備這個特性的就是表單組件,因此本文例子就是一個表單組件。本文例子參考 Ant Design 。本次不講 context 知識,須要的話等到下一次分享。html

準備

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

基本代碼

<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。
具體內容看代碼中的註釋git

Form

首先咱們要知道 Form 組件在聯合組件中的負責的內容es6

  • 數據收集
  • 數據校驗
  • 提交、重置動做處理

代碼以下web

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

它的功能很少npm

  • 向 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 ,後面能夠按照該組件內容,繼續增長其餘操做組件
該類型組件負責的東西不少app

  • 惟一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,這兩個組合起來,也能夠是一個單獨的驗證器。

相關文章
相關標籤/搜索