// 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 ? : 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 (
{_children}
) }