import "./style.pcss"; import { bubble } from "@web3r/flowerkit/evt"; import { getDebouncedFn } from "@web3r/flowerkit/fn"; import { isObjEmpty } from "@web3r/flowerkit/obj"; import type { FC, FormEvent, FormEventHandler, Ref, ReactElement, } from "react"; import { useCallback, useMemo, useRef, useState, useImperativeHandle, } from "react"; import { FormRow } from "./components/formRow"; import type { IForm, IFormRef, IFormCfg, TFormValues, } from "./config/types"; import { FormContext } from "./context"; import { isStateIncludesOtherState, getFormInitialValues, getCustomisationForCols } from "./lib"; import type { IComponentBaseProps } from "../../types/components"; import { getUserControlledFields } from "../../utils/getUserControlledFields"; import { useCN } from "../../utils/react/useCN"; import { Input } from "../input"; /** * Компонент формы * @returns {ReactElement} */ const Form: FC = ({ id = null, rows = [], cfg = {}, onSubmit = (e: FormEvent) => e.preventDefault(), onReset = null, baseClass = "form", children = null, setCustomComponent = null, setInputProps = null, setVisibility = null, extraClasses = {}, extraAttrs = {}, isCustomWrapper = false, isUseValuesCompare = false, initialValues = {}, auxParams = {}, isHidden = false, headerSlot = null, footerSlot = null, ref = null, }): ReactElement => { const { getCN } = useCN(baseClass); const formRef = useRef(null); const [ formValues, setFormValues ] = useState( () => (rows?.length ? getFormInitialValues({ formRows: rows }) : initialValues || {}) ); const stateActions = useMemo(() => ({ setFormValues }), []); const onChange = useCallback(({ target, detail, }: { target: { name: string; value: unknown; }; detail?: { name?: string; value?: unknown; } | unknown; }) => { const hasDetailName = typeof detail === "object" && detail !== null && "name" in detail; const source = hasDetailName ? (detail as { name?: string; value?: unknown; }) : target; const { name, value } = source; name && setFormValues((state) => ({ ...state, [name]: value as TFormValues[string] })); }, []); const handleReset = useCallback((event: FormEvent) => { if (typeof onReset === "function") { onReset({ event, data: { formValues }, stateActions }); } else { getUserControlledFields(formRef.current)?.forEach((node) => bubble(node, "reset", { bubbles: false })); } }, [ onReset, formValues, stateActions ]); const isFormElementVisible = useMemo(() => { return isUseValuesCompare ? ({ isShowForValues }: { isShowForValues?: Record; }) => isShowForValues ? isStateIncludesOtherState({ firstState: isShowForValues, secondState: formValues, }) : true : setVisibility; }, [ formValues, isUseValuesCompare, setVisibility ]); const formRows = useMemo(() => { if (!setCustomComponent && !setInputProps && !isUseValuesCompare) { return rows; } else { return rows?.map((row) => { const nextRow = { ...row, cols: row.cols?.map((col) => ({ ...col, field: col.field ? { ...col.field, input: col.field.input ? { ...col.field.input } : col.field.input, } : col.field, })), }; if (typeof isFormElementVisible === "function") { nextRow.isVisible = isFormElementVisible(nextRow); } getCustomisationForCols({ cols: nextRow.cols, setCustomComponent, setInputProps, isFormElementVisible, }); return nextRow; }); } }, [ rows, setCustomComponent, setInputProps, isFormElementVisible, isUseValuesCompare ]); const visibleRows = useMemo(() => { const keyCounter = new Map(); return formRows?.filter(({ isVisible = true }) => isVisible).map((row) => { const keyBase = JSON.stringify({ columns: row.columns, colsCount: row.cols?.length ?? 0, isShowForValues: row.isShowForValues, }); const keyOrder = (keyCounter.get(keyBase) ?? 0) + 1; keyCounter.set(keyBase, keyOrder); return ; }); }, [ formRows ]); const formBody = useMemo(() => (
{children || visibleRows}
), [ getCN, children, visibleRows ]); const formHeader = useMemo(() => ( (!!headerSlot || (!!auxParams && !isObjEmpty(auxParams))) && (
{Object.entries(auxParams || {}).map(([ name, value ]) => ( ))} {headerSlot}
) ), [ headerSlot, auxParams, getCN ]); const formFooter = useMemo(() => ( !!footerSlot &&
{footerSlot}
), [ footerSlot, getCN ]); const formExtraClasses = { ...extraClasses, isHidden }; useImperativeHandle(ref, (): IFormRef => ({ el: formRef.current, ref: null, utils: null, getData: () => ({ value: formValues }), stateActions, }), [ formValues, stateActions ]); const debouncedChangeHandler: Function = getDebouncedFn(onChange); const debouncedResetHandler: Function = getDebouncedFn(handleReset, 300, true); const ctx = useMemo(() => ({ getCN, baseClass, formValues, setFormValues, }), [ getCN, baseClass, formValues, setFormValues ]); return ( {isCustomWrapper ? (
} className={getCN("", formExtraClasses)} {...extraAttrs}> {formHeader} {formBody} {formFooter}
) : (
} hidden={isHidden} id={id || undefined} ref={formRef} onReset={debouncedResetHandler as FormEventHandler} onSubmit={onSubmit} autoComplete={"off"} className={getCN("", formExtraClasses)} data-js-form={JSON.stringify(cfg)} noValidate {...extraAttrs} > {formHeader} {formBody} {formFooter} )}
); }; export type { IForm, IFormCfg, IFormRef }; export { Form };