Skip to content

Instantly share code, notes, and snippets.

@andrewk
Created March 29, 2015 10:08
Show Gist options
  • Select an option

  • Save andrewk/0bd6080c43bf485d665b to your computer and use it in GitHub Desktop.

Select an option

Save andrewk/0bd6080c43bf485d665b to your computer and use it in GitHub Desktop.

Revisions

  1. andrewk created this gist Mar 29, 2015.
    214 changes: 214 additions & 0 deletions form-validation.md
    Original 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
    }
    ```