|
|
@ -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,28 +90,43 @@ 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;
|
|
|
|
if(!name) return child;
|
|
|
|
if(!name) return child;
|
|
|
|
return React.cloneElement(child, {
|
|
|
|
return React.cloneElement(child, {
|
|
|
@ -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>
|
|
|
|
)
|
|
|
|
)
|
|
|
|