jurassix / react-validation-mixin

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

Custom messages and validations #14

Closed heltonvalentini closed 9 years ago

heltonvalentini commented 9 years ago

I'm not sure if this is an issue but docs are not very clear about adding custom validations and/or messages. Is it possible ?

jurassix commented 9 years ago

Custom validations are limited to what Joi provides. Let me know if you have specific case that you cannot satisfy with Joi.

Custom messages are available within this library. Currently the getValidationMessages() API simply returns the Joi validation messages. But this library also provides an API for isValid() which you can use to return your own custom messages.

Example:

getCustomUsernameMessage: function () {
  if (!this.isValid('username') {
    return 'Custom message here';
  }
}
heltonvalentini commented 9 years ago

I'm trying to satisfy the following case:

Using my application specific case i'm trying to validate a field name CPF (wich is like SSN in Brazil). A CPF has 11 numbers, but the last 2 are calculated based on the first 9. So when user leaves field blank I'd like to show required message. When field has any input I'd to validated it using a function and show "Invalid CPF" error message.

This easily done using Revalidator but I couldn't find a way to do it using Joi. When using Revalidator I use required and conform options along with specifics messages for each case.

jurassix commented 9 years ago

I think you should open an issue with Joi and see what the community can offer as a holistically Joi solution. Please link the open issue here too so I can follow the progress and contribute if I can solve the issue too.

I know that Joi has open issues with supporting custom validations; that will be a big win for this library when that problem is solved.

Given your use case this library will support the behavior your looking for, provided the initial validation can be defined for your CPF field.

jurassix commented 9 years ago

Actually on second read of your problem I think you can solve this within this libraries current implementation.

Your code would be something like the following (refer to the example component to fill in the pieces):

...
validatorTypes: {
  cdf: Joi.string().alphanum().length(11).required().label('Field')
},
render: () {
  return (
    <form onSubmit={this.handleSubmit}>
       <label>CDF</label>
       <input type='text' ref='cdf' onBlur={this.deriveAndValidateCDF} valueLink={this.linkState('cdf')}/>
       {this.getValidationMessages('cdf').map(this.renderHelpText)}
    </form>
  );
},
deriveAndValidateCDF: function () {
  var cdfValue = this.state.cdf;
  //do your transformation of cdfValue
  this.setState({'cdf', cdfValue}, (function() {
    this.validate('cdf');
  }).bind(this));
}
...
heltonvalentini commented 9 years ago

I put an example together. But i didn't understand how I am supposed to invalidate field.

var TestForm = React.createClass({
    displayName: 'TestForm',
    mixins: [ValidationMixin, React.addons.LinkedStateMixin],
    validatorTypes: {
        cpf: Joi.string().alphanum().length(11).required().label('Field')
    },
    getInitialState: function () {
        return {
            cpf: null
        }
    },
    render: function () {
        return (
            <form onSubmit={this.handleSubmit}>
                <div className='form-group'>
                    <label>CPF</label>
                    <input type='text' ref='cpf' onBlur={this.deriveAndValidateCPF} valueLink={this.linkState('cpf')}/>
                    {this.getValidationMessages('cpf').map(this.renderHelpText)}
                </div>
                <div className='form-group'>
                    <h3>{this.state.feedback}</h3>
                </div>
            </form>
        )
    },
    deriveAndValidateCPF: function () {
        var cpfValue = this.state.cpf;
        //do your transformation of cpfValue
        if(cpf === '12345678900') {
            //Set input invalid
        }
        this.setState({'cpf': cpfValue}, (function() {
            this.validate('cpf');
        }).bind(this));
    },
    renderHelpText: function (message) {
        return (
            <span className="help-block">{message}</span>
        );
    },
    handleSubmit: function (event) {
        event.preventDefault();
        var onValidate = function(error, validationErrors) {
            if (error) {
                this.setState({
                    feedback: 'Form is invalid do not submit'
                });
            } else {
                this.setState({
                    feedback: 'Form is valid send to action creator'
                });
            }
        }.bind(this);
        this.validate(onValidate);
    }
});
jurassix commented 9 years ago

You can manually invalidate by operating on the this.state.errors object.

var errors = this.state.errors || {};
errors['cpf'] = ['Field is invalid'];
this.setState('errors', errors);

But you will loose this manual state when this.validate() is called.

I think you need to operate outside of the mixin for this field. See below, notice how I have removed the validatorType for this field.

var TestForm = React.createClass({
    displayName: 'TestForm',
    mixins: [ValidationMixin, React.addons.LinkedStateMixin],
    validatorTypes: {
       // validate other fields in the form
    },
    getInitialState: function () {
        return {
            cpf: null,
            isCpfValid: false
        }
    },
    render: function () {
        var cpfMessage = this.state.isCpfValid ? null : 'Field is Invalid';
        return (
            <form onSubmit={this.handleSubmit}>
                <div className='form-group'>
                    <label>CPF</label>
                    <input type='text' ref='cpf' onBlur={this.validateCPF} valueLink={this.linkState('cpf')}/>
                    {this.renderHelpText(cpfMessage)}
                </div>
                <div className='form-group'>
                    <h3>{this.state.feedback}</h3>
                </div>
            </form>
        )
    },
    validateCPF: function () {
        var cpf = this.state.cpf;
        //do your transformation of cpf
        this.setState({'isCpfValid': this.isCpfValid(cpf)});
    },
    isCpfValid: function(cpf) {
         cpf = cpf || this.state.cpf;
         if(cpf === '12345678900') {
            return false;
        } else {
          return true;
        }
    },
    renderHelpText: function (message) {
        return (
            <span className="help-block">{message}</span>
        );
    },
    handleSubmit: function (event) {
        event.preventDefault();
        var onValidate = function(error, validationErrors) {
            if (error || !this.isCpfValid()) {
                //add custom error flag to state
                this.setState({
                    feedback: 'Form is invalid do not submit',
                    isCpfValid: false
                });
            } else {
                this.setState({
                    feedback: 'Form is valid send to action creator',
                    isCpfValid: true
                });
            }
        }.bind(this);
        this.validate(onValidate);
    }
});
heltonvalentini commented 9 years ago

Yep that worked out fine. But I think that this won't be a viable solution because I have other fields that need this kind of calculations. I ended up creating a RevalidatorStrategy. In my opinion it is the best option.

var Revalidator = require('revalidator');
var union = require('lodash.union');

var RevalidatorValidationStrategy = {
    validate: function(revalidatorSchema, data, key) {
        revalidatorSchema = revalidatorSchema || {};
        data = data || {};

        var errors = this._format(Revalidator.validate(data, revalidatorSchema));
        if (key === undefined) {
            union(Object.keys(revalidatorSchema), Object.keys(data)).forEach(function(error) {
                errors[error] = errors[error] || [];
            });
            return errors;
        } else {
            var result = {};
            result[key] = errors[key];
            return result;
        }
    },

    _format: function(revalidatorResult) {
        if (revalidatorResult.error !== null) {
            return revalidatorResult.errors.reduce(function(memo, detail) {
                if (!Array.isArray(memo[detail.property])) {
                    memo[detail.property] = [];
                }
                memo[detail.property].push(detail.message);
                return memo;
            }, {});
        } else {
            return {};
        }
    }

};

module.exports = RevalidatorValidationStrategy;

And then my on a form:

var TestForm = React.createClass({
    displayName: 'TestForm',
    mixins: [ValidationMixin, React.addons.LinkedStateMixin],
    validatorTypes: {
        properties: {
            cpf: {
                type: 'string',
                required: true,
                conform: function(v) {
                    return v === '12345678900';
                },
                messages: {
                    type: 'CPF is required',
                    conform: 'Invalid CPF'
                }
            },
            firstName: {
                type: 'string',
                required: true,
                message: 'Firstname field is required'
            },
            username: {
                type: 'string',
                required: true,
                message: 'Username is required'
            }
        }
    },
    getInitialState: function () {
        return {
            cpf: null,
            firstName: null,
            username: null
        }
    },
    render: function () {
        return (
            <form onSubmit={this.handleSubmit}>
                <div>
                    <label htmlFor='firstName'>First Name</label>
                    <input
                        type='text'
                        id='firstName'
                        valueLink={this.linkState('firstName')}
                        onBlur={this.handleValidation('firstName')}
                        className='form-control'
                        placeholder='First Name'
                    />
                    {this.getValidationMessages('firstName').map(this.renderHelpText)}
                </div>
                <div>
                    <label htmlFor='username'>Username</label>
                    <input
                        type='text'
                        id='username'
                        valueLink={this.linkState('username')}
                        onBlur={this.handleValidation('username')}
                        className='form-control'
                        placeholder='Username'
                    />
                    {this.getValidationMessages('username').map(this.renderHelpText)}
                </div>
                <div>
                    <label htmlFor='username'>CPF</label>
                    <input
                        type='text'
                        id='username'
                        valueLink={this.linkState('cpf')}
                        onBlur={this.handleValidation('cpf')}
                        className='form-control'
                        placeholder='CPF'
                    />
                    {this.getValidationMessages('cpf').map(this.renderHelpText)}
                </div>
                <div className="Grid-cell">
                    <button type="submit" className="Button Button--amarelo">Enviar <i className="icon-seta_avancar"></i></button>
                    <h3>{this.state.feedback}</h3>
                </div>
            </form>
        )
    },
    renderHelpText: function (message) {
        return (
            <span className="help-block">{message}</span>
        );
    },
    handleSubmit: function (event) {
        event.preventDefault();
        var onValidate = function(error, validationErrors) {
            if (error) {
                this.setState({
                    feedback: 'Form is invalid do not submit'
                });
            } else {
                this.setState({
                    feedback: 'Form is valid send to action creator'
                });
            }
        }.bind(this);
        this.validate(onValidate);
    }
});

What are you thoughts about multiple strategies ?

jurassix commented 9 years ago

Nice! I agree this is the correct solution. I think the next steps will be to create a separate repositories for each strategy. I will need to document the API for future strategies, and provide an API for using various strategies at runtime.

I'll setup a test suite using both strategies. Once all tests are passing I'll update the documentation to promote the various strategies.

I can extract the Joi strategy, I assume you would like to take on the RevalidatorStrategy?

Let me know your thoughts. Thanks.

chiefjester commented 9 years ago

@jurassix when you say extract Joi strategy you're talking about this? https://github.com/hapijs/joi/blob/master/examples/customMessage.js

jurassix commented 9 years ago

@deezahyn The link you provided shows how to customize the label that Joi will return when a validation error occurs. What the OP was having issues with is Joi's current lack of providing a custom validation function. Since this lib is designed as a FactoryPattern we can easily add Strategies for other validation engines; Joi, Revalidator, etc. The end of this issue was suggesting that we breakout the current embedded Joi strategy and move that to it's own repository.

I'm working towards this approach. Still need to provide a well-defined Strategy interface and documentation.

Does this help?

Edit: the above examle does in fact allow custom messages via overriding language options. Read the docs on Custom Messages and I18N

jurassix commented 9 years ago

@deezahyn you can read more about it here

chiefjester commented 9 years ago

oh okay my bad, I thought that was validation messages already implemented. So if we are to abstract the message, what do you mean by 'extract Joi strategy'?

jurassix commented 9 years ago

I simply mean to externalize this file https://github.com/jurassix/react-validation-mixin/blob/master/JoiValidationStrategy.js into a separate repo. This will easily allow other validation strategies to be implemented using the same Component API of this library. So the OP created a RevalidatorStrategy, it too would live in a separate repo. Now this library could support both Joi, Revalidator, etc.

On Sat, May 9, 2015 at 9:09 AM, deezahyn notifications@github.com wrote:

oh okay my bad, I thought that was validation messages already implemented. So if we are to abstract the message, what do you mean by 'extract Joi strategy'?

— Reply to this email directly or view it on GitHub https://github.com/jurassix/react-validation-mixin/issues/14#issuecomment-100483595 .

chiefjester commented 9 years ago

+1

heltonvalentini commented 9 years ago

Yep let me know if you need any help on separating strategies, defining API, documenting API. Do you think we should close this issue and crete new ones ?

heltonvalentini commented 9 years ago

@jurassix As soon as we have a Strategy API I'm gonna build a Strategy for this lib. https://github.com/jquense/yup If think it has all the upsides of Joi but none of this weight

jurassix commented 9 years ago

@heltonvalentini if you want to officially create the revalidator-stategy nows the time. Strategies are pluggable now. Checkout joi-validation-strategy and the docs below.

Documentation for 5.0

jurassix commented 9 years ago

Joi allows custom message creation on validation:

Checkout the docs on Custom Messages and I18N

idealistic commented 8 years ago

@jurassix, I am passing custom error message but the label is showing up in the custom error message. Example, the following code example is returning "Password" Please enter your password.

while I only need the custom error message to show up, just Please enter your password.

password: joi.string().label('Password')
                .required()
                .options(
                                       language: {
                               any: {
                                  empty: "Please enter your password."
                               }
                            }
                            )

Any idea why?

avrame commented 8 years ago

I'm having the same problem as @idealistic - can we get rid of the label in our custom error messages?

jurassix commented 8 years ago

I experienced the same issue before and followed up with Joi maintainers. Joi should not be putting the "label" in there. I'll try to update my demo app to use the options override and report back when I figure it out.

avrame commented 8 years ago

Cool, thanks

idealistic commented 8 years ago

@avrame, It is working for me now. You can override the default behavior by doing '!!' + customeMessage

https://github.com/hapijs/joi/commit/08d974e43bfb04a7b8166321d573e7f3b3d3e750

jurassix commented 8 years ago

@idealistic thx! I'll add this to the documentation.