dynamic-modal
Version:
The dynamic-modal is a solution of creation different modals into project using a json configuration file
179 lines (178 loc) • 11.4 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { createElement as _createElement } from "react";
import { useContext, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Portal } from './components/portal/portal';
import MakeToggle from './components/make-toggle/make-toggle';
import MakeInput from './components/make-input/make-input';
import MakeSelect from './components/make-select/make-select';
import MakeTextarea from './components/make-textarea/make-textarea';
import MakeDescription from './components/make-description/make-description';
import MakeUpload from './components/make-upload/make-upload';
import MakeButton from './components/make-button/make-button';
import { ComponentStateContext } from './context/component/component-state';
import MakeCustomUpload from './components/make-custom-upload/make-custom-upload';
import MakeWatcher from './components/make-watcher/make-watcher';
import MakeTable from './components/make-table/make-table';
const getUseSubmit = (modalConfig) => {
return modalConfig.useSubmit !== false;
};
const getFieldKey = (element, index) => {
const fieldKey = 'name' in element && element.name
? element.name
: 'id' in element && element.id
? element.id
: element.elementType === 'group'
? (element.title ?? `group-${index}`)
: element.elementType === 'text'
? (element.text ?? `text-${index}`)
: `field-${index}`;
return `${element.elementType}-${fieldKey}`;
};
const renderModalField = (field, index, { fieldProps, getValues }) => {
const { elementType, ...element } = field;
const fieldKey = getFieldKey(field, index);
return elementType === 'input' ? (_createElement(MakeInput, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'select' ? (_createElement(MakeSelect, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'textarea' ? (_createElement(MakeTextarea, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'toggle' ? (_createElement(MakeToggle, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'text' ? (_createElement(MakeDescription, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'upload' ? (_createElement(MakeUpload, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'custom-upload' ? (_createElement(MakeCustomUpload, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'watcher' ? (_createElement(MakeWatcher, { ...fieldProps, key: fieldKey, element: element })) : elementType === 'button' ? (_createElement(MakeButton, { ...fieldProps, key: fieldKey, element: element, getValues: getValues })) : elementType === 'table' ? (_createElement(MakeTable, { ...fieldProps, key: fieldKey, element: element })) : null;
};
const ModalFields = ({ modalReady, fieldProps, getValues, BodyComponent, bodyClassName, bodyStyle, }) => {
const bodyContent = (_jsx(_Fragment, { children: modalReady.fields.map((element, index) => {
if (element.elementType !== 'group') {
return renderModalField(element, index, { fieldProps, getValues });
}
const groupElements = element.groups
.filter((sub) => !['group', 'watcher', 'table'].includes(sub.elementType))
.map((sub, subIndex) => renderModalField(sub, index + subIndex, { fieldProps, getValues }));
const hideDiv = groupElements.every((component) => component === null);
return (_jsxs("div", { className: `flex flex-col w-full gap-2 ${hideDiv && 'hidden'}`, children: [element.title && (_jsx("h3", { className: "font-bold border-b-2 pb-2 mb-2", children: element.title })), _jsx("div", { className: "flex gap-4 w-full", style: element.style, children: groupElements })] }, getFieldKey(element, index)));
}) }));
const currentBodyClassName = `flex flex-col gap-4 py-4${bodyClassName ? ` ${bodyClassName}` : ''}`;
const currentBodyStyle = {
overflowY: modalReady.overFlowBody ? 'auto' : undefined,
height: modalReady.overFlowBody,
minHeight: modalReady.minHeightBody,
...bodyStyle,
};
if (BodyComponent) {
return (_jsx(BodyComponent, { className: currentBodyClassName, style: currentBodyStyle, children: bodyContent }));
}
return (_jsx("div", { className: currentBodyClassName, style: currentBodyStyle, children: bodyContent }));
};
const ModalActions = ({ modalReady, closeHandler, manualSubmit, FooterComponent, footerClassName, footerStyle, showFooterDivider = true, }) => {
const { ModalButtonAction, ModalButtonCancel } = useContext(ComponentStateContext);
const footerContent = (_jsxs(_Fragment, { children: [modalReady.actions.cancel && (_jsx(ModalButtonCancel, { ...modalReady.actions.cancel, onClick: closeHandler })), getUseSubmit(modalReady) ? (_jsx(ModalButtonAction, { ...modalReady.actions.action, type: "submit" })) : (_jsx(ModalButtonAction, { ...modalReady.actions.action, onClick: manualSubmit, type: "button" }))] }));
const currentFooterClassName = `flex gap-4 items-center justify-center p-2${showFooterDivider ? ' border-t' : ''}${footerClassName ? ` ${footerClassName}` : ''}`;
const currentFooterStyle = {
...footerStyle,
...modalReady.actions.containerStyle,
};
if (FooterComponent) {
return (_jsx(FooterComponent, { className: currentFooterClassName, style: currentFooterStyle, children: footerContent }));
}
return (_jsx("div", { className: currentFooterClassName, style: currentFooterStyle, children: footerContent }));
};
export const Modal = ({ open, close, config }) => {
const [modalReady, setModalReady] = useState(undefined);
const [defaultLoaded, setDefaultLoaded] = useState(false);
const { control, handleSubmit, getValues, unregister, setValue, watch, trigger, reset, getFieldState, } = useForm();
const { ModalContainer, ModalHeader, ModalTitle, ModalBody, ModalFooter } = useContext(ComponentStateContext);
const formValueHandler = (element) => {
if ([
'group',
'upload',
'custom-upload',
'text',
'description',
'watcher',
'table',
].includes(element.elementType))
return;
if (element.defaultValue === undefined && element.renderIf) {
unregister(element.name);
return;
}
const defaultValue = element.defaultValue ?? '';
const parsedValue = defaultValue === 'true'
? true
: defaultValue === 'false'
? false
: defaultValue;
setValue(element.name, parsedValue ?? '');
};
const autoLoadGroup = (groupFields) => {
groupFields.forEach((element) => {
formValueHandler(element);
});
};
const autoLoadField = (modalFields) => {
if (defaultLoaded)
return;
modalFields.forEach((element) => {
if (element.elementType === 'group') {
autoLoadGroup(element.groups);
return;
}
formValueHandler(element);
});
setDefaultLoaded(true);
};
const fieldProps = useMemo(() => ({
control,
watch,
setValue,
unregister,
}), [control, setValue, unregister, watch]);
const closeHandler = () => {
if (modalReady?.onClose)
modalReady.onClose();
setTimeout(() => {
setModalReady(undefined);
setDefaultLoaded(false);
reset();
close();
}, 200);
};
const manualSubmit = async () => {
const form = getValues();
const fields = Object.keys(form);
await trigger(fields);
const validations = fields.map((field) => {
const { invalid } = getFieldState(field);
return invalid;
});
const result = validations.some((isInvalid) => isInvalid)
? undefined
: form;
if (!result)
return;
actionHandler({ ...(modalReady?.reservedData ?? {}), ...form });
};
const actionHandler = (data) => {
modalReady?.out({ ...(modalReady?.reservedData ?? {}), ...data });
closeHandler();
};
useEffect(() => {
if (open && !modalReady)
setModalReady(config);
}, [config, modalReady, open]);
useEffect(() => {
if (!modalReady || defaultLoaded)
return;
const timeout = window.setTimeout(() => {
autoLoadField(modalReady.fields);
}, 0);
return () => window.clearTimeout(timeout);
}, [defaultLoaded, modalReady]);
if (!modalReady)
return null;
const { container: containerLayout, header: headerLayout, title: titleLayout, body: bodyLayout, footer: footerLayout, } = modalReady.layout ?? {};
const showHeaderDivider = headerLayout?.showDivider !== false;
const showFooterDivider = footerLayout?.showDivider !== false;
const headerClassName = `w-full text-center${showHeaderDivider ? ' border-b pb-4' : ''}${headerLayout?.className ? ` ${headerLayout.className}` : ''}`;
const titleClassName = `font-semibold${titleLayout?.className ? ` ${titleLayout.className}` : ''}`;
const titleContent = ModalTitle ? (_jsx(ModalTitle, { className: titleClassName, style: titleLayout?.style, children: modalReady.title })) : (_jsx("h2", { className: titleClassName, style: titleLayout?.style, children: modalReady.title }));
const headerContent = ModalHeader ? (_jsx(ModalHeader, { className: headerClassName, style: headerLayout?.style, children: titleContent })) : (_jsx("div", { className: headerClassName, style: headerLayout?.style, children: titleContent }));
const modalContent = (_jsxs("form", { className: "flex flex-col p-4 gap-4", autoComplete: "off", onSubmit: handleSubmit(actionHandler), children: [headerContent, _jsx(ModalFields, { modalReady: modalReady, fieldProps: fieldProps, getValues: getValues, BodyComponent: ModalBody, bodyClassName: bodyLayout?.className, bodyStyle: bodyLayout?.style }), _jsx(ModalActions, { modalReady: modalReady, closeHandler: closeHandler, manualSubmit: manualSubmit, FooterComponent: ModalFooter, footerClassName: footerLayout?.className, footerStyle: footerLayout?.style, showFooterDivider: showFooterDivider })] }));
return (_jsx(Portal, { closeTime: 200, portalOpen: open, portalTag: '#modal-portal', useBlur: modalReady.useBlur, children: ModalContainer ? (_jsx(ModalContainer, { className: `rounded bg-white relative w-auto h-auto min-h-[200px] min-w-[500px]${modalReady.useBlur ? ' shadow-md border border-gray-200' : ''}${containerLayout?.className ? ` ${containerLayout.className}` : ''}`, style: { ...modalReady.style, ...containerLayout?.style }, children: modalContent })) : (_jsx("div", { className: `rounded bg-white relative w-auto h-auto min-h-[200px] min-w-[500px]${modalReady.useBlur ? ' shadow-md border border-gray-200' : ''}${containerLayout?.className ? ` ${containerLayout.className}` : ''}`, style: { ...modalReady.style, ...containerLayout?.style }, children: modalContent })) }));
};
export default Modal;