jurassix / react-validation-mixin

Simple validation mixin (HoC) for React.
MIT License
283 stars 38 forks source link

Calling this.props.validate on componentDidMount #76

Open Mottoweb opened 7 years ago

Mottoweb commented 7 years ago

I need to invalidate the form on mount. Calling this.props.validate in componentDidMount return an invariant error.

Invariant Violation: Data was not provided to the Validator. Implement "getValidatorData" to return data.

Adding directly to wrapped component looks like terrible thing to me, is there any other way to make this work?

jurassix commented 7 years ago

@Mottoweb can you provide me a little example. You should be able to call validate on mount.

Mottoweb commented 7 years ago

here is my component

import React                      from 'react'
import { ExnessBaseComponent }    from 'exness-libraries'
import validation                 from 'react-validation-mixin'
import strategy                   from 'react-validatorjs-strategy'
import classNames                 from 'classnames'
import { verificationValidatorTypes } from './helpers/verification-validator-types'

if (!window.Signup) window.Signup = {}
const Signup = window.Signup

class EmailForm extends ExnessBaseComponent {
  state = {
    email: 'adere@adere.com',
    phone: '+357 99911330',
    countdownState: true,
    emailCode: '',
    phoneCode: '',
  }
  i18n = Signup.i18n
  constructor(props) {
    super(props)
    this.i18n = Signup.i18n
    this.validatorTypes = verificationValidatorTypes
    this.bindAll(
      'renderPhoneText',
      'renderEmailText',
      'renderPhoneResendText',
      'renderEmailResendText',
      'onSubmit',
      'startTimer',
      'tick',
      'replaceString',
      'handleCallback',
      'handleEmailResend',
      'handlePhoneResend',
      'getValidatorData',
      'onChange',
      'onBlur',
      'renderValidationIcon',
      'getButtonClasses',
      'setInterval',
      'validate',
    )
  }

  validate(name) {
    if (!name) return this.props.isValid()
    return this.props.isValid(name)
  }

  getValidatorData() {
    return this.state
  }

  onChange(field, e) {
    const state = {}
    state[field] = e.target.value
    this.setState(state)
  }

  componentDidMount() {
    this.startTimer()
    const resendEmailLink = document.getElementById('verification-email-resend-link')
    if (!resendEmailLink) return
    resendEmailLink.addEventListener('click', this.handleEmailResend)
    this.props.validate()
  }

  componentDidUpdate() {
    const resendPhoneLink = document.getElementById('verification-phone-resend-link')
    const callbackLink = document.getElementById('verification-call-back-link')
    if (!resendPhoneLink || !callbackLink) return
    if (!this.state.countdownState) {
      resendPhoneLink.addEventListener('click', this.handlePhoneResend)
      callbackLink.addEventListener('click', this.handleCallback)
    } else {
      resendPhoneLink.removeEventListener('click', this.handlePhoneResend)
      callbackLink.removeEventListener('click', this.handleCallback)
    }
  }

  componentWillUnmount() {
    clearInterval(this.interval)
    const resendEmailLink = document.getElementById('verification-email-resend-link')
    if (!resendEmailLink) return
    resendEmailLink.removeEventListener('click', this.handleEmailResend)
  }

  startTimer() {
    this.setState({
      secondsRemaining: 60,
      countdownState: true,
    }, this.setInterval())
  }

  setInterval() {
    this.interval = setInterval(this.tick, 1000)
  }

  tick() {
    this.setState({
      secondsRemaining: this.state.secondsRemaining - 1,
    })
    if (this.state.secondsRemaining <= 0) {
      clearInterval(this.interval)
      this.setState({
        countdownState: false,
      })
    }
  }

  replaceString(string, value) {
    return string.replace(/<%([^%>]+)?%>/, value)
  }

  onSubmit(event) {
    if (event) event.preventDefault()
    const onValidate = error => {
      if (error) {
        // yo
      } else {
        // yo
      }
    }
    this.props.validate(onValidate)
  }

  render() {
    return (
      <div>
        <div className="signup-verification__wrapper">
          <input
            type="text"
            className="ui-input ui-input__middleBigTidy"
            value={this.state.phoneCode}
            onChange={this.onChange.bind(this, 'phoneCode')}
            onBlur={this.props.handleValidation('phoneCode')}
          />
          {this.renderValidationIcon('phoneCode')}
        </div>
        <div className="signup-verification__wrapper">
          <input
            type="text"
            className="ui-input ui-input__middleBigTidy"
            value={this.state.emailCode}
            onChange={this.onChange.bind(this, 'emailCode')}
            onBlur={this.props.handleValidation('emailCode')}
          />
        {this.renderValidationIcon('emailCode')}

        </div>
        <button
          className={this.getButtonClasses()}
          onClick={this.onSubmit}
        >
          {this.i18n.Confirm}
        </button>
      </div>
    )
  }

  renderValidationIcon(name) {
    const isValid = this.validate(name)
    const className = classNames({
      'verification-text': true,
      valid: isValid,
      invalid: !isValid,
    })
    return (
      <p className={className}>
        {isValid ? this.i18n.Valid : this.i18n.Invalid}
      </p>
    )
  }

  getButtonClasses() {
    return classNames({
      'ui-btn': true,
      'signup-verification__margin-top': true,
      'ui-btn__disabled': !this.validate(),
    })
  }

}

export default validation(strategy)(EmailForm)
jurassix commented 7 years ago

@Mottoweb hmm my guess is that you need to create your state object in your constructor. It looks like your getValidatorData call is returning undefined initially after mount.

Try this:

constructor(props) {
  // ...
  this.state = {};
}
jurassix commented 7 years ago

Hmm ok second look I see you have the state define on the class? try moving it into the constructor:

constructor(props) {
  // ...
  this.state =  {
    email: 'adere@adere.com',
    phone: '+357 99911330',
    countdownState: true,
    emailCode: '',
    phoneCode: '',
  };
}
Mottoweb commented 7 years ago

Did not help :( Temporary workaround for me - set this.props.errors in constructor to invalidate specific field and/or form. Once the true validations takes place, parent state.errors is empty or repopulated with true error messages.

jurassix commented 7 years ago

Can you try binding the getValiadatorData method in the constructor? Or even put a dubugger inside this function to see if it's called. It seems to me that this.state must not be resolving correctly when getValidatorData is called on Mount.

constructor(props) {
  //...
  this.getValidatorData = this.getValidatorData.bind(this);
}

Edit: I now see the binding for getValidatorData, try the debugger inside the method and inspect this.state. Let me know what you see.

jurassix commented 7 years ago

Follow up ? - Can you try to extend React.Component instead of the ExnessBaseComponent there could be some conflict in the extention of this base class. We may need to simplify your example to find the root issue.

camilacnery commented 7 years ago

Hey, I’m having the exact same problem. I have a component that used to work as expected when I was using React.createClass but today I refactored it to use ES6 class and got this problem. I have to validate the data when the component mounts to enable the button that will allow the user to proceed depending on the data that loads in the state. The component has a lot of stuff but I simplified the code to the maximum to test it out and still got the problem. The form validates as expected if I remove the validation call from componentDidMount but when I add it I get this error and the validation doesn't work at all after that. Also, from what I can see getValidatorData is not even being called. Here is my problematic code: https://gist.github.com/camilacnery/4be2543e5803f95481b17f42b0a14652

I managed to get around the problem by starting with an empty state and calling a function in componentDidMount to set the state with the information I want. It's not ideal but it'll do for now... Let me know if I can do anything else to help find the source of the problem. Here is the code that works: https://gist.github.com/camilacnery/96e60d283faae4bf4a9dda7a7e7c822a

jurassix commented 7 years ago

@camilacnery thanks for putting together these examples. I'll try to get a resolution soon.