| /** |
| * Used for inputs on a validation form. |
| * All properties will be passed on to the input element. |
| * |
| * The following properties can be set for OOB validations and callbacks: |
| - required: Boolean: Should be set to true if the input must have a value |
| - numeric: Boolean : Should be set to true id the input should be an integer |
| - onChange : Function : Will be called to validate the value if the default validations are not sufficient, should return a boolean value |
| indicating whether the value is valid |
| - didUpdateCallback :Function: Will be called after the state has been updated and the component has rerendered. This can be used if |
| there are dependencies between inputs in a form. |
| * |
| * The following properties of the state can be set to determine |
| * the state of the input from outside components: |
| - isValid : Boolean - whether the value is valid |
| - value : value for the input field, |
| - disabled : Boolean, |
| - required : Boolean - whether the input value must be filled out. |
| */ |
| import React from 'react'; |
| import ReactDOM from 'react-dom'; |
| import Validator from 'validator'; |
| import FormGroup from 'react-bootstrap/lib/FormGroup.js'; |
| import Input from 'react-bootstrap/lib/Input.js'; |
| import Overlay from 'react-bootstrap/lib/Overlay.js'; |
| import Tooltip from 'react-bootstrap/lib/Tooltip.js'; |
| import isEqual from 'lodash/isEqual.js'; |
| import i18n from 'nfvo-utils/i18n/i18n.js'; |
| import JSONSchema from 'nfvo-utils/json/JSONSchema.js'; |
| import JSONPointer from 'nfvo-utils/json/JSONPointer.js'; |
| |
| |
| import InputOptions from '../inputOptions/InputOptions.jsx'; |
| |
| const globalValidationFunctions = { |
| required: value => value !== '', |
| maxLength: (value, length) => Validator.isLength(value, {max: length}), |
| minLength: (value, length) => Validator.isLength(value, {min: length}), |
| pattern: (value, pattern) => Validator.matches(value, pattern), |
| numeric: value => { |
| if (value === '') { |
| // to allow empty value which is not zero |
| return true; |
| } |
| return Validator.isNumeric(value); |
| }, |
| maxValue: (value, maxValue) => value < maxValue, |
| minValue: (value, minValue) => value >= minValue, |
| alphanumeric: value => Validator.isAlphanumeric(value), |
| alphanumericWithSpaces: value => Validator.isAlphanumeric(value.replace(/ /g, '')), |
| validateName: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-/g, ''), 'en-US'), |
| validateVendorName: value => Validator.isAlphanumeric(value.replace(/[\x7F-\xFF]|\s/g, ''), 'en-US'), |
| freeEnglishText: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-|\,|\(|\)|\?/g, ''), 'en-US'), |
| email: value => Validator.isEmail(value), |
| ip: value => Validator.isIP(value), |
| url: value => Validator.isURL(value) |
| }; |
| |
| const globalValidationMessagingFunctions = { |
| required: () => i18n('Field is required'), |
| maxLength: (value, maxLength) => i18n('Field value has exceeded it\'s limit, {maxLength}. current length: {length}', { |
| length: value.length, |
| maxLength |
| }), |
| minLength: (value, minLength) => i18n('Field value should contain at least {minLength} characters.', {minLength}), |
| pattern: (value, pattern) => i18n('Field value should match the pattern: {pattern}.', {pattern}), |
| numeric: () => i18n('Field value should contain numbers only.'), |
| maxValue: (value, maxValue) => i18n('Field value should be less than: {maxValue}.', {maxValue}), |
| minValue: (value, minValue) => i18n('Field value should be at least: {minValue}.', {minValue}), |
| alphanumeric: () => i18n('Field value should contain letters or digits only.'), |
| alphanumericWithSpaces: () => i18n('Field value should contain letters, digits or spaces only.'), |
| validateName: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'), |
| validateVendorName: ()=> i18n('Field value should contain English letters digits and spaces only.'), |
| freeEnglishText: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'), |
| email: () => i18n('Field value should be a valid email address.'), |
| ip: () => i18n('Field value should be a valid ip address.'), |
| url: () => i18n('Field value should be a valid url address.'), |
| general: () => i18n('Field value is invalid.') |
| }; |
| |
| class ValidationInput extends React.Component { |
| |
| static contextTypes = { |
| validationParent: React.PropTypes.any, |
| isReadOnlyMode: React.PropTypes.bool, |
| validationSchema: React.PropTypes.instanceOf(JSONSchema), |
| validationData: React.PropTypes.object |
| }; |
| |
| static defaultProps = { |
| onChange: null, |
| disabled: null, |
| didUpdateCallback: null, |
| validations: {}, |
| value: '' |
| }; |
| |
| static propTypes = { |
| type: React.PropTypes.string.isRequired, |
| onChange: React.PropTypes.func, |
| disabled: React.PropTypes.bool, |
| didUpdateCallback: React.PropTypes.func, |
| validations: React.PropTypes.object, |
| isMultiSelect: React.PropTypes.bool, |
| onOtherChange: React.PropTypes.func, |
| pointer: React.PropTypes.string |
| }; |
| |
| |
| state = { |
| isValid: true, |
| style: null, |
| value: this.props.value, |
| error: {}, |
| previousErrorMessage: '', |
| wasInvalid: false, |
| validations: this.props.validations, |
| isMultiSelect: this.props.isMultiSelect |
| }; |
| |
| componentWillMount() { |
| if (this.context.validationSchema) { |
| let {validationSchema: schema, validationData: data} = this.context, |
| {pointer} = this.props; |
| |
| if (!schema.exists(pointer)) { |
| console.error(`Field doesn't exists in the schema ${pointer}`); |
| } |
| |
| let value = JSONPointer.getValue(data, pointer); |
| if (value === undefined) { |
| value = schema.getDefault(pointer); |
| if (value === undefined) { |
| value = ''; |
| } |
| } |
| this.setState({value}); |
| |
| let enums = schema.getEnum(pointer); |
| if (enums) { |
| let values = enums.map(value => ({enum: value, title: value, groupName: pointer})), |
| isMultiSelect = schema.isArray(pointer); |
| |
| if (!isMultiSelect && this.props.type !== 'radiogroup') { |
| values = [{enum: '', title: i18n('Select...')}, ...values]; |
| } |
| if (isMultiSelect && Array.isArray(value) && value.length === 0) { |
| value = ''; |
| } |
| |
| this.setState({ |
| isMultiSelect, |
| values, |
| onEnumChange: value => this.changedInputOptions(value), |
| value |
| }); |
| } |
| |
| this.setState({validations: this.extractValidationsFromSchema(schema, pointer, this.props)}); |
| } |
| } |
| |
| extractValidationsFromSchema(schema, pointer, props) { |
| /* props are here to get precedence over the scheme definitions */ |
| let validations = {}; |
| |
| if (schema.isRequired(pointer)) { |
| validations.required = true; |
| } |
| |
| if (schema.isNumber(pointer)) { |
| validations.numeric = true; |
| |
| const maxValue = props.validations.maxValue || schema.getMaxValue(pointer); |
| if (maxValue !== undefined) { |
| validations.maxValue = maxValue; |
| } |
| |
| const minValue = props.validations.minValue || schema.getMinValue(pointer); |
| if (minValue !== undefined) { |
| validations.minValue = minValue; |
| } |
| } |
| |
| |
| if (schema.isString(pointer)) { |
| |
| const pattern = schema.getPattern(pointer); |
| if (pattern) { |
| validations.pattern = pattern; |
| } |
| |
| const maxLength = schema.getMaxLength(pointer); |
| if (maxLength !== undefined) { |
| validations.maxLength = maxLength; |
| } |
| |
| const minLength = schema.getMinLength(pointer); |
| if (minLength !== undefined) { |
| validations.minLength = minLength; |
| } |
| } |
| |
| return validations; |
| } |
| |
| componentWillReceiveProps({value: nextValue, validations: nextValidations, pointer: nextPointer}, nextContext) { |
| const {validations, value} = this.props; |
| const validationsChanged = !isEqual(validations, nextValidations); |
| if (nextContext.validationSchema) { |
| if (this.props.pointer !== nextPointer || |
| this.context.validationData !== nextContext.validationData) { |
| let currentValue = JSONPointer.getValue(this.context.validationData, this.props.pointer), |
| nextValue = JSONPointer.getValue(nextContext.validationData, nextPointer); |
| if(nextValue === undefined) { |
| nextValue = ''; |
| } |
| if (this.state.isMultiSelect && Array.isArray(nextValue) && nextValue.length === 0) { |
| nextValue = ''; |
| } |
| if (currentValue !== nextValue) { |
| this.setState({value: nextValue}); |
| } |
| if (validationsChanged) { |
| this.setState({ |
| validations: this.extractValidationsFromSchema(nextContext.validationSchema, nextPointer, {validations: nextValidations}) |
| }); |
| } |
| } |
| } else { |
| if (validationsChanged) { |
| this.setState({validations: nextValidations}); |
| } |
| if (this.state.wasInvalid && (value !== nextValue || validationsChanged)) { |
| this.validate(nextValue, nextValidations); |
| } else if (value !== nextValue) { |
| this.setState({value: nextValue}); |
| } |
| } |
| } |
| |
| shouldTypeBeNumberBySchemeDefinition(pointer) { |
| return this.context.validationSchema && |
| this.context.validationSchema.isNumber(pointer); |
| } |
| |
| hasEnum(pointer) { |
| return this.context.validationSchema && |
| this.context.validationSchema.getEnum(pointer); |
| } |
| |
| render() { |
| let {value, isMultiSelect, values, onEnumChange, style, isValid, validations} = this.state; |
| let {onOtherChange, type, pointer} = this.props; |
| if (this.shouldTypeBeNumberBySchemeDefinition(pointer) && !this.hasEnum(pointer)) { |
| type = 'number'; |
| } |
| let props = {...this.props}; |
| |
| let groupClasses = this.props.groupClassName || ''; |
| if (validations.required) { |
| groupClasses += ' required'; |
| } |
| let isReadOnlyMode = this.context.isReadOnlyMode; |
| |
| if (value === true && (type === 'checkbox' || type === 'radio')) { |
| props.checked = true; |
| } |
| return ( |
| <div className='validation-input-wrapper'> |
| { |
| !isMultiSelect && !onOtherChange && type !== 'select' && type !== 'radiogroup' |
| && <Input |
| {...props} |
| type={type} |
| groupClassName={groupClasses} |
| ref={'_myInput'} |
| value={value} |
| disabled={isReadOnlyMode || Boolean(this.props.disabled)} |
| bsStyle={style} |
| onChange={() => this.changedInput()} |
| onBlur={() => this.blurInput()}> |
| {this.props.children} |
| </Input> |
| } |
| { |
| type === 'radiogroup' |
| && <FormGroup> |
| { |
| values.map(val => |
| <Input disabled={isReadOnlyMode || Boolean(this.props.disabled)} |
| inline={true} |
| ref={'_myInput' + (typeof val.enum === 'string' ? val.enum.replace(/\W/g, '_') : val.enum)} |
| value={val.enum} checked={value === val.enum} |
| type='radio' label={val.title} |
| name={val.groupName} |
| onChange={() => this.changedInput()}/> |
| ) |
| } |
| </FormGroup> |
| } |
| { |
| (isMultiSelect || onOtherChange || type === 'select') |
| && <InputOptions |
| onInputChange={() => this.changedInput()} |
| onBlur={() => this.blurInput()} |
| hasError={!isValid} |
| ref={'_myInput'} |
| isMultiSelect={isMultiSelect} |
| values={values} |
| onEnumChange={onEnumChange} |
| selectedEnum={value} |
| multiSelectedEnum={value} |
| {...props} /> |
| } |
| {this.renderOverlay()} |
| </div> |
| ); |
| } |
| |
| renderOverlay() { |
| let position = 'right'; |
| if (this.props.type === 'text' |
| || this.props.type === 'email' |
| || this.props.type === 'number' |
| || this.props.type === 'password' |
| |
| ) { |
| position = 'bottom'; |
| } |
| |
| let validationMessage = this.state.error.message || this.state.previousErrorMessage; |
| return ( |
| <Overlay |
| show={!this.state.isValid} |
| placement={position} |
| target={() => { |
| let target = ReactDOM.findDOMNode(this.refs._myInput); |
| return target.offsetParent ? target : undefined; |
| }} |
| container={this}> |
| <Tooltip |
| id={`error-${validationMessage.replace(' ', '-')}`} |
| className='validation-error-message'> |
| {validationMessage} |
| </Tooltip> |
| </Overlay> |
| ); |
| } |
| |
| componentDidMount() { |
| if (this.context.validationParent) { |
| this.context.validationParent.register(this); |
| } |
| } |
| |
| componentDidUpdate(prevProps, prevState) { |
| if (this.context.validationParent) { |
| if (prevState.isValid !== this.state.isValid) { |
| this.context.validationParent.childValidStateChanged(this, this.state.isValid); |
| } |
| } |
| if (this.props.didUpdateCallback) { |
| this.props.didUpdateCallback(); |
| } |
| |
| } |
| |
| componentWillUnmount() { |
| if (this.context.validationParent) { |
| this.context.validationParent.unregister(this); |
| } |
| } |
| |
| isNumberInputElement() { |
| return this.props.type === 'number' || this.refs._myInput.props.type === 'number'; |
| } |
| |
| /*** |
| * Adding same method as the actual input component |
| * @returns {*} |
| */ |
| getValue() { |
| if (this.props.type === 'checkbox') { |
| return this.refs._myInput.getChecked(); |
| } |
| if (this.props.type === 'radiogroup') { |
| for (let key in this.refs) { // finding the value of the radio button that was checked |
| if (this.refs[key].getChecked()) { |
| return this.refs[key].getValue(); |
| } |
| } |
| } |
| if (this.isNumberInputElement()) { |
| return Number(this.refs._myInput.getValue()); |
| } |
| |
| return this.refs._myInput.getValue(); |
| } |
| |
| resetValue() { |
| this.setState({value: this.props.value}); |
| } |
| |
| |
| /*** |
| * internal method that validated the value. includes callback to the onChange method |
| * @param value |
| * @param validations - map containing validation id and the limitation describing the validation. |
| * @returns {object} |
| */ |
| validateValue = (value, validations) => { |
| let {customValidationFunction} = validations; |
| let error = {}; |
| let isValid = true; |
| for (let validation in validations) { |
| if ('customValidationFunction' !== validation) { |
| if (validations[validation]) { |
| if (!globalValidationFunctions[validation](value, validations[validation])) { |
| error.id = validation; |
| error.message = globalValidationMessagingFunctions[validation](value, validations[validation]); |
| isValid = false; |
| break; |
| } |
| } |
| } else { |
| let customValidationResult = customValidationFunction(value); |
| |
| if (customValidationResult !== true) { |
| error.id = 'custom'; |
| isValid = false; |
| if (typeof customValidationResult === 'string') {//custom validation error message supplied. |
| error.message = customValidationResult; |
| } else { |
| error.message = globalValidationMessagingFunctions.general(); |
| } |
| break; |
| } |
| |
| |
| } |
| } |
| |
| return { |
| isValid, |
| error |
| }; |
| }; |
| |
| /*** |
| * Internal method that handles the change event of the input. validates and updates the state. |
| */ |
| changedInput() { |
| |
| let {isValid, error} = this.state.wasInvalid ? this.validate() : this.state; |
| let onChange = this.props.onChange; |
| if (onChange) { |
| onChange(this.getValue(), isValid, error); |
| } |
| if (this.context.validationSchema) { |
| let value = this.getValue(); |
| if (this.state.isMultiSelect && value === '') { |
| value = []; |
| } |
| if (this.shouldTypeBeNumberBySchemeDefinition(this.props.pointer)) { |
| value = Number(value); |
| } |
| this.context.validationParent.onValueChanged(this.props.pointer, value, isValid, error); |
| } |
| } |
| |
| changedInputOptions(value) { |
| this.context.validationParent.onValueChanged(this.props.pointer, value, true); |
| } |
| |
| blurInput() { |
| if (!this.state.wasInvalid) { |
| this.setState({wasInvalid: true}); |
| } |
| |
| let {isValid, error} = !this.state.wasInvalid ? this.validate() : this.state; |
| let onBlur = this.props.onBlur; |
| if (onBlur) { |
| onBlur(this.getValue(), isValid, error); |
| } |
| } |
| |
| validate(value = this.getValue(), validations = this.state.validations) { |
| let validationStatus = this.validateValue(value, validations); |
| let {isValid, error} = validationStatus; |
| let _style = isValid ? null : 'error'; |
| this.setState({ |
| isValid, |
| error, |
| value, |
| previousErrorMessage: this.state.error.message || '', |
| style: _style, |
| wasInvalid: !isValid || this.state.wasInvalid |
| }); |
| |
| return validationStatus; |
| } |
| |
| isValid() { |
| return this.state.isValid; |
| } |
| |
| } |
| export default ValidationInput; |