Form controller can handle form submit and errors

main
Ashelyn Dawn 5 years ago
parent 51f26f5692
commit 5d42b78bd8

@ -17,6 +17,19 @@ router.post('/', parseJSON, registerValidation, async (req, res) => {
req.body.password req.body.password
) )
if(!user){
return res.status(422).json({errors: [{
param: 'email',
msg: 'Unable to complete registration'
},{
param: 'password',
msg: ' '
},{
param: 'password2',
msg: ' '
}]})
}
const session = await db.session.create( const session = await db.session.create(
user.uuid, user.uuid,
req.ip, req.ip,

@ -1,58 +1,80 @@
// TODO: Enable exportDefaultFrom in Babel syntax // TODO: Enable exportDefaultFrom in Babel syntax
import React, { useReducer } from 'react' import React, { useReducer } from 'react'
import axios from 'axios'
import styles from './styles.module.css' import styles from './styles.module.css'
export const Input = require('./input.js').default export const Input = require('./input.js').default
export const Button = require('./button.js').default export const Button = require('./button.js').default
export const FormController = function FormController({errors, errorDispatch, children, onSubmit}){ const errorReducer = (errors, action) => {
const initialState = { switch (action.type) {
fields: {} case 'set_errors':
} const newErrors = {}
for(const field of action.errors)
newErrors[field.param] = field.msg
return newErrors
const reducer = (state, action)=>{ case 'clear_error':
let fields = {...state.fields} console.log('clear_error', action)
const prevField = state.fields[action.name] return {
...errors,
[action.field]: undefined
}
// If no value (onBlur), just mark touched and clear error default:
if(action.value === undefined){ return errors
errorDispatch({type: 'clear_error', field: action.name}) }
}
return { const formReducer = (state, action)=>{
fields: { let fields = {...state.fields}
...fields, const prevField = state.fields[action.name]
[action.name]: {
...prevField, // If no value (onBlur), just mark touched and clear error
touched: true, if(action.value === undefined){
isValid: prevField.validate?prevField.validate(prevField.value, fields):true errorDispatch({type: 'clear_error', field: action.name})
}
return {
fields: {
...fields,
[action.name]: {
...prevField,
touched: true,
isValid: prevField.validate?prevField.validate(prevField.value, fields):true
} }
} }
} }
}
// Update currently changing field // Update currently changing field
const updatedField = { const updatedField = {
...prevField, ...prevField,
value: action.value !== undefined ? action.value : prevField.value, value: action.value !== undefined ? action.value : prevField.value,
isValid: prevField.validate?prevField.validate(action.value, fields):true isValid: prevField.validate?prevField.validate(action.value, fields):true
} }
fields[action.name] = updatedField fields[action.name] = updatedField
// Update other fields where the validate function takes whole state // Update other fields where the validate function takes whole state
const fieldNames = Object.keys(fields) const fieldNames = Object.keys(fields)
for(const name of fieldNames){ for(const name of fieldNames){
if(name === action.name) continue if(name === action.name) continue
const field = fields[name] const field = fields[name]
if(field.validate && field.validate.length > 1)
fields[name] = {
...field,
isValid: field.validate(field.value, fields)
}
}
return {fields}
}
if(field.validate && field.validate.length > 1)
fields[name] = {
...field,
isValid: field.validate(field.value, fields)
}
}
return {fields} export const FormController = function FormController({children, url, method = 'POST', afterSubmit = ()=>null}){
const initialState = {
fields: {}
} }
// Update initial state // Update initial state
@ -68,26 +90,41 @@ export const FormController = function FormController({errors, errorDispatch, c
} }
}) })
// Create reducer // Create reducers
const [state, dispatch] = useReducer(reducer, initialState) const [state, dispatch] = useReducer(formReducer, initialState)
const [errors, errorDispatch] = useReducer(errorReducer, {})
const submitForm = ev=>{ // Handle submitting form
const handleSubmit = async (ev) => {
if(ev) ev.preventDefault(); if(ev) ev.preventDefault();
const fields = {} const data = {}
for(const name in state.fields){ for(const name in state.fields){
const field = state.fields[name] const field = state.fields[name]
fields[field.name] = field.value data[field.name] = field.value
} }
onSubmit(fields) if(url)
try {
await axios({ method, url, data })
} catch (err) {
if(!err.response || err.response.status !== 422) throw err;
return errorDispatch({
type: 'set_errors',
errors: err.response.data.errors
})
}
afterSubmit(data)
} }
// Map children
const _children = React.Children.map(children, child => { const _children = React.Children.map(children, child => {
if(child.type === Button && child.props.type.toLowerCase() === 'submit') if(child.type === Button && child.props.type.toLowerCase() === 'submit')
return React.cloneElement(child, { return React.cloneElement(child, {
enabled: !Object.values(state.fields).some(field=>!field.isValid), enabled: !Object.values(state.fields).some(field=>!field.isValid),
onClick: submitForm onClick: handleSubmit
}) })
const {name} = child.props; const {name} = child.props;
@ -102,7 +139,7 @@ export const FormController = function FormController({errors, errorDispatch, c
}) })
return ( return (
<form onSubmit={onSubmit} className={styles.formContainer}> <form autocomplete="off" onSubmit={handleSubmit} className={styles.formContainer}>
{_children} {_children}
</form> </form>
) )

@ -1,25 +0,0 @@
import {useReducer} from 'react'
const errorReducer = (errors, action) => {
switch (action.type) {
case 'set_errors':
const newErrors = {}
for(const field of action.errors)
newErrors[field.param] = field.msg
return newErrors
case 'clear_error':
console.log('clear_error', action)
return {
...errors,
[action.field]: undefined
}
default:
return errors
}
}
export default function useErrorReducer(initialState = {}){
return useReducer( errorReducer )
}

@ -15,7 +15,7 @@ function Layout({ Component, pageProps, user }){
return ( return (
<> <>
<Header user={user} /> <Header user={user} />
<main><Component {...pageProps} /></main> <main><Component {...{user, ...pageProps}} /></main>
<Footer/> <Footer/>
</> </>
) )

@ -1,31 +1,17 @@
import React from 'react' import React, {useEffect} from 'react'
import Link from 'next/link' import Link from 'next/link'
import Router from 'next/router' import Router from 'next/router'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import axios from 'axios'
import {FormController, Input, Button} from '~/components/form' import {FormController, Input, Button} from '~/components/form'
import useErrorReducer from '../hooks/errorReducer'
export default function Login(){ export default function Login({user}){
const [errors, dispatch] = useErrorReducer() useEffect(()=>{
if(user) Router.push('/account')
const submit = async (values)=>{ }, [user])
try {
await axios.post(`/api/auth`, values)
Router.push('/')
} catch (err) {
if(!err.response || err.response.status !== 422) throw err;
dispatch({
type: 'set_errors',
errors: err.response.data.errors
})
}
}
return ( return (
<FormController errors={errors} errorDispatch={dispatch} onSubmit={submit}> <FormController url="/api/auth" afterSubmit={()=>Router.push('/account')}>
<h1>Login</h1> <h1>Login</h1>
<Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" /> <Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" />
<Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" /> <Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />

@ -1,30 +1,17 @@
import React from 'react' import React, {useEffect} from 'react'
import Link from 'next/link' import Link from 'next/link'
import Router from 'next/router'
import isEmail from 'validator/lib/isEmail' import isEmail from 'validator/lib/isEmail'
import axios from 'axios'
import {FormController, Input, Button} from '~/components/form' import {FormController, Input, Button} from '~/components/form'
import useErrorReducer from '../hooks/errorReducer'
export default function Register(){ export default function Register({user}){
const [errors, dispatch] = useErrorReducer() useEffect(()=>{
if(user) Router.push('/account')
const submit = async (values)=>{ }, [user])
try {
const {data} = await axios.post(`/api/users`, values)
console.log(data)
} catch (err) {
if(!err.response || err.response.status !== 422) throw err;
dispatch({
type: 'set_errors',
errors: err.response.data.errors
})
}
}
return ( return (
<FormController errors={errors} errorDispatch={dispatch} onSubmit={submit}> <FormController url="/api/users" afterSubmit={()=>Router.push('/account')}>
<h1>Register</h1> <h1>Register</h1>
<Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" /> <Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" />
<Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" /> <Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />

Loading…
Cancel
Save