Skip to content

Instantly share code, notes, and snippets.

@insin
Last active February 14, 2017 15:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save insin/49040037bbb6cd99faf7 to your computer and use it in GitHub Desktop.
Save insin/49040037bbb6cd99faf7 to your computer and use it in GitHub Desktop.
React Form Play

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

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
}
})
{
"name": "react-form-play",
"dependencies": {
"react": "^0.14.0"
},
"devDependencies": {
"react-heatpack": "^3.0.0"
},
"scripts": {
"start": "heatpack index.js"
}
}
.has-error {
border-color: red
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment