Created
March 29, 2015 10:08
-
-
Save andrewk/0bd6080c43bf485d665b to your computer and use it in GitHub Desktop.
Revisions
-
andrewk created this gist
Mar 29, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,214 @@ 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). Requirements ------------ - markup must match our existing markup, for UX consistency. - inputs are re-usable components with an explicit contract -- they are responsponsible for their error display and any required filtering of their value. Basic Form ---------- ```javascript 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__ ```javascript 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__ ```javascript export default class FieldError extends PureComponent { render() { return ( <div className="field__validation"> {this.props.message} </div> ) } } FieldError.propTypes = { message: React.PropTypes.string, } ``` ControllerComponent ------------------- __Using the form__ ```javascript 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 } ```