sc-react-ions
Version:
An open source set of React components that implement Ambassador's Design and UX patterns.
199 lines (164 loc) • 5.6 kB
JavaScript
import React from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash/debounce'
import { validate } from '../../utilities/validation'
import { formSchemaToKeyVal } from '../../utilities/form'
import { is, Iterable, fromJS, List, Map } from 'immutable'
import style from './style.scss'
import optclass from '../internal/OptClass'
class FormGroup extends React.Component {
constructor(props) {
super(props)
this.debounce = debounce(this.handleChange, this.props.debounceTime)
this._formValidation = null
}
static propTypes = {
/**
* A configuration object of name/value pairs
* that correspond to the form fields names.
*/
schema: PropTypes.object,
/**
* A callback function to be called when a form value changes.
*/
changeCallback: PropTypes.func,
/**
* A callback function to be called when the form is submitted.
*/
submitCallback: PropTypes.func,
/**
* Optional CSS class(es) to be used for local styles (string or array of strings)
*/
optClass: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string
]),
/**
* Option to turn off form wrapper (for nested components)
*/
nested: PropTypes.bool,
/**
* Option to turn off debounce when something in the form group changes
*/
debounceTime: PropTypes.number,
/**
* A key value pair eg: { 'message': 'this is an error message' }, were the
* key repesents the `name` of the given field to validate
*/
fieldErrors: PropTypes.object
}
static defaultProps = {
debounceTime: 0,
fieldErrors: Map()
}
state = {
fieldErrors: Map()
}
componentWillReceiveProps = nextProps => {
const nextPropsSchema = fromJS(nextProps.schema)
const thisPropsSchema = fromJS(this.props.schema)
if (!is(nextPropsSchema, thisPropsSchema)) {
this.setState({
fields: fromJS(nextProps.schema)
})
}
}
componentWillMount = () => {
this.setState({
fields: fromJS(this.props.schema)
})
}
// Errors can be passed in via props if external validation is used or
// errors can be captured from state if internal validation is used
_mapFieldErrors = () => this.state.fieldErrors.merge(this.props.fieldErrors)
handleSubmit = event => {
event.preventDefault()
const fieldErrors = validate(this._formValidation, formSchemaToKeyVal(this.state.fields))
// Required to send error prop to ValidatedField component
this.setState({ fieldErrors })
if (fieldErrors && fieldErrors.size && typeof this.props.errorCallback === 'function') {
return this.props.errorCallback(fieldErrors)
}
if (typeof this.props.submitCallback === 'function') {
this.props.submitCallback(event, this.state.fields.toJS())
}
}
handleChange = event => {
const name = event.target.name
let val = event.target.value
let option = event.target.option
// Handle checkbox values
if (event.target.type === 'checkbox') {
val = event.target.checked
}
this.setState(prevState => {
let fields = prevState.fields.setIn([name, 'value'], val)
if (option) {
fields = fields.setIn([name, 'option'], option)
}
const fieldErrors = prevState.fieldErrors.set(name, '')
return { fields, fieldErrors }
}, () => {
if (this.props.changeCallback) {
this.props.changeCallback(this.state.fields.toJS(), this.props.fieldErrors.merge(this.state.fieldErrors))
}
})
}
getElements = (children, recursiveCall) => {
// Reset validation each time this called on the form group
if (!recursiveCall) {
this._formValidation = Map()
}
const fieldErrors = this._mapFieldErrors()
return React.Children.map(children, child => {
if (!child) return child
let childProps = {}
if (child.props) {
const name = child.props.name
const error = fieldErrors.get(name)
const value = this.state.fields.getIn([name, 'value'])
const valueIsImmutable = Iterable.isIterable(value)
const valueProp = valueIsImmutable ? value.toJS() : value
if (child.props.validation) {
this._formValidation = this._formValidation.set(name, Map({
validators: fromJS(child.props.validation)
}))
}
if (this.state.fields.has(name) && React.isValidElement(child)) {
childProps = {
changeCallback: this.props.debounceTime ? this.debounce : this.handleChange,
value: valueProp,
error
}
}
childProps.children = this.getElements(child.props.children, true)
return React.cloneElement(child, childProps)
}
return child
})
}
renderForm = () => {
const elements = this.getElements(this.props.children)
const formGroupClass = optclass(style, 'form-group', this.props.optClass)
let formWrapper
if (!this.props.nested) {
formWrapper = <form className={formGroupClass} onSubmit={this.handleSubmit}>
<fieldset className={style.fieldset}>
{elements}
</fieldset>
</form>
} else {
const fieldsetClass = optclass(style, 'fieldset', this.props.optClass)
formWrapper = <fieldset className={fieldsetClass}>
{elements}
</fieldset>
}
return formWrapper
}
render = () => {
return (
this.renderForm()
)
}
}
export default FormGroup