material-ui-form
Version: 
State and validation management for Material-UI form components
526 lines (435 loc) • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _class, _temp;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
var _FormControl = require('@material-ui/core/FormControl');
var _FormControl2 = _interopRequireDefault(_FormControl);
var _FormControlLabel = require('@material-ui/core/FormControlLabel');
var _FormControlLabel2 = _interopRequireDefault(_FormControlLabel);
var _FormHelperText = require('@material-ui/core/FormHelperText');
var _FormHelperText2 = _interopRequireDefault(_FormHelperText);
var _FormLabel = require('@material-ui/core/FormLabel');
var _FormLabel2 = _interopRequireDefault(_FormLabel);
var _InputLabel = require('@material-ui/core/InputLabel');
var _InputLabel2 = _interopRequireDefault(_InputLabel);
var _Checkbox = require('@material-ui/core/Checkbox');
var _Checkbox2 = _interopRequireDefault(_Checkbox);
var _Switch = require('@material-ui/core/Switch');
var _Switch2 = _interopRequireDefault(_Switch);
var _FormControlClone = require('./FormControlClone');
var _FormControlClone2 = _interopRequireDefault(_FormControlClone);
var _FormControlLabelClone = require('./FormControlLabelClone');
var _FormControlLabelClone2 = _interopRequireDefault(_FormControlLabelClone);
var _FieldClone = require('./FieldClone');
var _FieldClone2 = _interopRequireDefault(_FieldClone);
var _CheckableFieldClone = require('./CheckableFieldClone');
var _CheckableFieldClone2 = _interopRequireDefault(_CheckableFieldClone);
var _DeleteFieldRowButton = require('./DeleteFieldRowButton');
var _DeleteFieldRowButton2 = _interopRequireDefault(_DeleteFieldRowButton);
var _propNames = require('../propNames');
var _propNames2 = _interopRequireDefault(_propNames);
var _validation2 = require('../validation');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function verifyFieldElement(component) {
  var whitelist = [_FormControlLabel2.default];
  return whitelist.includes(component.type) || _lodash2.default.has(component, 'props.name') && _lodash2.default.has(component, 'props.value');
}
function extractFieldValidators(fieldProps) {
  var validators = _lodash2.default.get(fieldProps, _propNames2.default.FIELD_VALIDATORS);
  if (validators !== undefined) {
    if (_lodash2.default.isString(validators)) {
      validators = validators.replace(/\s/g, '').split(',');
    } else if (!_lodash2.default.isArray(validators)) {
      validators = [validators];
    }
    return validators;
  }
  return [];
}
function getFieldValues(fields) {
  var values = {};
  _lodash2.default.each(fields, function (field, name) {
    if (_lodash2.default.get(field, 'checked') !== false) {
      values[name] = field.value;
    }
  });
  return values;
}
function getPristineFieldValues(fields) {
  var values = {};
  _lodash2.default.each(fields, function (field, name) {
    if (!field.isPristine && _lodash2.default.get(field, 'checked') !== false) {
      values[name] = field.pristineValue;
    }
  });
  return values;
}
function getFieldTemplate() {
  return {
    isDirty: false,
    isPristine: true,
    isRequired: null,
    pristineValue: null,
    step: undefined,
    validations: [],
    validators: [],
    value: undefined
  };
}
function deriveErrorSteps(fields) {
  var errorSteps = [];
  _lodash2.default.each(fields, function (field) {
    if (field.validations.length > 0 && !errorSteps.includes(field.step)) {
      errorSteps.push(field.step);
    }
  });
  return errorSteps;
}
function isValidForm(fields) {
  return _lodash2.default.size(_lodash2.default.filter(fields, function (field) {
    return field.validations.length > 0;
  })) === 0;
}
var Form = (_temp = _class = function (_React$Component) {
  _inherits(Form, _React$Component);
  function Form(props) {
    _classCallCheck(this, Form);
    var _this = _possibleConstructorReturn(this, (Form.__proto__ || Object.getPrototypeOf(Form)).call(this, props));
    _this.validation = {
      messageMap: _validation2.messageMap,
      messageMapKeyPrefix: '',
      requiredValidatorName: _validation2.constants.REQUIRED_VALIDATOR_NAME,
      validators: _validation2.validators,
      validate: _validation2.validate,
      validateInputOnBlur: false
    };
    _this.onFieldConstruct = function (fieldProps) {
      var checked = fieldProps.checked,
          name = fieldProps.name,
          required = fieldProps.required,
          value = fieldProps.value;
      // checkable input
      if (checked === true) {
        _lodash2.default.defer(function () {
          _this.setState({
            fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, getFieldTemplate(), {
              checked: checked || false,
              step: _this.props.activeStep,
              value: value
            })))
          });
        });
        // other inputs
      } else if (!_lodash2.default.isBoolean(checked)) {
        var _requiredValidatorName = _this.validation.requiredValidatorName;
        if (!_lodash2.default.has(_this.state.fields, name)) {
          var _validators = extractFieldValidators(fieldProps);
          if (required && !_lodash2.default.isEmpty(_requiredValidatorName)) {
            _validators.unshift(_requiredValidatorName);
          }
          var isRequired = required || _validators.includes(_requiredValidatorName);
          // set any validations on first construct
          var _validations = [];
          if (!_lodash2.default.has(_this.state.fields, name) && _lodash2.default.has(_this.props.validations, name)) {
            _validations = _this.props.validations[name];
          }
          _lodash2.default.defer(function () {
            _this.setState({
              fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, getFieldTemplate(), {
                isRequired: isRequired,
                pristineValue: value,
                step: _this.props.activeStep,
                validators: _validators,
                validations: _validations,
                value: value
              })))
            });
            if (!_lodash2.default.isEmpty(value)) {
              _this.validateField(name, value);
            }
          });
        }
      }
    };
    _this.onFieldValueChange = function (name, value) {
      var isDirty = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
      _lodash2.default.defer(function () {
        _this.setState({
          fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], {
            isDirty: isDirty || _this.state.fields[name].isDirty,
            isPristine: false,
            validations: [],
            value: value
          })))
        });
        if (isValidForm(_this.state.fields)) {
          _this.enableSubmitButton();
        }
        if (_this.onValuesChange !== undefined) {
          _this.onValuesChange(getFieldValues(_this.state.fields), getPristineFieldValues(_this.state.fields));
        }
        if (_this.state.fields[name].isDirty) {
          _this.validateField(name, value);
        }
      });
    };
    _this.onFieldToggle = function (name, value, checked) {
      _this.setState({
        fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], {
          checked: checked,
          isPristine: false,
          validations: [],
          value: value
        })))
      });
    };
    _this.validateField = function (name, value) {
      var field = _this.state.fields[name];
      if (!(field.value === '' && !field.isRequired) && !_lodash2.default.isEmpty(field.validators)) {
        var _validation = _this.validation;
        var _validations2 = _validation.validate(value, field.validators, _validation);
        // update state
        field.validations = _validations2;
        _this.setState({
          fields: _extends({}, _this.state.fields, _defineProperty({}, name, field))
        });
        // disable submit button
        if (!_lodash2.default.isEmpty(_validations2)) {
          _this.disableSubmitButton();
        }
        // propogate validation
        if (_this.props.onFieldValidation !== undefined) {
          var errorSteps = void 0;
          if (field.step !== undefined) {
            errorSteps = deriveErrorSteps(_this.state.fields);
          }
          _this.props.onFieldValidation(field, errorSteps);
        }
      }
    };
    _this.reset = function () {
      var fields = _this.state.fields;
      _lodash2.default.defer(function () {
        _lodash2.default.each(fields, function (field, name) {
          _this.setState({
            fields: _extends({}, _this.state.fields, _defineProperty({}, name, _extends({}, _this.state.fields[name], {
              isDirty: false,
              isPristine: true,
              value: ''
            })))
          });
        });
      });
    };
    _this.submit = function (event) {
      event.preventDefault();
      var isValid = true;
      var fields = _this.state.fields;
      _lodash2.default.each(fields, function (field, name) {
        if (field.isRequired && field.value === '') {
          _this.validateField(name, '');
          isValid = false;
        }
      });
      if (isValid) {
        _this.props.onSubmit(getFieldValues(fields), getPristineFieldValues(fields));
      }
    };
    _this.deleteRow = function (row) {
      var pos = row.indexOf('[');
      var rowName = row.substr(0, pos);
      var rowIndex = parseInt(row.substr(pos + 1), 10);
      var fields = _this.state.fields;
      _lodash2.default.each(fields, function (field, fieldName) {
        if (fieldName.startsWith(row)) {
          delete fields[fieldName];
        } else if (fieldName.startsWith(rowName)) {
          var index = parseInt(fieldName.substr(pos + 1), 10);
          if (index > rowIndex) {
            var newRow = fieldName.replace(/\[\d+\]/, '[' + (index - 1) + ']');
            delete fields[fieldName];
            fields[newRow] = field;
          }
        }
      });
      _this.setState({ fields: fields });
    };
    _this.onValuesChange = props.onValuesChange;
    _this.validation = Object.assign(_this.validation, props.validation);
    _this.state = {
      disableSubmitButton: false,
      fields: {}
    };
    return _this;
  }
  // eslint-disable-next-line react/sort-comp
  _createClass(Form, [{
    key: 'enableSubmitButton',
    value: function enableSubmitButton() {
      if (this.state.disableSubmitButton) {
        this.setState({ disableSubmitButton: false });
      }
    }
  }, {
    key: 'disableSubmitButton',
    value: function disableSubmitButton() {
      if (this.props.disableSubmitButtonOnError) {
        this.setState({ disableSubmitButton: true });
      }
    }
  }, {
    key: 'cloneChildrenRecursively',
    value: function cloneChildrenRecursively(children) {
      var _this2 = this;
      return _react2.default.Children.map(children, function (child) {
        if (_lodash2.default.isEmpty(child)) {
          return null;
        }
        if (_lodash2.default.isString(child)) {
          return child;
        }
        var isFieldElement = verifyFieldElement(child);
        var nestedChildren = _lodash2.default.isArray(child.props.children) && !isFieldElement ? _lodash2.default.filter(child.props.children, function (v) {
          return _lodash2.default.isObject(v) || _lodash2.default.isString(v);
        }) : false;
        // nested elements
        if (nestedChildren !== false) {
          // FormControl element with field/group name-value props
          if (child.type === _FormControl2.default) {
            var fieldElement = nestedChildren.find(function (el) {
              return ![_FormLabel2.default, _InputLabel2.default, _FormHelperText2.default].includes(el.type) && el.props.name !== undefined && el.props.value !== undefined;
            });
            if (fieldElement !== undefined) {
              var _name = fieldElement.props.name;
              return _react2.default.createElement(_FormControlClone2.default, {
                key: _name,
                field: _this2.state.fields[_name],
                formControlComp: child,
                onConstruct: _this2.onFieldConstruct,
                onValueChange: _this2.onFieldValueChange
              });
            }
          }
          // non-FormControl element
          return _react2.default.cloneElement(child, {
            children: _this2.cloneChildrenRecursively(nestedChildren)
          });
        }
        // add disable functionality to submit button
        if (child.props.type === 'submit') {
          return _react2.default.cloneElement(child, {
            disabled: _this2.state.disableSubmitButton
          });
          // non-interactive elements should be rendered as is
        } else if (!isFieldElement) {
          // delete row button
          if (child.props[_propNames2.default.DELETE_FIELD_ROW] !== undefined) {
            return _react2.default.createElement(_DeleteFieldRowButton2.default, {
              buttonComp: child,
              onRequestRowDelete: _this2.deleteRow
            });
          }
          // any other element
          return child;
        }
        // clone control label
        if (child.type === _FormControlLabel2.default) {
          var _name2 = child.props.control.props.name;
          return _react2.default.createElement(_FormControlLabelClone2.default, {
            key: _name2,
            field: _this2.state.fields[_name2],
            control: child.props.control,
            label: child.props.label,
            onConstruct: _this2.onFieldConstruct,
            onToggle: _this2.onFieldToggle
          });
        }
        // clone input element
        var name = child.props.name;
        // checkable
        if (child.type === _Checkbox2.default || child.type === _Switch2.default) {
          return _react2.default.createElement(_CheckableFieldClone2.default, {
            key: name,
            field: _this2.state.fields[name],
            fieldComp: child,
            onConstruct: _this2.onFieldConstruct,
            onToggle: _this2.onFieldToggle
          });
        }
        return _react2.default.createElement(_FieldClone2.default, {
          key: name,
          field: _this2.state.fields[name],
          fieldComp: child,
          onConstruct: _this2.onFieldConstruct,
          onValueChange: _this2.onFieldValueChange,
          useNativeRequiredValidator: !_this2.validation.requiredValidatorName,
          validateInputOnBlur: _this2.validation.validateInputOnBlur
        });
      });
    }
  }, {
    key: 'render',
    value: function render() {
      return _react2.default.createElement(
        'form',
        {
          autoComplete: this.props.autoComplete,
          className: this.props.className,
          onReset: this.reset,
          onSubmit: this.submit,
          style: this.props.style,
          id: this.props.id,
          method: this.props.method,
          action: this.props.action,
          name: this.props.name
        },
        this.cloneChildrenRecursively(this.props.children)
      );
    }
  }], [{
    key: 'getDerivedStateFromProps',
    value: function getDerivedStateFromProps(nextProps, prevState) {
      var fields = prevState.fields;
      if (!_lodash2.default.isEmpty(fields)) {
        // add validations to fields
        _lodash2.default.each(nextProps.validations, function (validations, name) {
          if (_lodash2.default.has(fields, name)) {
            fields[name].validations = validations;
          } else {
            // eslint-disable-next-line no-console
            console.warn('validations field "' + name + '" does not exist');
          }
        });
        return { fields: fields };
      }
      return null;
    }
  }]);
  return Form;
}(_react2.default.Component), _class.defaultProps = {
  activeStep: 0,
  autoComplete: 'off',
  className: undefined,
  disableSubmitButtonOnError: true,
  onFieldValidation: undefined,
  onValuesChange: undefined,
  style: {},
  validation: {},
  validations: {},
  id: undefined,
  method: undefined,
  action: undefined,
  name: undefined }, _temp);
exports.default = Form;