formsy / formsy-react

A form input builder and validator for React JS
MIT License
763 stars 125 forks source link

Trigger validations onSubmit vs onBlur option #93

Open th3fallen opened 6 years ago

th3fallen commented 6 years ago

is this currently possible?

mikelyncheski commented 6 years ago

@th3fallen I am currently using formsy to only show the messages after a user leaves the input (instead of on typing). The validations are still done as the user types but I am only showing them after the user modifies the input or clicks the submit button. I control that in my input wrapper component by choosing when the error message gets displayed or not. Something like:

if (!this.state.isFieldEdited && !this.props.isFormSubmitted()) errorMessage = null;

Note that isFieldEdited is part of my component and isFormSubmitted() is from Formsy.

MilosRasic commented 6 years ago

I've been wanting this for a long time too. What @mikelyncheski suggest is certainly possible and a good way to do it, but sometimes you may want to optimize a demanding validation by not doing it on every keyup. Debounce would also be another nice option. We could probably add this as a non-breaking change as a prop which works as before by default, something like:

<SomeFormsyComponent validateOn={'blur'} />

with 'keyup' as default, or whatever is the default atm

MilosRasic commented 6 years ago

Ok, we're all stupid. Sorry for calling you stupid, but I'm calling myself stupid too so it's fine ;)

Upon further investigation turns out this is up to the implementation of a formsy field component. this.props.setValue provided by withFormsy has a secon parameter, boolean, which tells formsy whether to validate the new value or not. This altered example from readme does exactly what we need:

class MyInput extends React.Component {
  constructor(props) {
    super(props);
    this.changeValue = this.changeValue.bind(this);
    this.validateValue = this.validateValue.bind(this);
  }

  changeValue(event) {
    // setValue() will set the value of the component, which in
    // turn will validate it and the rest of the form
    // Important: Don't skip this step. This pattern is required
    // for Formsy to work.
    this.props.setValue(event.currentTarget.value, false);
  }

  validateValue(event) {
    this.props.setValue(event.currentTarget.value);
  }

  render() {
    // An error message is returned only if the component is invalid
    const errorMessage = this.props.getErrorMessage();

    return (
      <div>
        <input
          onChange={this.changeValue}
          onBlur={this.validateValue}
          type="text"
          value={this.props.getValue() || ''}
        />
        <span>{errorMessage}</span>
      </div>
    );
  }
}
djensen47 commented 5 years ago

As a followup on this one. When you use onBlur to set the error, a common pattern is to clear the error when the user starts typing.

  1. Enter invalid value
  2. Blur the field
  3. Error appears
  4. Change the value in the field
  5. Error message goes away
  6. Blur the field
  7. Validation occurs again

Right now, it seems that Formsy does not internally handle steps 4-5, correct.

hungbang commented 5 years ago

Is there any solution for this issue? we actually need to trigger validations onSubmit and onBlur , it's really needed

rkuykendall commented 4 years ago

@hungbang There is currently no solution for this issue, but PRs are welcome as well as failing tests or even just edits to API.md with the desired feature spec. I think anyone will find the project very easy to work with.

aqos156 commented 4 years ago

Hi, I wanted to implement this behavior so I went through the source code of formsy and came up with this solution, but I don't know much about the structure and the way formsy validates the forms in it's internals (frankly don't have that much time), but it seems to be working as expected.

That's why i wanted to ask @rkuykendall if this is a possible proof of concept that could be implemented.

ValidationInput.js

import { withFormsy } from "formsy-react";
import React from "react";
class ValidationInput extends React.Component {
  constructor(props) {
    super(props);
    this.changeValue = this.changeValue.bind(this);
    this.validateValue = this.validateValue.bind(this);
  }

  changeValue(event) {

   // this is an add method via the InputWrapper
   // that resets the validation properties if an error 
   // is present
    if (this.props.showError) {
      this.props.resetValidation();
    }

    // setValue() will set the value of the component, which in
    // turn will validate it and the rest of the form
    // Important: Don't skip this step. This pattern is required
    // for Formsy to work.
    if (this.props.isValidValue(event.currentTarget.value)) {
      this.props.setValue(event.currentTarget.value);
    } else {
      this.props.setValue(event.currentTarget.value, false);
    }
  }

  validateValue(event) {
    this.props.setValue(event.currentTarget.value);
  }

  keyDown = event => {
    // If we press enter, we want to validate the input (omitting
    // the false argument)
    if (event.keyCode == "13") {
      this.props.setValue(event.currentTarget.value);
    } 
  };

  render() {
    // An error message is returned only if the component is invalid
    const {
      errorMessage,
      className,
      showRequired,
      isFormSubmitted,
      showError,
      type,
      value,
      placeholder,
      children
    } = this.props;

    let nClassName = className ? className : "form-control";

    nClassName =
      isFormSubmitted && showRequired
        ? nClassName + " is-invalid"
        : showError
        ? nClassName + " is-invalid"
        : nClassName;

    return (
      <fieldset className="form-group">
        <input
          className={nClassName}
          type={type ? type : "text"}
          value={value}
          onChange={this.changeValue}
          onBlur={this.validateValue}
          placeholder={placeholder}
          onKeyDown={this.keyDown}
        />
        {children ? children : null}
        {errorMessage ? (
          <div className="invalid-feedback">{errorMessage}</div>
        ) : null}
      </fieldset>
    );
  }
}

class WrappedInput extends withFormsy(ValidationInput) {
  resetValidation = () => {
    const { pristineValue } = this.state;
    const { validate } = this.context;

    this.setState({
      isPristine: true,
      isValid: true
    });
  };

  render() {
    const { innerRef } = this.props;
    const propsForElement = {
      ...this.props,
      errorMessage: this.getErrorMessage(),
      errorMessages: this.getErrorMessages(),
      hasValue: this.hasValue(),
      isFormDisabled: this.isFormDisabled(),
      isFormSubmitted: this.isFormSubmitted(),
      isPristine: this.isPristine(),
      isRequired: this.isRequired(),
      isValid: this.isValid(),
      isValidValue: this.isValidValue,
      resetValue: this.resetValue,
      setValidations: this.setValidations,
      setValue: this.setValue,
      showError: this.showError(),
      showRequired: this.showRequired(),
      value: this.getValue(),
      resetValidation: this.resetValidation
    };

    if (innerRef) {
      propsForElement.ref = innerRef;
    }

    return React.createElement(ValidationInput, propsForElement);
  }
}

export default WrappedInput;

And the form source code:

<Formsy onValidSubmit={this.submitForm}>
  <ValidationInput
    name="email"
    validations="isEmail"
    validationError={strings.form.errors.emailFormat}
    placeholder={strings.auth.email}
    required
  />
  <div className="text-center">
    <button
      className="btn btn-primary pull-center w-50 mt-5"
      type="submit"
      disabled={this.props.inProgress}
    >
      {strings.auth.login}
    </button>
  </div>
</Formsy>

EDIT

Added onKeyDown check for validation of the input on enter key press