Closed heltonvalentini closed 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';
}
}
I'm trying to satisfy the following case:
A
is requiredA
, when not empty must go through some calculations to verify that it's a valid entryUsing 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.
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.
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));
}
...
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);
}
});
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);
}
});
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 ?
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.
@jurassix when you say extract Joi strategy you're talking about this? https://github.com/hapijs/joi/blob/master/examples/customMessage.js
@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
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'?
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 .
+1
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 ?
@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
@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.
Joi allows custom message creation on validation:
@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?
I'm having the same problem as @idealistic - can we get rid of the label in our custom error messages?
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.
Cool, thanks
@avrame, It is working for me now. You can override the default behavior by doing
'!!' + customeMessage
https://github.com/hapijs/joi/commit/08d974e43bfb04a7b8166321d573e7f3b3d3e750
@idealistic thx! I'll add this to the documentation.
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 ?