wubocong/react-upload-file

View on GitHub
src/index.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * Created by Warrior! on 2016/10/18.
 */

import React, { PropTypes, Component } from 'react';
import ReactDOM from 'react-dom';

export default class ReactUploadFile extends Component {
  static propTypes = {
    options: PropTypes.shape({
      /* basics*/
      baseUrl: PropTypes.string.isRequired,
      query: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
      body: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
      dataType: PropTypes.string,
      timeout: PropTypes.number,
      numberLimit: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
      fileFieldName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
      withCredentials: PropTypes.bool,
      requestHeaders: PropTypes.object,
      accept: PropTypes.string,
      multiple: PropTypes.bool,
      userAgent: PropTypes.string,
      /* funcs*/
      beforeChoose: PropTypes.func,
      didChoose: PropTypes.func,
      beforeUpload: PropTypes.func,
      didUpload: PropTypes.func,
      uploading: PropTypes.func,
      uploadSuccess: PropTypes.func,
      uploadError: PropTypes.func,
      uploadFail: PropTypes.func,
      onAbort: PropTypes.func,
    }).isRequired,
    style: PropTypes.object,
    className: PropTypes.string,
    /* buttons*/
    chooseFileButton: PropTypes.element.isRequired,
    uploadFileButton: PropTypes.element,
  };

  static defaultProps = {
    /* buttons*/
    chooseFileButton: <button>Select File</button>,
  };

  constructor(props) {
    super(props);
    const emptyFunction = f => f
    const options = {
      dataType: 'json',
      timeout: 0,
      numberLimit: 0,
      userAgent: window ? window.navigator.userAgent : '',
      multiple: false,
      withCredentials: false,
      beforeChoose: emptyFunction,
      didChoose: emptyFunction,
      beforeUpload: emptyFunction,
      didUpload: emptyFunction,
      uploading: emptyFunction,
      uploadSuccess: emptyFunction,
      uploadError: emptyFunction,
      uploadFail: emptyFunction,
      onAbort: emptyFunction,
      ...props.options,
    };
    const timeout = parseInt(options.timeout, 10);
    options.timeout = (Number.isInteger(timeout) && timeout > 0) ? timeout : 0;
    const dataType = options.dataType && options.dataType.toLowerCase();
    options.dataType = dataType !== 'json' && 'text';

    /* copy options to instance */
    Object.keys(options).forEach((key) => {
      this[key] = options[key];
    });
  }

  state = {
    /* xhrs' list after start uploading files */
    xhrList: [],
    currentXHRId: 0,
  };

  componentDidMount = () => {
    this.input = ReactDOM.findDOMNode(this).querySelector('[name=ajax-upload-file-input]');
  }

  /* trigger input's click*/
  /* trigger beforeChoose*/
  commonChooseFile = (e) => {
    if (e.target.name !== 'ajax-upload-file-input') {
      const jud = this.beforeChoose();
      if (jud !== true && jud !== undefined) return;
      this.input.click();
    }
  };

  /* input change event with File API */
  /* trigger chooseFile */
  commonChangeFile = (e) => {
    this.files = this.input.files;
    this.didChoose(this.files);

    /* immediately upload files */
    if (!this.props.uploadFileButton) {
      this.commonUploadFile(e);
    }
  };

  /* execute upload */
  commonUploadFile = (e) => {
    if (!this.files || !this.files.length) return false;
    if (!this.baseUrl) {
      throw new Error('baseUrl missed in options');
    }

    const jud = e === true ? true : this.beforeUpload(this.files);
    if (!jud) {
      return false;
    }

    let formData = new FormData();
    formData = this.appendFieldsToFormData(formData);

    const fieldNameType = typeof this.fileFieldName;
    const numberLimit = this.numberLimit === 0 ? this.files.length : Math.min(this.files.length, this.numberLimit);
    for (let i = numberLimit - 1; i >= 0; i--) {
      if (fieldNameType === 'function') {
        const file = this.files[i];
        const fileFieldName = this.fileFieldName(file);
        formData.append(fileFieldName, file);
      } else if (fieldNameType === 'string') {
        const file = this.files[i];
        formData.append(this.fileFieldName, file);
      } else {
        const file = this.files[i];
        formData.append(file.name, file);
      }
    }

    let baseUrl = this.baseUrl;
    /* url query*/
    const query = typeof this.query === 'function' ? this.query(this.files) : this.query;
    const pos = baseUrl.indexOf('?');
    let queryStr;
    if (pos > -1) {
      queryStr = baseUrl.substring(pos);
      baseUrl = baseUrl.substring(0, pos);
    }
    if (query) {
      if (queryStr) {
        console.warn('Your url contains query string, which will be ignored when options.query is set.');
      }
      const queryArr = [];
      Object.keys(query).forEach(key => queryArr.push(`${key}=${query[key]}`)
      );
      queryStr = `?${queryArr.join('&')}`;
    }
    queryStr = queryStr || '';
    const targetUrl = `${baseUrl}${queryStr}`;

    const xhr = new XMLHttpRequest();
    xhr.open('post', targetUrl, true);

    /* authorization info for cross-domain */
    xhr.withCredentials = this.withCredentials;

    const rh = this.requestHeaders;
    if (rh) {
      Object.keys(rh).forEach(key => xhr.setRequestHeader(key, rh[key]));
    }

    if (this.timeout) {
      xhr.timeout = this.timeout;
      xhr.addEventListener('timeout', () => {
        this.uploadError({
          type: '408',
          message: 'Request Timeout',
        });
      });
      setTimeout(() => {
      }, this.timeout);
    }

    xhr.addEventListener('load', () => {
      this.input.value = '';
      const res = this.dataType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseText;
      this.uploadSuccess(res);
    });

    xhr.addEventListener('error', () => {
      const err = this.dataType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseText;
      this.uploadError({
        type: err.type,
        message: err.message,
      });
    });

    xhr.addEventListener('progress', (progress) => {
      this.uploading(progress);
    });

    const curId = this.state.xhrList.length - 1;
    xhr.addEventListener('abort', () => {
      this.onAbort(curId);
    });

    xhr.send(formData);

    this.setState({
      currentXHRId: curId,
      xhrList: [...this.state.xhrList, xhr]
    });

    /* trigger didUpload */
    this.didUpload(this.files, this.state.currentXHRId);

    return true;
  }

  /* append text params to formData */
  appendFieldsToFormData = (formData) => {
    const field = typeof this.body === 'function' ? this.body() : this.body;
    if (field) {
      Object.keys(field).forEach((index) => {
        formData.append(index, field[index]);
      });
    }
    return formData;
  }

  /**
   * public method. Manually process files
   * @param func(files)
   * @return files
   * Filelist:
   * {
   *   0: file,
   *   1: file,
   *   length: 2
   * }
   */
  processFile = (func) => {
    this.files = func(this.files);
  }

  /* public method. Manually trigger commonChooseFile for debug */
  manuallyChooseFile = (e) => {
    this.commonChooseFile(e);
  }

  /* public method. Manually trigger commonUploadFile to upload files */
  manuallyUploadFile = (files) => {
    this.files = files && files.length ? files : this.files;
    this.commonUploadFile(true);
  }

  /* public method. Abort a xhr by id which didUpload has returned, default the last one */
  abort = (id) => {
    if (id) {
      this.state.xhrList[id].abort();
    } else {
      this.state.xhrList[this.state.currentXHRId].abort();
    }
  }

  render() {
    const inputProps = {
      accept: this.props.options.accept,
      multiple: this.props.options.multiple
    };
    const chooseFileButton = React.cloneElement(this.props.chooseFileButton, {
      onClick: this.commonChooseFile
    }, [...(this.props.chooseFileButton.props.children ? [this.props.chooseFileButton.props.children] : []), (<input type="file" name="ajax-upload-file-input" style={{ display: 'none' }} onChange={this.commonChangeFile} {...inputProps} key="file-button" />)]);
    const uploadFileButton = this.props.uploadFileButton && React.cloneElement(this.props.uploadFileButton, {
      onClick: this.commonUploadFile
    });
    return (
      <div style={{ display: 'inline-block' }}>
        { chooseFileButton }
        { uploadFileButton }
      </div>
    );
  }
}