jurassix / react-validation-mixin

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

Disabling a form submit button until form is valid? #33

Closed 1django closed 9 years ago

1django commented 9 years ago

I'd like to disable my form submit button, and leave it disabled, until the user has (successfully) filled out the form. I'm wondering the preferred method is to achieve this?

My attempts of disabling the form submit button based on this.isValid() have failed. I.e., assuming a form with some validated inputs...my button would look something like this: <button disabled={!this.isValid()} onClick={this.saveAndContinue}>Submit</button>.

On initial rendering, this button is not disabled (I'm guessing because validation has not run on the form yet). The button becomes disabled as soon as I interact with a (validated) input.

I tried calling this.validate() on componentDidMount, which successfully disables the button, but also displays all of the error messages on my form. This is not desirable in my case.

Suggestions on how to achieve disabling the form button until the form is valid, w/o showing all form errors on initial render?

Thanks.

jurassix commented 9 years ago

Ok - let me think about the best practice for this.

1django commented 9 years ago

Thanks.

A workaround is completely acceptable for the moment, as I'm facing a deadline pretty soon!

jurassix commented 9 years ago

ok so here is how I would approach this: note: you need to be on latest version of library > 5.0.4

//other imports
import strategy from 'joi-validation-strategy';

const Component = React.createClass({
//..other methods

componentDidMount: function() {
  this.isFormValid();
},

componentWillUpdate: function() {
  this.isFormValid();
},

isFormValid: function() {
  const options = {
    abortEarly: false,
    allowUnknown: true,
  };
  // might need to inspect the return type here, this has not been tested
  this.setState({
    showSubmit: Object.keys(strategy(options).validate(this.getValidatorData(), this.validatorTypes()).length === 0,
  });    
}
});

export default Component;

So now you have access to the strategy to validate in a stateless way. I think this is the correct pattern. Just add a state variable for showSubmit and it will be enabled whenever the form is fully valid and only then.

jurassix commented 9 years ago

actually this should work pre 5.0 if you just import the strategy. Let me know what version your on.

also edited the above example, the validation call returns an array that needs to me checked for length

1django commented 9 years ago

Thanks for the quick responses.

I'm on version 4.2.0.

jurassix commented 9 years ago

edited example to reflect 4.x

1django commented 9 years ago

Thanks.

I'm getting the following error: Uncaught TypeError: this.getValidatorData is not a function.

The error appears to be coming from: showSubmit: strategy(options).validate(this.getValidatorData(), this.validatorTypes()).error === null,

1django commented 9 years ago

Changing this.validatorTypes() to this.validatorTypes removed the previous error, but led me to another: this.getValidatorData is not a function. Which is true for my component at least, but not sure what this lib exposes.

Here's a slightly condensed version of my component:

var React = require('react/addons');
var ValidationMixin = require('react-validation-mixin');
var Joi = require('joi');
import strategy from 'joi-validation-strategy';

module.exports = React.createClass({
    mixins: [ValidationMixin, React.addons.LinkedStateMixin],
    validatorTypes:  {
        firstName: Joi.string().required().regex(nameRegex).max(255).label('First Name'),
    },
    getInitialState: function() {
        return {
            showSubmit: false,
            firstName: null,
        }
    },
    componentDidMount: function() {
        this.isFormValid();
    },

    componentWillUpdate: function() {
        this.isFormValid();
    },

    isFormValid: function() {
         const options = {
            abortEarly: false,
            allowUnknown: true,
        };
        this.setState({
            showSubmit: strategy(options).validate(this.getValidatorData(), this.validatorTypes).error === null,
        });
    },

   render: function(){
        return (
            <div>
                <div className={this.getClasses('firstName')}>
                    <label htmlFor="firstName" className={this.getLabelClasses('firstName')}>First Name</label>
                    <input type="text" className="form-control" id="firstName" ref="firstName" defaultValue={this.props.fieldValues.firstName} placeholder="First Name" valueLink={this.linkState('firstName')} onBlur={this.handleValidation('firstName')} />
                    {this.renderCustomHelpText('firstName')}
                </div>
                <div className="col-md-12">
                    <button disabled={!this.showSubmit} id="continue-btn" className="btn btn-primary" onClick={this.saveAndContinue}>Save and Continue</button>
                </div>
            </div> 
        )
    },

    // more methods
});
jurassix commented 9 years ago

if your component did not implement getValididatorData then replace it with this.state

  // might need to inspect the return type here, this has not been tested
  this.setState({
    showSubmit: Object.keys(strategy(options).validate(this.state, this.validatorTypes).length === 0,
  });  
jurassix commented 9 years ago

also your button is incorrect: disabled={!this.showSubmit}

should be: disabled={!this.state.showSubmit}

jurassix commented 9 years ago

And if your not using es6 transpiler replace 'const' with 'var' and

import strategy from 'joi-validation-strategy';

with

var strategy = require( 'joi-validation-strategy');

1django commented 9 years ago

Seems closer, but not quite there.

With the above changes, and the following:

    isFormValid: function() {
        const options = {
            abortEarly: false,
            allowUnknown: true,
        };
        this.setState({
            showSubmit: Object.keys(strategy(options).validate(this.state, this.validatorTypes)).length === 0,
        });
    }

I get a Uncaught RangeError: Maximum call stack size exceeded error.

The error can be averted by commenting out the call to this.isFormValid() in componentWillUpdate, but of course then the showSubmit state variable is never updated, and the form submit is never possible.

Thank you again for your help. Much appreciated...

jurassix commented 9 years ago

Ok try componentWillReceiveProps instead

1django commented 9 years ago

This is likely what you were trying to do, just more succinctly and leveraging reacts component lifecycle, but here is what finally worked for me:

var React = require('react/addons');
var ValidationMixin = require('react-validation-mixin');
var Joi = require('joi');
var strategy = require( 'joi-validation-strategy');

module.exports = React.createClass({
    mixins: [ValidationMixin, React.addons.LinkedStateMixin],
    validatorTypes:  {
        firstName: Joi.string().required().regex(nameRegex).max(255).label('First Name')
    },
    getInitialState: function() {
        return {
            showSubmit: false,
            firstName: null,
        };
    },

    componentDidMount: function() {
        this.isFormValid();
    },

    // callback for this.handleValidation onBlur of form inputs 
    // my hacky substitute for using componentWillUpdate
    handleValidationCallback: function(){
        this.isFormValid();
    },

    isFormValid: function() {
         const options = {
            abortEarly: false,
            allowUnknown: true,
        };

        var valMessages = strategy(options).validate(this.state, this.validatorTypes),
            isValid = true;

        for (var p in valMessages) {
            if(valMessages.hasOwnProperty(p) && valMessages[p].length){
                isValid = false;
                break;
            }
        }

        this.setState({
            showSubmit: isValid
        });
    },

    render: function(){
        return (
            <div>

                <div className="col-md-6">
                    <fieldset>
                        <div className={this.getClasses('firstName')}>
                            <label htmlFor="firstName" className={this.getLabelClasses('firstName')}>First Name</label>
                            <input type="text" className="form-control" id="firstName" ref="firstName" defaultValue={this.props.fieldValues.firstName} placeholder="First Name" valueLink={this.linkState('firstName')} onBlur={this.handleValidation('firstName', this.handleValidationCallback)} />
                            {this.renderCustomHelpText('firstName')}
                        </div>
                    </fieldset>
                </div>

                <div className="col-md-12">
                    <button disabled={!this.state.showSubmit} id="continue-btn" className="btn btn-primary" onClick={this.saveAndContinue}>Save and Continue</button>
                </div>
            </div>
        )
    },

    // more methods...
});
jurassix commented 9 years ago

nice! glad you got it to workout. I'll try to put together an example of this later too.