Good Enough™ Form Validation in React
…specifically React 0.13 using ES6 class syntax
Originally I was implementing the validator as a context, but then I got stung by parent context vs owner context. I'll likely return to the context model when React's context implementation is more final (looks like they're moving towards parent context over owner context, which I'd prefer).
export default class NewCreditCardForm extends React.Component {
constructor() {
this.state = {
fields: ['name', 'number', 'expiry', 'cvc']
}
}
render() {
return (
<form ref="newCard" onSubmit={this.submit.bind(this)}>
<Input ref="name"
attr={{ placeholder: 'Ben Franklin' }}
label="Full name"
name="name"
validator={ v => v.trim().length > 0 }
validationMessage="Card holder's name is required"
/>
<Input ref="number"
label="Card number"
name="number"
attr={{ placeholder: '1234 5678 9012 3456' }}
filter={ v => v.replace(/-|\s/g, '') }
validator={ v => v.match(/^\d{16}$/) }
validationMessage="Entered card number is invalid"
/>
<div className="fieldset">
<Input ref="expiry"
label="Expiration"
name="expiry"
attr={{ placeholder: '04/15' }}
filter={ v => v.trim() }
validator={ v => v.match(/^\d{2}\/\d{2}$/) }
validationMessage="Must be in MM/YY format"
extraClasses="field--half"
/>
<Input ref="cvc"
label="CVC"
name="cvc"
attr={{ placeholder: '123' }}
filter={ v => v.trim() }
validationMessage="CVC is a 3 digit number"
validator={ v => v.match(/^\d{3}$/) }
extraClasses="field--half"
/>
</div>
<PayButton payment={this.props.payment} />
</form>
)
}
submit(e) {
e.preventDefault()
let isValid = true
this.state.fields.forEach((ref) => {
isValid = this.refs[ref].validate() && isValid
})
if (isValid) {
this.props.onSuccess(this.formData())
}
}
formData() {
const data = {}
this.state.fields.forEach(ref => {
data[ref] = this.refs[ref].state.value
})
return data
}
}
NewCreditCardForm.propTypes = {
payment: React.PropTypes.object.isRequired,
}
NewCreditCardForm.contextTypes = {
router: React.PropTypes.func.isRequired,
flux: React.PropTypes.object.isRequired
}
Input Component
export default class Input extends React.Component {
constructor() {
this.state = { value: '', error: '' }
}
handleChange(event) {
const newValue = this.props.filter
? this.props.filter(event.target.value)
: event.target.value
this.setState({ value: newValue})
}
validate() {
if (this.props.validator && !this.props.validator.call(undefined, this.state.value)) {
this.setState({ error: this.props.validationMessage })
return false
}
this.setState({ error: '' })
return true
}
render() {
const attr = this.props.attr || {}
const type = attr.type || 'text'
const classes = ['field']
attr.id = attr.id || this.props.name
const value = this.props.value
const hasError = !(this.state.error === undefined || this.state.error.length === 0)
if (this.props.extraClasses) {
classes.push(this.props.extraClasses)
}
return (
<div data-field
data-field-error={hasError}
className={classes.join(' ')}>
<label className="field__title" htmlFor={attr.id}>{this.props.label}</label>
<FieldError message={this.state.error} />
<div className="field__input">
<input type={type}
className="input-text"
value={value}
onChange={this.handleChange.bind(this)}
{...attr}
/>
</div>
</div>
)
}
}
Input.propTypes = {
label: React.PropTypes.string,
name: React.PropTypes.string,
extraClasses: React.PropTypes.string,
}
Default error message
export default class FieldError extends PureComponent {
render() {
return (
<div className="field__validation">
{this.props.message}
</div>
)
}
}
FieldError.propTypes = {
message: React.PropTypes.string,
}
Using the form
export default class NewCreditCard extends React.Component {
render() {
const payment = this.context.flux.getStore('payment').getPayment()
return (
<div>
<NewCreditCardForm payment={payment} onSuccess={this.newCard.bind(this)} />
</div>
)
}
// this method receives the validated, filtered data.
newCard(data) {
this.context.flux.getActions('creditcards').newCreditCard(data)
this.context.router.transitionTo('whatever')
}
}
NewCreditCard.contextTypes = {
router: React.PropTypes.func.isRequired,
flux: React.PropTypes.object.isRequired
}