|
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 |
|
} |
|
}) |