// TODO: Enable exportDefaultFrom in Babel syntax import React, { useReducer } from 'react' import axios from 'axios' import styles from './styles.module.css' export const Input = require('./input.js').default export const NumInput = require('./numInput.js').default export const Button = require('./button.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, value: child.props.initialValue || "", isValid: child.props.validate ?(child.props.initialValue ? child.props.validate(child.props.initialValue): false) :true, touched: false } }) // Create reducers const [errors, errorDispatch] = useReducer(errorReducer, {}) const [state, dispatch] = useReducer(formReducer(errorDispatch), initialState) // Handle submitting form const handleSubmit = async (ev) => { if(ev) ev.preventDefault(); const data = {} for(const name in state.fields){ const field = state.fields[name] data[field.name] = field.value } if(url) try { const {data: response} = await axios({ method, url, data }) return afterSubmit(response) } 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 => { 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 && !Object.values(state.fields).some(field=>!field.isValid), onClick: handleSubmit }) const {name} = child.props; if(!name) return child; return React.cloneElement(child, { onChange: ev=>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 (
) }