UNPKG

react-form-validator-component

Version:

React Form Validator exposes a single React component which uses the render prop pattern to validate the input on its child form. It is built as a pure React component, with no additional dependencies, making it efficient and cheap to add to any React

304 lines (268 loc) 11.3 kB
import React from 'react' import PropTypes from 'prop-types' import 'babel-polyfill' import * as defaultRules from './rules' import addToStateProperty from './utils/addToStateProperty' import toArray from './utils/toArray' export default class Validator extends React.Component { constructor(props) { super(props) this.state = { errors: this.initialiseStateErrors(props.fields), groupValidation: this.initialiseStateGroupValidation(props.fields), validation: this.initialiseStateFieldValidation(props.fields), isFormValid: false, validatorInput: {}, // only used if user sets returnInput hasChanged: {} } } componentDidMount() { this.validateFieldsProp() this.addRequiredRuleToFields() this.initialValidation() // TODO: only remove errors from empty fields if (this.props.validateOnLoad) Object.values(this.props.fields).forEach(field => this.removeAllErrorMessages(field.name)) } // each field gets an (empty) array for its errors initialiseStateErrors(fields) { return Object.keys(fields).reduce((accumulator, currentValue) => { accumulator[currentValue] = [] return accumulator }, {}) } initialiseStateGroupValidation(fields) { return Object.keys(fields).reduce((groupValidation, currentField) => { const field = fields[currentField] if (field.required && typeof field.required === 'string') { groupValidation[field.required] = Object.assign({}, groupValidation[field.required], { [currentField]: (field.rules && field.rules.length > 0) || field.required ? false : true }) return groupValidation } return groupValidation }, {}) } initialiseStateFieldValidation(fields) { return Object.keys(fields).reduce((accumulator, currentValue) => { const fieldValue = fields[currentValue] //if field is a member of a group, add that group to validation and add the field to validation.groupValidation if (fieldValue.required && typeof fieldValue.required === 'string') { accumulator[fieldValue.required] = false return accumulator } else { accumulator[currentValue] = (fieldValue.rules && fieldValue.rules.length > 0) || fieldValue.required ? false : true return accumulator } }, {}) } componentDidUpdate(prevProps) { const currentProps = this.props if (JSON.stringify(currentProps.fields) !== JSON.stringify(prevProps.fields)) this.setState({ fields: currentProps.fields, errors: this.initialiseStateErrors(currentProps.fields), groupValidation: this.initialiseStateGroupValidation(currentProps.fields), validation: this.initialiseStateFieldValidation(currentProps.fields), isFormValid: Object.values(this.initialiseStateFieldValidation(currentProps.fields)).every( field => field === true ) }) } // default behaviour for handling successfully validated input onValidate = (fieldName, fieldValue) => { this.props.parent.setState({ [fieldName]: fieldValue }) } removeError = (fieldName, errorMessage) => { const errorArray = this.state.errors[fieldName] const messagePosition = errorArray.indexOf(errorMessage) if (messagePosition > -1) errorArray.splice(messagePosition, 1) addToStateProperty('errors', { [fieldName]: errorArray }, this) } removeAllErrorMessages = fieldName => { this.setState({ errors: Object.assign(this.state.errors, { [fieldName]: [] }) }) } updateErrorsForField = (validation, fieldName, errorMessage) => { if (!validation) { addToStateProperty( 'errors', { [fieldName]: [...new Set([...(this.state.errors[fieldName] || []), errorMessage])] }, this ) } else this.removeError(fieldName, errorMessage) } validateRules = (fieldName, fieldValue, fieldRules) => fieldRules.reduce((accumulator, fieldRule) => { const rule = defaultRules[fieldRule] || fieldRule const validation = rule.validator(fieldValue) this.updateErrorsForField(validation, fieldName, rule.error) return accumulator && validation }, true) validateGroup = (fieldName, fieldValue, groupName) => { // check if any other member of the group is valid const otherMemberValid = this.state.groupValidation[groupName] && Object.entries(this.state.groupValidation[groupName]) .filter(field => !field.includes(fieldName)) .some(member => member.includes(true)) // if another member is valid do this if (otherMemberValid) { const fieldRules = this.props.fields[fieldName].rules const isFieldValid = this.validateRules(fieldName, fieldValue, fieldRules) const newGroupValidation = this.state.groupValidation // and if another member is valid and this field is empty or valid, all is well, otherwise set invalidValuePresent newGroupValidation[groupName] = !fieldValue || (Array.isArray(fieldValue) && fieldValue.length === 0) || isFieldValid ? Object.assign({}, this.state.groupValidation[groupName], { [fieldName]: isFieldValid, invalidValuePresent: false }) : Object.assign({}, this.state.groupValidation[groupName], { [fieldName]: isFieldValid, invalidValuePresent: true }) const newValidation = Object.assign({}, this.state.validation, { [groupName]: Object.values( Object.assign({}, newGroupValidation[groupName], { invalidValuePresent: false }) // "filter" out invalidValuesPresent ).some(member => member === true) && !newGroupValidation[groupName].invalidValuePresent }) const newValidState = Object.values(newValidation).every(field => field === true) this.setState( { groupValidation: newGroupValidation }, () => this.setState({ validation: newValidation, isFormValid: newValidState }) ) return isFieldValid } // if no other member is valid, or this field has a value, check if this field is valid const fieldRules = this.props.fields[fieldName].rules const isFieldValid = this.validateRules(fieldName, fieldValue, fieldRules) const newGroupValidation = this.state.groupValidation newGroupValidation[groupName] = Object.assign({}, newGroupValidation[groupName], { [fieldName]: isFieldValid }) const newValidation = Object.assign(this.state.validation, { [groupName]: Object.values( Object.assign({}, this.state.groupValidation[groupName], { invalidValuePresent: false }) // "filter" out invalidValuesPresent ).some(member => member === true) && !this.state.groupValidation[groupName].invalidValuePresent }) const newFormValid = Object.values(newValidation).every(field => field === true) this.setState( { groupValidation: Object.assign({}, this.state.groupValidation, newGroupValidation) }, () => { this.setState({ validation: newValidation, isFormValid: newFormValid }) } ) return isFieldValid } validateField = (fieldName, fieldValue) => { const field = this.props.fields[fieldName] // Check whether field is in a group const groupName = this.props.fields[fieldName].required && typeof this.props.fields[fieldName].required === 'string' ? this.props.fields[fieldName].required : undefined // ensure that empty non-required fields pass validation and don't throw errors if (!groupName && !field.required && fieldValue.length === 0) { addToStateProperty('validation', { [fieldName]: true }, this) return true } if (groupName) { return this.validateGroup(fieldName, fieldValue, groupName) } // standard validation const fieldRules = field.rules const isFieldValid = this.validateRules(fieldName, fieldValue, fieldRules) addToStateProperty('validation', { [fieldName]: isFieldValid }, this) return isFieldValid } validateFieldAndUpdateState(fieldName, fieldValue) { const onValidate = this.props.fields[fieldName].onValidate || this.props.onValidate || this.onValidate if (this.validateField(fieldName, fieldValue)) { onValidate(fieldName, fieldValue) this.setState({ isFormValid: Object.values(this.state.validation).every(field => field === true) }) } else { if (this.state.validation[fieldName] === null) return null onValidate(fieldName, null) this.setState({ isFormValid: Object.values(this.state.validation).every(field => field === true) }) } // if the user provides the returnInput prop, we set the input to parent state regardless of whether it is valid in the validatorInput object if (this.props.returnInput) { addToStateProperty('validatorInput', { [fieldName]: fieldValue }, this) this.props.parent.setState({ validatorInput: this.state.validatorInput }) } } initialValidation = async () => { const fields = Object.values(this.props.fields).filter(field => field) fields.forEach(field => { const fieldInDom = document.getElementsByName(field.name)[0] const valueFromDom = (fieldInDom || {}).value const fieldValue = field.defaultValue || valueFromDom || '' this.validateFieldAndUpdateState(field.name, fieldValue) }) } onChange = (e, d) => { const changeEvent = d ? d : e.target this.setState({ hasChanged: { ...this.state.changeEvent, [changeEvent.name]: true } }) this.validateFieldAndUpdateState(changeEvent.name, changeEvent.value) } validateFieldsProp = () => { Object.values(this.props.fields).forEach(field => { if (!field.name) throw new Error(`Please provide a name value for all of your fields`) if (!field.rules) throw new Error( `Please provide a rules array for field ${field.name} (or an empty array for non-validated fields)` ) }) } // Add isRequired rule if field is required or in a group addRequiredRuleToFields() { Object.values(this.props.fields).forEach(field => { if (field.required === true || typeof field.required === 'string') { const newFields = this.props.fields newFields[field.name].rules.push('isRequired') this.setState({ fields: newFields }) } }) } render() { const { errors, isFormValid, validation, groupValidation, hasChanged } = this.state return this.props.children({ isFormValid, isFieldValid: Object.assign({}, validation, ...Object.values(groupValidation)), //we spread in all values of group Validation to show grouped fields individually hasChanged, fields: toArray(this.props.fields || {}), onChange: this.onChange, errors }) } } Validator.propTypes = { parent: PropTypes.object, children: PropTypes.func, onValidate: PropTypes.func, fields: PropTypes.object, validateOnLoad: PropTypes.bool, returnInput: PropTypes.bool } Validator.defaultProps = { validateOnLoad: true, returnInput: false }