gdumitrescu
11/13/2015 - 1:48 PM

React Form Play

React Form Play

.has-error {
  border-color: red
}
{
  "name": "react-form-play",
  "dependencies": {
    "react": "^0.14.0"
  },
  "devDependencies": {
    "react-heatpack": "^3.0.0"
  },
  "scripts": {
    "start": "heatpack index.js"
  }
}
import './style.css'

import React, {Component} from 'react'

// Helpers

let classNames = (...classes) =>
  classes.filter(cn => !!cn)
         .join(' ')

/**
 * Initialise a state object for a for which has the given named fields.
 */
let createFormState = (fields) => ({
  form: keyValue(fields, ''),
  errors: keyValue(fields, null),
  touched: keyValue(fields, false),
  submitted: false
})

/**
 * Determine if there are any non-falsy errors.
 */
let hasErrors = (errors) =>
  Object.keys(errors)
        .some(field => !!errors[field])

/**
 * Create an object mapping a list of keys to a value.
 */
let keyValue = (keys, value) =>
  keys.reduce((obj, key) => {
    obj[key] = value
    return obj
  }, {})

/**
 * Return the results of calling a (name, error) function for every non-falsy
 * error.
 */
let mapErrors = (errors, fn) =>
  Object.keys(errors)
        .filter(field => !!errors[field])
        .map(field => fn(field, errors[field]))

/**
 * Create an object with a prop for every field, with the result o calling
 * validateField with each field's data.
 */
let validateForm = (form, fields, validateField) =>
  fields.reduce((errors, field) => {
    errors[field] = validateField(field, form[field], form)
    return errors
  }, {})

// Form HoC

function makeForm(FormComponent, {
  fields,
  validateField
} = {}) {
  if (!fields) {
    throw new Error('makeForm: fields option is required')
  }
  if (!validateField) {
    throw new Error('makeForm: validateField option is required')
  }

  class Form extends Component {
    constructor(props) {
      super(props)

      this.state = createFormState(fields)

      this.handleBlur = this.handleBlur.bind(this)
      this.handleChange = this.handleChange.bind(this)
      this.handleSubmit = this.handleSubmit.bind(this)
    }

    // Validate a field and mark it as touched if the user leaves it for the
    // first time without having made any changes.
    handleBlur(e) {
      var {name, value} = e.target
      let {errors, form, touched} = this.state
      if (!touched[name]) {
        let error = validateField(name, value, form)
        this.setState({
          errors: {...errors, [name]: error},
          touched: {...touched, [name]: true}
        })
      }
    }

    // Update a field's value and error message in state and mark it as touched
    // if this is the first time it's been changed.
    handleChange(e) {
      let {name, value} = e.target
      let {errors, form, touched} = this.state
      let error = validateField(name, value, form)
      var stateChange = {
        form: {...form, [name]: value},
        errors: {...errors, [name]: error},
        displayError: error
      }
      if (!touched[name]) {
        stateChange.touched = {...touched, [name]: true}
      }
      this.setState(stateChange)
    }

    // Validate all fields and mark them as touched if this is the first time the
    // form has been submitted.
    handleSubmit(e) {
      // If we're given a function, create a function which will handle form
      // submission then call the given function only if the form is valid.
      if (typeof e == 'function') {
        let onValidSubmit = e
        return (e) => {
          if (this.handleSubmit(e)) {
            onValidSubmit(this.state.form)
          }
        }
      }

      if (e) {
        e.preventDefault()
      }

      var {form, submitted} = this.state
      let errors = validateForm(form, fields, validateField)
      let stateChange = {errors}
      // Mark all fields as touched the first time the form is submitted
      if (!submitted) {
        stateChange.touched = {
          username: true,
          password: true
        }
        stateChange.submitted = true
      }
      this.setState(stateChange)
      return !hasErrors(errors)
    }

    render() {
      var {form, errors, touched} = this.state
      return <FormComponent
        form={form}
        errors={errors}
        onBlur={this.handleBlur}
        onChange={this.handleChange}
        onSubmit={this.handleSubmit}
        hasErrors={hasErrors(errors)}
        touched={touched}
      />
    }
  }

  Form.prototype.displayName = `Form(${Component.displayName || Component.name || 'Anonymous'})`

  return Form
}

// Component

class HeaderDiv extends Component {
  constructor(props) {
    super(props)

    // Use an instance variable to store which button was clicked
    this.handleRegister = () => this.submitButton = 'register'
    this.handleLogin = () => this.submitButton = 'login'

    this.handleSubmit = props.onSubmit(this.handleSubmit.bind(this))
  }

  handleSubmit(form) {
    console.log(this.submitButton, form)
  }

  render() {
    let {errors, form, hasErrors, onBlur, onChange} = this.props
    return <div className="headBar">
      <div className="logoPanel">
        Logo Here
      </div>
      <div className="loginPanel">
        <form className="authForm" onSubmit={this.handleSubmit}>
          <span className="formTitle">My Account</span>
          <input
            className={classNames('logFormInput', errors.username && 'has-error')}
            maxLength="20"
            name="username"
            onBlur={onBlur}
            onChange={onChange}
            placeholder="Username"
            type="text"
            value={form.username}
          />
          <input
            className={classNames('logFormInput', errors.password && 'has-error')}
            maxLength="20"
            name="password"
            onBlur={onBlur}
            onChange={onChange}
            type="password"
            value={form.password}
          />
          <div className="formBtns">
            <button className="logFormInput" type="submit" onClick={this.handleRegister}>Register</button>
            <button className="logFormInput" type="submit" onClick={this.handleLogin}>Login</button>
          </div>
          {hasErrors && <div>
            {mapErrors(errors, (field, error) =>
              <p className="inputError" key={field}>{error}</p>
            )}
          </div>}
        </form>
      </div>
    </div>
  }
}

export default makeForm(HeaderDiv, {
  fields: ['username', 'password'],
  validateField(name, value, form) {
    if (name === 'username') {
      if (value === '') {
        return 'Username Is Missing'
      }
      else if (/\s/.test(value)) {
        return 'Username Contains Spaces'
      }
    }
    else if (name === 'password') {
      if (value === '') {
        return 'Password Is Missing'
      }
      else if (/\s/.test(value)) {
        return 'Password Contains Spaces'
      }
    }
    return null
  }
})

Messing about with a different way to implement the validation in this component and things got out of hand because forms.

git clone https://gist.github.com/insin/49040037bbb6cd99faf7 react-form-play
cd react-form-play
npm install
npm start

Then point your browser at http://localhost:3000