Skip to content

Instantly share code, notes, and snippets.

@rstacruz
Created December 21, 2017 12:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rstacruz/da1bb0be616065660732abf106ba723d to your computer and use it in GitHub Desktop.
Save rstacruz/da1bb0be616065660732abf106ba723d to your computer and use it in GitHub Desktop.
/* @flow */
/*::
import type {Error, Model} from './Validate'
type Props = {
input: (Model) => React.Element<any>
}
*/
import React from 'react'
import Validate from './Validate'
/*
* A required field.
* Basically a preset of <Validate /> with error display.
*
* <RequiredField input={({ ref, onChange, errors }) =>
* <input type='text' name='email' {...{ref, onChange}} />
* } />
*/
function RequiredField ({ input } /*: Props */) {
return <Validate rules={{ required: true }} input={m =>
<span className='field'>
{input(m)}
<FieldError errors={m.errors} />
</span>
} />
}
function FieldError ({ errors } /*: { errors: ?Array<Error> } */) {
if (errors == null) return <noscript />
const messages = errors.map(e => e.message)
return <div className='error-message'>{messages.join(', ')}</div>
}
export default RequiredField
/* @flow */
/*::
export type Error = {
error: string,
message: string
}
export type Model = {
validate: (any) => void,
onChange: (any) => void,
ref: (InputElement) => void,
errors: ?Array<Error>
}
export type Rules = {
required?: boolean
}
export type Props = {
input: (Model) => React.Element<any>,
rules: Rules
}
export type State = {
lastValue?: any,
errors: ?Array<Error>
}
export type InputElement = HTMLInputElement | HTMLSelectElement
*/
import React from 'react'
/**
* Validates an input.
*
* @example
* <Validate
* rules={{required: true}}
* input={({ events, errors }) =>
* <input type='text' name='email' {...events} />
* { errors ? <span>Error: {errors.map(e => e.message).join(',' )}</span> : null }
* } />
*/
class Validate extends React.Component {
/*::
state: State
props: Props
model: Model
el: InputElement
*/
constructor () {
super()
this.state = { errors: null }
// Details to be passed onto input()
this.model = {
errors: null,
validate: this.validate.bind(this),
onChange: this.onChange.bind(this),
ref: this.onRef.bind(this)
}
}
componentDidMount () {
// Run the validation on the actual element value. This would account for
// updates that aren't initiated by the app nor the user, eg, browser
// autofill.
this.validate(this.el.value)
}
componentDidUpdate () {
// Run the validation when the input is changed programatically (eg, when
// updating the `value` prop from a parent component).
this.validate(this.el.value)
}
render () {
const { model } = this
const { input } = this.props
const { errors } = this.state
return input({ ...model, errors })
}
onRef (el /*: InputElement */) {
this.el = el
}
onChange (event /*: SyntheticInputEvent */) {
// For some reason, Enzyme doesn't get to keep track of `el` properly,
// this "resyncs" this.el to the element we need
this.el = event.target
this.validate(event.target.value)
}
validate (value /*: any */) {
// Don't re-run validation if input hasn't changed.
const { lastValue } = this.state
if (lastValue === value) return
const errors = this.getErrors(value)
this.setState({ errors, lastValue: value })
}
getErrors (value /*: any */) /*: ?Array<Error> */ {
const { rules } = this.props
if (rules.required) {
if (!value || value.trim().length === 0) {
return [{ error: 'required', message: 'Required' }]
}
}
return null
}
}
export default Validate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment