gcanti / tcomb-form

Forms library for react
https://gcanti.github.io/tcomb-form
MIT License
1.16k stars 136 forks source link

How to set form in error state? hasError does not set the form in error state immediately. #325

Closed mnazim closed 8 years ago

mnazim commented 8 years ago

Version

Based on the data I receive from the API, I need to set errors per field and set the form in error state. I am doing this with redux and I get the API response in props.signup. I am using componentWillReceiveProps to check for errors in nextProps.signup and set errors accordingly.

The problem

I can confirm the new this.state.formOptions being set in componentWillReceiveProps and render being called again, but the form itself does not render in error state.

Form is only set in error state when I edit a field for which an hasError = true has been set in componentWillReceiveProps. Say, I got error from the server for email field, "email has already been used to register an account"; the moment I edit email field, the errors I set in componentWillReceiveProps only for email field are displayed.

What am I doing wrong?

The Component

const SignupFormSchema = t.struct({
  first_name: t.String,
  last_name: t.String,
  email: EmailRefinement,
  password1: t.String,
  password2: t.String,
  accept_tos: t.Boolean
});

const signupFormOptions = {
  auto: 'none',
  error: '<p className="form-error">Acount creation failed</p>',
  hasError: false,
  fields: {
    first_name: {
      error: 'First name cannot be blank',
      attrs: {
        placeholder: 'First Name'
      }
    },
    last_name: {
      error: 'Last name cannot be blank',
      attrs: {
        placeholder: 'Last Name'
      }
    },
    email: {
      error: 'Please provide a valid email address',
      type: 'email',
      attrs: {
        placeholder: 'Email'
      }
    },
    password1: {
      error: 'Please enter a password',
      type: 'password',
      attrs: {
        placeholder: 'Password'
      }
    },
    password2: {
      error: 'Please repeat the same password',
      type: 'password',
      attrs: {
        placeholder: 'Repeat Password'
      }
    },
    accept_tos: {
      label: 'I agree to terms, conditions, and privacy policy',
      error: 'You must accept the terms, conditions, and privacy policy before proceeding'
    }
  }
};

class SignupForm extends Component {
  state = {
    formValue: {},
    formOptions: signupFormOptions
  };

  onFormChange = formValue => {
    this.setState({formValue});
  };

  componentWillReceiveProps(nextProps) {
    let { signup }  = nextProps;
    let newFormOptions = this.state.formOptions;
    if(signup && signup.error) {
      newFormOptions.hasError = true;
      newFormOptions.error = <p className="form-error">{ signup.message }</p>;

      if(signup.fieldErrors) {

        if(signup.fieldErrors.first_name) {
          newFormOptions.fields.first_name.hasError = true;
          newFormOptions.fields.first_name.error =   signup.fieldErrors.first_name.map(error => {
            return <span className="">{error}</span>;
          });
        }
        else {
          newFormOptions.fields.first_name.hasError = false;
          newFormOptions.fields.first_name.error = signupFormOptions.fields.first_name.error;
        }

        if(signup.fieldErrors.last_name) {
          newFormOptions.fields.last_name.hasError = true;
          newFormOptions.fields.last_name.error =   signup.fieldErrors.last_name.map(error => {
            return <span className="">{error}</span>;
          });
        }
        else {
          newFormOptions.fields.last_name.hasError = false;
          newFormOptions.fields.last_name.error = signupFormOptions.fields.last_name.error;
        }

        if(signup.fieldErrors.email) {
          newFormOptions.fields.email.hasError = true;
          newFormOptions.fields.email.error =   signup.fieldErrors.email.map(error => {
            return <span className="">{error}</span>;
          });
        }
        else {
          newFormOptions.fields.email.hasError = false;
          newFormOptions.fields.email.error = signupFormOptions.fields.email.error;
        }

        if(signup.fieldErrors.password1) {
          newFormOptions.fields.password1.hasError = true;
          newFormOptions.fields.password1.error =   signup.fieldErrors.password1.map(error => {
            return <span className="">{error}</span>;
          });
        }
        else {
          newFormOptions.fields.password1.hasError = false;
          newFormOptions.fields.password1.error = signupFormOptions.fields.password1.error;
        }

        if(signup.fieldErrors.password2) {
          newFormOptions.fields.password2.hasError = true;
          newFormOptions.fields.password2.error =  signup.fieldErrors.password2.map(error => {
            return <span className="">{error}</span>;
          });
        }
        else {
          newFormOptions.fields.password2.hasError = false;
          newFormOptions.fields.password2.error = signupFormOptions.fields.password2.error;
        }
      }
    }
    else {
      newFormOptions.hasError = false;
      newFormOptions.error = signupFormOptions.error;
    }
    this.setState( { formOptions: newFormOptions });
  }

  render() {

    return (
      <form onSubmit={(e)=>{ e.prevenDetault(); }}>
        <t.form.Form ref="signupForm" value={this.state.formValue} onChange={this.onFormChange} type={SignupFormSchema} options={this.state.formOptions} />
        <button onClick={this.handleSignupSubmit} className="button button-primary" type="submit">Signup</button>
      </form>
    );

  }
};
gcanti commented 8 years ago

tcomb-form implements shouldComponentUpdate in a way similar to PureRenderMixin, using === comparisons, so options must be treated as immutable data structures (i.e. return a new copy with changed references). My guess is that, since you are mutating formOptions, the form doesn't trigger a re-rendering.

Tip: you might want to use the provided immutability helpers or a library of your choice in order to do less work.

mnazim commented 8 years ago

@gcanti

Thank you, good sir! That was it. formOptions with a new reference was need.