2 * Used for inputs on a validation form.
3 * All properties will be passed on to the input element.
5 * The following properties can be set for OOB validations and callbacks:
6 - required: Boolean: Should be set to true if the input must have a value
7 - numeric: Boolean : Should be set to true id the input should be an integer
8 - onChange : Function : Will be called to validate the value if the default validations are not sufficient, should return a boolean value
9 indicating whether the value is valid
10 - didUpdateCallback :Function: Will be called after the state has been updated and the component has rerendered. This can be used if
11 there are dependencies between inputs in a form.
13 * The following properties of the state can be set to determine
14 * the state of the input from outside components:
15 - isValid : Boolean - whether the value is valid
16 - value : value for the input field,
18 - required : Boolean - whether the input value must be filled out.
20 import React from 'react';
21 import ReactDOM from 'react-dom';
22 import Validator from 'validator';
23 import FormGroup from 'react-bootstrap/lib/FormGroup.js';
24 import Input from 'react-bootstrap/lib/Input.js';
25 import Overlay from 'react-bootstrap/lib/Overlay.js';
26 import Tooltip from 'react-bootstrap/lib/Tooltip.js';
27 import isEqual from 'lodash/isEqual.js';
28 import i18n from 'nfvo-utils/i18n/i18n.js';
29 import JSONSchema from 'nfvo-utils/json/JSONSchema.js';
30 import JSONPointer from 'nfvo-utils/json/JSONPointer.js';
33 import InputOptions from '../inputOptions/InputOptions.jsx';
35 const globalValidationFunctions = {
36 required: value => value !== '',
37 maxLength: (value, length) => Validator.isLength(value, {max: length}),
38 minLength: (value, length) => Validator.isLength(value, {min: length}),
39 pattern: (value, pattern) => Validator.matches(value, pattern),
42 // to allow empty value which is not zero
45 return Validator.isNumeric(value);
47 maxValue: (value, maxValue) => value < maxValue,
48 minValue: (value, minValue) => value >= minValue,
49 alphanumeric: value => Validator.isAlphanumeric(value),
50 alphanumericWithSpaces: value => Validator.isAlphanumeric(value.replace(/ /g, '')),
51 validateName: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-/g, ''), 'en-US'),
52 validateVendorName: value => Validator.isAlphanumeric(value.replace(/[\x7F-\xFF]|\s/g, ''), 'en-US'),
53 freeEnglishText: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-|\,|\(|\)|\?/g, ''), 'en-US'),
54 email: value => Validator.isEmail(value),
55 ip: value => Validator.isIP(value),
56 url: value => Validator.isURL(value)
59 const globalValidationMessagingFunctions = {
60 required: () => i18n('Field is required'),
61 maxLength: (value, maxLength) => i18n('Field value has exceeded it\'s limit, {maxLength}. current length: {length}', {
65 minLength: (value, minLength) => i18n('Field value should contain at least {minLength} characters.', {minLength}),
66 pattern: (value, pattern) => i18n('Field value should match the pattern: {pattern}.', {pattern}),
67 numeric: () => i18n('Field value should contain numbers only.'),
68 maxValue: (value, maxValue) => i18n('Field value should be less than: {maxValue}.', {maxValue}),
69 minValue: (value, minValue) => i18n('Field value should be at least: {minValue}.', {minValue}),
70 alphanumeric: () => i18n('Field value should contain letters or digits only.'),
71 alphanumericWithSpaces: () => i18n('Field value should contain letters, digits or spaces only.'),
72 validateName: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
73 validateVendorName: ()=> i18n('Field value should contain English letters digits and spaces only.'),
74 freeEnglishText: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
75 email: () => i18n('Field value should be a valid email address.'),
76 ip: () => i18n('Field value should be a valid ip address.'),
77 url: () => i18n('Field value should be a valid url address.'),
78 general: () => i18n('Field value is invalid.')
81 class ValidationInput extends React.Component {
83 static contextTypes = {
84 validationParent: React.PropTypes.any,
85 isReadOnlyMode: React.PropTypes.bool,
86 validationSchema: React.PropTypes.instanceOf(JSONSchema),
87 validationData: React.PropTypes.object
90 static defaultProps = {
93 didUpdateCallback: null,
99 type: React.PropTypes.string.isRequired,
100 onChange: React.PropTypes.func,
101 disabled: React.PropTypes.bool,
102 didUpdateCallback: React.PropTypes.func,
103 validations: React.PropTypes.object,
104 isMultiSelect: React.PropTypes.bool,
105 onOtherChange: React.PropTypes.func,
106 pointer: React.PropTypes.string
113 value: this.props.value,
115 previousErrorMessage: '',
117 validations: this.props.validations,
118 isMultiSelect: this.props.isMultiSelect
121 componentWillMount() {
122 if (this.context.validationSchema) {
123 let {validationSchema: schema, validationData: data} = this.context,
124 {pointer} = this.props;
126 if (!schema.exists(pointer)) {
127 console.error(`Field doesn't exists in the schema ${pointer}`);
130 let value = JSONPointer.getValue(data, pointer);
131 if (value === undefined) {
132 value = schema.getDefault(pointer);
133 if (value === undefined) {
137 this.setState({value});
139 let enums = schema.getEnum(pointer);
141 let values = enums.map(value => ({enum: value, title: value, groupName: pointer})),
142 isMultiSelect = schema.isArray(pointer);
144 if (!isMultiSelect && this.props.type !== 'radiogroup') {
145 values = [{enum: '', title: i18n('Select...')}, ...values];
147 if (isMultiSelect && Array.isArray(value) && value.length === 0) {
154 onEnumChange: value => this.changedInputOptions(value),
159 this.setState({validations: this.extractValidationsFromSchema(schema, pointer, this.props)});
163 extractValidationsFromSchema(schema, pointer, props) {
164 /* props are here to get precedence over the scheme definitions */
165 let validations = {};
167 if (schema.isRequired(pointer)) {
168 validations.required = true;
171 if (schema.isNumber(pointer)) {
172 validations.numeric = true;
174 const maxValue = props.validations.maxValue || schema.getMaxValue(pointer);
175 if (maxValue !== undefined) {
176 validations.maxValue = maxValue;
179 const minValue = props.validations.minValue || schema.getMinValue(pointer);
180 if (minValue !== undefined) {
181 validations.minValue = minValue;
186 if (schema.isString(pointer)) {
188 const pattern = schema.getPattern(pointer);
190 validations.pattern = pattern;
193 const maxLength = schema.getMaxLength(pointer);
194 if (maxLength !== undefined) {
195 validations.maxLength = maxLength;
198 const minLength = schema.getMinLength(pointer);
199 if (minLength !== undefined) {
200 validations.minLength = minLength;
207 componentWillReceiveProps({value: nextValue, validations: nextValidations, pointer: nextPointer}, nextContext) {
208 const {validations, value} = this.props;
209 const validationsChanged = !isEqual(validations, nextValidations);
210 if (nextContext.validationSchema) {
211 if (this.props.pointer !== nextPointer ||
212 this.context.validationData !== nextContext.validationData) {
213 let currentValue = JSONPointer.getValue(this.context.validationData, this.props.pointer),
214 nextValue = JSONPointer.getValue(nextContext.validationData, nextPointer);
215 if(nextValue === undefined) {
218 if (this.state.isMultiSelect && Array.isArray(nextValue) && nextValue.length === 0) {
221 if (currentValue !== nextValue) {
222 this.setState({value: nextValue});
224 if (validationsChanged) {
226 validations: this.extractValidationsFromSchema(nextContext.validationSchema, nextPointer, {validations: nextValidations})
231 if (validationsChanged) {
232 this.setState({validations: nextValidations});
234 if (this.state.wasInvalid && (value !== nextValue || validationsChanged)) {
235 this.validate(nextValue, nextValidations);
236 } else if (value !== nextValue) {
237 this.setState({value: nextValue});
242 shouldTypeBeNumberBySchemeDefinition(pointer) {
243 return this.context.validationSchema &&
244 this.context.validationSchema.isNumber(pointer);
248 return this.context.validationSchema &&
249 this.context.validationSchema.getEnum(pointer);
253 let {value, isMultiSelect, values, onEnumChange, style, isValid, validations} = this.state;
254 let {onOtherChange, type, pointer} = this.props;
255 if (this.shouldTypeBeNumberBySchemeDefinition(pointer) && !this.hasEnum(pointer)) {
258 let props = {...this.props};
260 let groupClasses = this.props.groupClassName || '';
261 if (validations.required) {
262 groupClasses += ' required';
264 let isReadOnlyMode = this.context.isReadOnlyMode;
266 if (value === true && (type === 'checkbox' || type === 'radio')) {
267 props.checked = true;
270 <div className='validation-input-wrapper'>
272 !isMultiSelect && !onOtherChange && type !== 'select' && type !== 'radiogroup'
276 groupClassName={groupClasses}
279 disabled={isReadOnlyMode || Boolean(this.props.disabled)}
281 onChange={() => this.changedInput()}
282 onBlur={() => this.blurInput()}>
283 {this.props.children}
287 type === 'radiogroup'
291 <Input disabled={isReadOnlyMode || Boolean(this.props.disabled)}
293 ref={'_myInput' + (typeof val.enum === 'string' ? val.enum.replace(/\W/g, '_') : val.enum)}
294 value={val.enum} checked={value === val.enum}
295 type='radio' label={val.title}
297 onChange={() => this.changedInput()}/>
303 (isMultiSelect || onOtherChange || type === 'select')
305 onInputChange={() => this.changedInput()}
306 onBlur={() => this.blurInput()}
309 isMultiSelect={isMultiSelect}
311 onEnumChange={onEnumChange}
313 multiSelectedEnum={value}
316 {this.renderOverlay()}
322 let position = 'right';
323 if (this.props.type === 'text'
324 || this.props.type === 'email'
325 || this.props.type === 'number'
326 || this.props.type === 'password'
332 let validationMessage = this.state.error.message || this.state.previousErrorMessage;
335 show={!this.state.isValid}
338 let target = ReactDOM.findDOMNode(this.refs._myInput);
339 return target.offsetParent ? target : undefined;
343 id={`error-${validationMessage.replace(' ', '-')}`}
344 className='validation-error-message'>
351 componentDidMount() {
352 if (this.context.validationParent) {
353 this.context.validationParent.register(this);
357 componentDidUpdate(prevProps, prevState) {
358 if (this.context.validationParent) {
359 if (prevState.isValid !== this.state.isValid) {
360 this.context.validationParent.childValidStateChanged(this, this.state.isValid);
363 if (this.props.didUpdateCallback) {
364 this.props.didUpdateCallback();
369 componentWillUnmount() {
370 if (this.context.validationParent) {
371 this.context.validationParent.unregister(this);
375 isNumberInputElement() {
376 return this.props.type === 'number' || this.refs._myInput.props.type === 'number';
380 * Adding same method as the actual input component
384 if (this.props.type === 'checkbox') {
385 return this.refs._myInput.getChecked();
387 if (this.props.type === 'radiogroup') {
388 for (let key in this.refs) { // finding the value of the radio button that was checked
389 if (this.refs[key].getChecked()) {
390 return this.refs[key].getValue();
394 if (this.isNumberInputElement()) {
395 return Number(this.refs._myInput.getValue());
398 return this.refs._myInput.getValue();
402 this.setState({value: this.props.value});
407 * internal method that validated the value. includes callback to the onChange method
409 * @param validations - map containing validation id and the limitation describing the validation.
412 validateValue = (value, validations) => {
413 let {customValidationFunction} = validations;
416 for (let validation in validations) {
417 if ('customValidationFunction' !== validation) {
418 if (validations[validation]) {
419 if (!globalValidationFunctions[validation](value, validations[validation])) {
420 error.id = validation;
421 error.message = globalValidationMessagingFunctions[validation](value, validations[validation]);
427 let customValidationResult = customValidationFunction(value);
429 if (customValidationResult !== true) {
432 if (typeof customValidationResult === 'string') {//custom validation error message supplied.
433 error.message = customValidationResult;
435 error.message = globalValidationMessagingFunctions.general();
451 * Internal method that handles the change event of the input. validates and updates the state.
455 let {isValid, error} = this.state.wasInvalid ? this.validate() : this.state;
456 let onChange = this.props.onChange;
458 onChange(this.getValue(), isValid, error);
460 if (this.context.validationSchema) {
461 let value = this.getValue();
462 if (this.state.isMultiSelect && value === '') {
465 if (this.shouldTypeBeNumberBySchemeDefinition(this.props.pointer)) {
466 value = Number(value);
468 this.context.validationParent.onValueChanged(this.props.pointer, value, isValid, error);
472 changedInputOptions(value) {
473 this.context.validationParent.onValueChanged(this.props.pointer, value, true);
477 if (!this.state.wasInvalid) {
478 this.setState({wasInvalid: true});
481 let {isValid, error} = !this.state.wasInvalid ? this.validate() : this.state;
482 let onBlur = this.props.onBlur;
484 onBlur(this.getValue(), isValid, error);
488 validate(value = this.getValue(), validations = this.state.validations) {
489 let validationStatus = this.validateValue(value, validations);
490 let {isValid, error} = validationStatus;
491 let _style = isValid ? null : 'error';
496 previousErrorMessage: this.state.error.message || '',
498 wasInvalid: !isValid || this.state.wasInvalid
501 return validationStatus;
505 return this.state.isValid;
509 export default ValidationInput;