Почему мой ввод формы регистрирует только один символ после повторного рендеринга? - PullRequest
0 голосов
/ 26 февраля 2020

При начальном рендеринге компоненты ввода работают как обычно. Однако после того, как он снова сфокусирован и перефокусирован, он примет только один символ. Это также случается, если я сбрасываю форму.

Я создал класс FormValidationStateManager для повторного использования необходимых для проверки логов Joi c.

Form.jsx

class Form extends Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.validation = new FormValidationStateManager();
  }

  handleSubmit(e) {
    e.preventDefault();
    var result = this.validation.validateAll();

    this.setState({
      // need to do this, trigger render after validations
      // we could eventually encapsulate this logic into FormValidationSM.
      submit: Date.now()
    }, function() {
      if (result.valid) {
        this.props.onSubmit(result.data);
      }
    }.bind(this));
  }

  resetForm(e) {
    e.preventDefault();
    this.validation.clearForm();

    this.setState({
      // need to do this, trigger render after validations,
      // we could eventually encapsulate this logic into FormValidationSM.
      submit: Date.now()
    });
  }

  render() {
    // TODO implement logic for rendering name from API call
    let name = "John Doe";
    return (
      <form class='info-form' onSubmit={this.handleSubmit} autocomplete='off'>
        <div class='info-block-section-title'>
          <h2>Welcome {name}</h2>
          <h4>Some nicely worded few sentences that let's the user know they just need to fill out a few bits of information to start the process of getting their student loans paid down!</h4>
        </div>

        <br />

        <div class='info-block-section-body'>
          <InfoFieldset validation={this.validation} />
        </div>
        <div class='info-form-button'>
          <FormButton label="Continue" />
          <FormButton type="button" onClick={this.resetForm.bind(this)} label="Reset" />
        </div>
      </form>

    )
  }
}

export default Form;

Набор полей

class Fieldset extends Component {
  constructor(props) {
    super(props);
    this.state = {
      secondaryEmailVisible: false
    };

    this.handleToggle = this.handleToggle.bind(this);

    this.validation = this.props.validation || new FormValidationStateManager();
    this.validation.addRules(this.validatorRules.bind(this));
  }

  validatorRules() {
    var _rules = {
      // TODO validate format of date, address and zip
      date_of_birth: Joi.date().label("Date of Birth").required(),
      address_street: Joi.string().label("Street Address").required(),
      address_zip: Joi.string().label("Zip Code").required(),

      password_confirmation: Joi.any()
        .valid(Joi.ref('password'))
        .required()
        .label('Password Confirmation')
        .messages({
          "any.only": "{{#label}} must match password"
        }).strip(),

      password: Joi.string()
        .pattern(/\d/, 'digit')
        .pattern(/^\S*$/, 'spaces')
        .pattern(/^(?!.*?(.)\1{2})/, 'duplicates')
        .pattern(/[a-zA-z]/, 'alpha')
        .required()
        .min(CONSTANTS.PASSWORD_MIN)
        .label('Password')
        .messages({
          "string.min": PASSWORD_HINT,
          "string.pattern.name": PASSWORD_HINT
        })
    }

    if (this.state.secondaryEmailVisible) {
      _rules['secondary_email'] = Joi.string()
        .label("Secondary Email")
        .email({ tlds: false })
        .required();
    }

    return _rules;
  }

  handleToggle() {
    this.setState(state => ({
      secondaryEmailVisible: !state.secondaryEmailVisible
    }));
  }

  render() {
    return(
      <div class='info-fieldset'>
        {/* for skipping chrome browser autocomplete feature
        <input type='text' style={{ display: 'none '}} />
        <input type='password' style={{ display: 'none '}} />
        */ }

        <strong>Primary email for login: work@example.com</strong>

        <FormElementToggle
          field="secondary"
          label={"Add another email for notifications?"}
          onChange={this.handleToggle} />

        <div class='info-form-element'>
          <DisplayToggle remove={true} show={this.state.secondaryEmailVisible}>
            <FormElementInput
              field='secondary_email'
              label="Secondary Email"
              validation={this.validation}
              placeholder='john@example.com' />
          </DisplayToggle>

          <FormElementInput
            field='password'
            type='password'
            label={(
              <div>
                Password
                &nbsp;
                <Tooltip
                  placement='top'
                  trigger={['hover']}
                  overlay={PASSWORD_HINT}>
                  <span><i className='fa fa-info-circle'></i></span>
                </Tooltip>
              </div>
            )}
            placeholder='********'
            validation={this.validation} />

          <FormElementInput
            key={1}
            field='password_confirmation'
            type='password'
            label="Password Confirmation"
            placeholder='********'
            validation={this.validation} />

          <FormElementInput
            field='date_of_birth'
            label="Date of Birth"
            validation={this.validation}
            placeholder='MM/DD/YYYY' />

          <FormElementInput
            field='address_street'
            label="Street Address"
            validation={this.validation}
            placeholder='123 Example St' />

          <FormElementInput
            field="address_zip"
            label="Zip Code"
            validation={this.validation}
            placeholder='01234' />

        </div>
      </div>
    )
  }
}

export default Fieldset;

FormElementInput.jsx

class FormElementInput extends Component {
  constructor(props) {
    super(props);
    this.input = React.createRef();
    this.cachedErrors = {};
    this.cachedValue = null;
    this.state = { focused: false };
    this.validation = this.props.validation || new FormValidationStateManager();
  }

  componentDidUpdate(prevProps, prevState) {

  }

  shouldComponentUpdate(nextProps, nextState) {
    var filter = function(val, key, obj) {
      if (_.isFunction(val)) {
        return true;
      }

      // Certain props like "label" can take a React element, but those
      // are created dynamically on the fly and they have an random internal UUID,
      // which trips the deep-equality comparision with a false-positive.
      // This is wasted cycles for rendering.
      if (React.isValidElement(val)) {
        return true;
      }

      // validation is a complex obj that we shouldn't be caring about
      if (_.contains(['validation'], key)) {
        return true;
      }
    };

    // if the errors after a validation got updated, we should re-render.
    // We have to compare in this manner because the validation object is not shallow-copied
    // via props since it is managed at a higher-component level, so we need to
    // do a local cache comparision with our local copy of errors.
    if (!_.isEqual(this.cachedErrors, this.getErrors())) {
      return true;
    }

    // if the errors after a validation got updated, we should re-render.
    // We have to compare in this manner because the validation object is not shallow-copied
    // via props since it is managed at a higher-component level, so we need to
    // do a local cache comparision with our local copy of errors.
    if (this.cachedValue !== this.getDisplayValue()) {
      return true;
    }

    return !_.isEqual(_.omit(nextProps, filter), _.omit(this.props, filter)) || !_.isEqual(this.state, nextState);
  }

  setValue(value) {
    console.error('deprecated - can not set value directly on FormElementInput anymore');
    throw 'deprecated - can not set value directly on FormElementInput anymore';
  }

  isDollarType() {
    return this.props.type === 'dollar';
  }

  isRateType() {
    return this.props.type === 'rate';
  }

  getType() {
    if (this.isDollarType() || this.isRateType()) {
      return 'text';
    } else {
      return this.props.type;
    }
  }

  getPrefix() {
    if (this.isDollarType()) {
      return _.compact([this.props.prefix, '$']).join(' ');
    } else {
      return this.props.prefix;
    }
  }

  getPostfix() {
    if (this.isRateType()) {
      return _.compact([this.props.postfix, '%']).join(' ');
    } else {
      return this.props.postfix;
    }
  }

  getDisplayValue() {
    var value = this.props.value || this.validation.getFormValue(this.props.field);
      // while DOM input is focused, just return the same value being typed
      if (this.state.focused) {
        return value;
      }

      if (this.isDollarType()) {
        if (_.isUndefined(value)) {
          return value;
        }
        // keep in sync with the validation check in getOnChange()
        // even if this is different - it wont break - cause accounting.js handles gracefully
        else if (_.isNumber(value) || !value.match(/[a-z]/i)) {
          return accounting.formatMoney(value, '', 2);
        } else {
          return value;
        }
      } else if (this.isRateType()){
        if (_.isUndefined(value)) {
          return '';
        }

        return accounting.formatNumber(value, 3)
      } else {
        return value;
      }
  }

  getErrors() {
    return this.props.errors || this.validation.getValidationMessages(this.props.field);
  }

  onBlur(event) {
    this.validation.validate(this.props.field);
    this.setState({ focused: false });
  }

  onFocus(event) {
    this.setState({ focused: true });
  }

  onChange(event) {
    if (this.isDollarType()) {
      // only accept if the input at least resembles a currency
      // accounting.parse already strips alot of characters and does this silently
      // we want to be a little less strict than accounting.parse since we need to allow more
      // different types of inputs +-,. and also let accounting.parse do its job
      // very basic check here for a-z characters
      if (!event.target.value.match(/[a-z]/i)) {
        var parsedValue = accounting.parse(event.target.value);
        event._parsedValue = parsedValue;
      }
    }

    let _value = event._parsedValue || event.target.value;
    console.log(this.input)
    this.validation.setFormValue(this.props.field, _value);
  }

  _eventHandlers(field) {
    return {
      onChange: this.onChange.bind(this),
      onBlur: this.onBlur.bind(this),
      onFocus: this.onFocus.bind(this)
    };
  }

  _mergeEventHandlers(p1, p2) {
    var events = [
      'onChange',
      'onBlur',
      'onFocus'
    ];

    var r1 = {};

    events.map(function(e) {
      r1[e] = FuncUtils.mergeFunctions(p1[e], p2[e]);
    }.bind(this));

    return r1;
  }

  render() {
    let _errors = this.cachedErrors = this.getErrors();
    let _value = this.cachedValue = this.getDisplayValue();

    let attrs = {
      id: this.props.htmlId || ('field_' + this.props.field),
      className: classnames({
        'error': _errors && _errors.length
      }),
      type: this.getType(),
      placeholder: this.props.placeholder,
      autoComplete: "false",
      disabled: this.props.disabled,
      readOnly: this.props.disabled,
      value: _value,
      ref: this.input,
    };

    attrs = _.extend(attrs, this._mergeEventHandlers(this.props, this._eventHandlers()));

    const prefix = this.getPrefix();
    const postfix = this.getPostfix();

    return (
      <FormElementWrapper
        type="input"
        field={this.props.field}
        errors={_errors}
        label={this.props.label}>
        <div className="form-element-input">
          <DisplayToggle remove={true} hide={!prefix}>
            <span className="prefix">{prefix}</span>
          </DisplayToggle>
          <div className={classnames({
          "has-prefix": !!prefix,
          "has-postfix": !!postfix
        })}>
            <input {...attrs} />
          </div>
          <DisplayToggle remove={true} hide={!postfix}>
            <span className="postfix">{postfix}</span>
          </DisplayToggle>
        </div>
        <div className="form-error">
          {_errors.map((msg, idx) => {
            return (<FormError key={idx}>{msg}</FormError>)
          })}
        </div>
      </FormElementWrapper>
    )
  }
};

FormElementInput.PropTypes = {
  field: PropTypes.string.isRequired,
  id: PropTypes.string.isRequired,
  value: PropTypes.any,
  label: PropTypes.any,
  errors: PropTypes.array,
  type: PropTypes.string,
  placeholder: PropTypes.string,
  onChange: PropTypes.func,
  onFocus: PropTypes.func,
  onKeyUp: PropTypes.func,
  tooltip: PropTypes.string,
  prefix: PropTypes.string,
  postfix: PropTypes.string,
  disabled: PropTypes.bool,
  disableTrimming: PropTypes.bool
}

FormElementInput.defaultProps = {
  type: 'text',
  placeholder: 'Enter Value',
  tooltip: undefined,
  prefix: undefined,
  postfix: undefined,
  disabled: false,
  disableTrimming: false
}

export default FormElementInput;

FormValidationStateManager. js

class FormValidationStateManager {
  // default state, validation ref obj
  constructor(defaultState) {
    this.rules = [];
    this.state = defaultState || {};
    this.errors = {};
    this.dirty = false;
  }

  getFormValue(key) {
    return this.state[key];
  }

  setFormValue(key, value) {
    this.dirty = true;
    this.state[key] = value;
  }

  setFormState(newState) {
    this.state = _.extend({}, newState);
  }

  addRules(rules) {
    this.rules.push(rules);
  }

  isFormDirty(){
    return this.dirty;
  }

  /**
   * Clear all previous validations
   *
   * @return {void}
   */
  clearValidations() {
    this.dirty = false;
    this.errors = {};
  }

  clearForm() {
    this.state = _.mapObject(this.state, function(v, k) {
      return '';
    });
    this.clearValidations();
  }

  /**
   * Check current validity for a specified key or entire form.
   *
   * @param {?String} key to check validity (entire form if undefined).
   * @return {Boolean}.
   */
  isValid(key) {
    return _.isEmpty(this.getValidationMessages(key));
  }

  // call within submit
  validateAll() {
    return this.validate();
  }

  /**
   * Method to validate single form key or entire form against the component data.
   *
   * @param {String|Function} key to validate, or error-first containing the validation errors if any.
   * @param {?Function} error-first callback containing the validation errors if any.
   */
  validate(key, callback) {
    if (_.isFunction(key)) {
      callback = key;
      key = undefined;
    }

    var schema = this._buildRules();
    var data = this.state;
    var result = this._joiValidate(schema, data, key);

    if (key) {
      this.errors[key] = result.errors[key];
    } else {
      this.errors = result.errors;
    }

    return {
      valid: this.isValid(key),
      errors: this.errors,
      data: result.data
    };
  }

  _buildRules() {
    var allRules = this.rules.map(function(r) {
      // Rules can be functions and be dynamically evalulated on-the-fly,
      // to allow for more complex validation rules.
      if (_.isFunction(r)) {
        return r();
      } else {
        return r;
      }
    });

    // collapse all the rules into one single object that will
    // be evaluated against `this.state`
    return Joi.object(_.extend({}, ...allRules));
  }

  /**
   * Get current validation messages for a specified key or entire form.
   *
   * @param {?String} key to get messages, or entire form if key is undefined.
   * @return {Array}
   */
  getValidationMessages(key) {
    let errors = this.errors || {};
    if (_.isEmpty(errors)) {
      return [];
    } else {
      if (key === undefined) {
        return _.flatten(_.keys(errors).map(error => {
          return errors[error] || []
        }));
      } else {
        return errors[key] ? errors[key].map(he.decode) : [];
      }
    }
  }

  _joiValidate(joiSchema, data, key) {
    joiSchema = joiSchema || Joi.object();
    data = data || {};

    const joiOptions = {
      // when true, stops validation on the first error, otherwise returns all the errors found. Defaults to true.
      abortEarly: false,
      // when true, allows object to contain unknown keys which are ignored. Defaults to false.
      allowUnknown: true,
      // remove unknown elements from objects and arrays. Defaults to false
      stripUnknown: true,
      errors: {
        escapeHtml: true,
        // overrides the way values are wrapped (e.g. [] around arrays, "" around labels).
        // Each key can be set to a string with one (same character before and after the value)
        // or two characters (first character before and second character after), or false to disable wrapping:
        label: false,
        wrap: {
          label: false,
          array: false
        }
      },
      messages: {
        "string.empty": "{{#label}} is required",
        "any.empty": "{{#label}} is required",
        "any.required": "{{#label}} is required"
      }
    };

    const result = joiSchema.validate(data, joiOptions);
    let errors = this._formatErrors(result);

    if (key) {
      errors = _.pick(errors, key);
    }

    return {
      errors: errors,
      data: result.value
    };
  }

  _formatErrors(joiResult) {
    if (joiResult.error) {
      return _.reduce(joiResult.error.details, (memo, detail) => {
        // According to docs:
        //  - "detail.path": ordered array where each element is the accessor to the value where the error happened.
        //  - "detail.context.key": key of the value that erred, equivalent to the last element of details.path.
        //  Which is why we get the last element in "path"
        var key = _.last(detail.path);
        if (!Array.isArray(memo[key])) {
          memo[key] = [];
        }
        if (!_.contains(memo[key], detail.message)) {
          memo[key].push(detail.message);
        }
        return memo;
      }, {});
    } else {
      return {};
    }
  }

}

export default FormValidationStateManager;

1 Ответ

0 голосов
/ 26 февраля 2020

Я думаю, что понял. Я изменил value: _value на defaultValue: _value для атрибутов в FormElementInput.jsx

увидел его здесь

Кто-нибудь знает, почему это исправлено в капюшон?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...