import { toggle } from "@saleor/utils/lists"; import isEqual from "lodash/isEqual"; import omit from "lodash/omit"; import React from "react"; import { useState } from "react"; import useStateFromProps from "./useStateFromProps"; export interface ChangeEvent { target: { name: string; value: TData; }; } export type SubmitPromise = Promise; export type FormChange = (event: ChangeEvent, cb?: () => void) => void; export type FormErrors = { [field in keyof T]?: string | React.ReactNode; }; export interface UseFormResult { change: FormChange; data: T; hasChanged: boolean; reset: () => void; set: (data: Partial) => void; submit: () => void; triggerChange: () => void; toggleValue: FormChange; errors: FormErrors; setError: (name: keyof T, error: string | React.ReactNode) => void; clearErrors: (name?: keyof T | Array) => void; } type FormData = Record; function merge(prevData: T, prevState: T, data: T): T { return Object.keys(prevState).reduce( (acc, key) => { if (!isEqual(data[key], prevData[key])) { acc[key as keyof T] = data[key]; } return acc; }, { ...prevState } ); } function handleRefresh( data: T, newData: T, setChanged: (status: boolean) => void ) { if (isEqual(data, newData)) { setChanged(false); } } function useForm( initial: T, onSubmit?: (data: T) => SubmitPromise | void ): UseFormResult { const [hasChanged, setChanged] = useState(false); const [errors, setErrors] = useState>({}); const [data, setData] = useStateFromProps(initial, { mergeFunc: merge, onRefresh: newData => handleRefresh(data, newData, setChanged) }); function toggleValue(event: ChangeEvent, cb?: () => void) { const { name, value } = event.target; const field = data[name as keyof T]; if (Array.isArray(field)) { if (!hasChanged) { setChanged(true); } setData({ ...data, [name]: toggle(value, field, isEqual) }); } if (typeof cb === "function") { cb(); } } function change(event: ChangeEvent) { const { name, value } = event.target; if (!(name in data)) { console.error(`Unknown form field: ${name}`); return; } else { if (data[name] !== value) { setChanged(true); } setData(data => ({ ...data, [name]: value })); } } function reset() { setData(initial); } function set(newData: Partial, setHasChanged = true) { setData(data => ({ ...data, ...newData })); setChanged(setHasChanged); } async function submit() { if (typeof onSubmit === "function" && !Object.keys(errors).length) { const result = onSubmit(data); if (result) { const errors = await result; if (errors?.length === 0) { setChanged(false); } } } } function triggerChange() { setChanged(true); } const setError = (field: keyof T, error: string | React.ReactNode) => setErrors(e => ({ ...e, [field]: error })); const clearErrors = (field?: keyof T | Array) => { if (!field) { setErrors({}); } else { setErrors(errors => omit>(errors, Array.isArray(field) ? field : [field]) ); } }; return { setError, errors, change, clearErrors, data, hasChanged, reset, set, submit, toggleValue, triggerChange }; } export default useForm;