trezy/firebase-system-update

View on GitHub
src/components/Form/index.js

Summary

Maintainability
B
5 hrs
Test Coverage
// Module imports
import {
    createContext,
    useCallback,
    useContext,
    useReducer,
} from 'react'
import PropTypes from 'prop-types'





// Constants
const INITIAL_STATE = {
    errors: {},
    isValid: false,
    isTouched: false,
    touched: {},
    validity: {},
    values: {},
}





function reducer(state, action) {
    const {
        payload,
        type,
    } = action
    const newState = {
        ...INITIAL_STATE,
        ...state,
    }

    switch (type) {
        case 'validity changed':
            newState.validity = {
                ...newState.validity,
                [payload.fieldName]: !payload.errors?.length,
            }
            newState.errors = {
                ...newState.errors,
                [payload.fieldName]: payload.errors,
            }
            newState.isValid = !Object
                .values(newState.validity)
                .some(isValid => !isValid)
            break

        case 'value changed':
            newState.values = {
                ...newState.values,
                [payload.fieldName]: payload.value,
            }
            newState.touched = {
                ...newState.touched,
                [payload.fieldName]: newState.initialValues[payload.fieldName] !== payload.value,
            }
            newState.isTouched = Object
                .values(newState.touched)
                .some(isTouched => isTouched)
            break

        default:
            console.warn(`Unrecognized action dispatched: ${type}`, payload)
            return state
    }

    return newState
}





const FormContext = createContext({
    ...INITIAL_STATE,
    updateValidity: () => {},
    updateValue: () => {},
})

const Form = props => {
    const {
        children,
        className,
        initialValues,
        onSubmit,
    } = props
    const [state, dispatch] = useReducer(reducer, {
        ...INITIAL_STATE,
        errors: Object
            .keys(initialValues)
            .reduce((accumulator, key) => {
                accumulator[key] = []
                return accumulator
            }, {}),
        initialValues,
        touched: Object
            .keys(initialValues)
            .reduce((accumulator, key) => {
                accumulator[key] = false
                return accumulator
            }, {}),
        validity: Object
            .keys(initialValues)
            .reduce((accumulator, key) => {
                accumulator[key] = false
                return accumulator
            }, {}),
        values: { ...initialValues },
    })

    const handleSubmit = useCallback(event => {
        event.preventDefault()
        onSubmit(state)
    }, [
        onSubmit,
        state,
    ])

    const updateValidity = useCallback((fieldName, errors) => {
        dispatch({
            payload: {
                errors,
                fieldName,
            },
            type: 'validity changed',
        })
    }, [dispatch])

    const updateValue = useCallback((fieldName, value) => {
        dispatch({
            payload: {
                fieldName,
                value,
            },
            type: 'value changed',
        })
    }, [
        dispatch,
    ])

    return (
        <FormContext.Provider
            value={{
                ...state,
                updateValidity,
                updateValue,
            }}>
            <form
                className={className}
                onSubmit={handleSubmit}>
                {children}
            </form>
        </FormContext.Provider>
    )
}

Form.propTypes = {
    className: null,
    initialValues: {},
    onSubmit: () => {},
}

Form.propTypes = {
    children: PropTypes.node.isRequired,
    className: PropTypes.string,
    initialValues: PropTypes.object,
    onSubmit: PropTypes.func,
}

const useForm = () => useContext(FormContext)





export {
    FormContext,
    Form,
    useForm,
}