You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

187 lines
5.3 KiB
JavaScript

// TODO: Enable exportDefaultFrom in Babel syntax
import React, { useState, useReducer } from 'react'
import axios from 'axios'
import Spinner from '~/components/spinner'
import styles from './styles.module.css'
export const Dropdown = require('./dropdown.js').default
export const Input = require('./input.js').default
export const IntegerInput = require('./integerInput.js').default
export const DecimalInput = require('./decimalInput.js').default
export const Button = require('./button.js').default
export const Checkbox = require('./checkbox.js').default
export const TextArea = require('./textArea.js').default
export const DateInput = require('./dateInput.js').default
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':
return {
...errors,
[action.field]: undefined
}
default:
return errors
}
}
const formReducer = errorDispatch => (state, action)=>{
let fields = {...state.fields}
const prevField = state.fields[action.name]
// If no value (onBlur), just mark touched and clear error
if(action.value === undefined){
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
const updatedField = {
...prevField,
value: action.value !== undefined ? action.value : prevField.value,
isValid: prevField.validate?prevField.validate(action.value, fields):true
}
fields[action.name] = updatedField
// Update other fields where the validate function takes whole state
const fieldNames = Object.keys(fields)
for(const name of fieldNames){
if(name === action.name) continue
const field = fields[name]
if(field.validate && field.validate.length > 1)
fields[name] = {
...field,
isValid: field.validate(field.value, fields)
}
}
return {fields}
}
export const FormController = function FormController({children, className, url, method = 'POST', afterSubmit = ()=>null}){
const initialState = {
fields: {}
}
// Update initial state
React.Children.forEach(children, child => {
if(!child.props.name) return;
initialState.fields[child.props.name] = {
name: child.props.name,
validate: child.props.validate,
transform: child.props.transform,
value: child.props.initialValue || "",
isValid: child.props.validate
?(child.props.initialValue ? child.props.validate(child.props.initialValue): false)
:true,
touched: child.props.initialValue !== undefined
}
})
// Create state
const [errors, errorDispatch] = useReducer(errorReducer, {})
const [state, dispatch] = useReducer(formReducer(errorDispatch), initialState)
const [submitting, setSubmitting] = useState(false)
// Handle submitting form
const handleSubmit = async (ev) => {
if(ev) ev.preventDefault();
const hidden = React.Children.map(children, child => {
if(child?.props?.hidden)
return child.props.name
return undefined
}).filter(name => !!name)
const data = {}
for(const name in state.fields){
if(hidden.includes(name))
continue;
const field = state.fields[name]
if(field.transform)
data[field.name] = field.transform(field.value)
else
data[field.name] = field.value
}
if(url)
try {
setSubmitting(true)
const {data: response} = await axios({ method, url, data })
return afterSubmit(response)
} catch (err) {
const status = err?.response?.status;
if(![400, 401, 403, 422, 429].includes(status)) throw err;
return errorDispatch({
type: 'set_errors',
errors: err.response.data.errors
})
} finally {
setTimeout(()=>setSubmitting(false), 200)
}
afterSubmit(data)
}
// Map children
const _children = React.Children.map(children, child => {
if(child.type === Button && child.props.type?.toLowerCase() === 'submit')
return React.cloneElement(child, {
// Allow enabled prop to disable, but not solely enable
enabled: child.props.enabled !== false && !submitting && !Object.values(state.fields).some(field=>!field.isValid),
onClick: handleSubmit,
children: submitting
? <Spinner />
: child.props.children
})
const {name} = child.props;
if(!name) return child;
if(child.props.hidden) return null;
return React.cloneElement(child, {
onChange: ev=> {
if(child.props.onChange)
child.props.onChange(ev.target.value)
dispatch({name, value: ev.target.value})
},
onBlur: ev=>dispatch({name}),
value: state.fields[name].value,
isValid: state.fields[name].touched ? state.fields[name].isValid : true,
error: errors ? errors[child.props.name] : undefined
})
})
return (
<form autoComplete="off" onSubmit={handleSubmit} className={styles.formContainer + (className?' ' + className:'')}>
{_children}
</form>
)
}