UNPKG

@bolttech/form-engine

Version:

A react adapter for bolttech form engine

1,008 lines (989 loc) 33.5 kB
'use client'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { createContext, useContext, useRef, useEffect, useMemo, useState, useCallback, Suspense } from 'react'; import { FormGroup, FormField } from '@bolttech/form-engine-core'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const FormGroupContext = /*#__PURE__*/createContext({}); /** * context interface to be used isolated or with the context provider * * @param {TFormContextProvider} param context parameters * @returns {TFormContext} */ const IsolatedContext = ({ debugMode: _debugMode = false, mappers, config }) => { const formGroupInstance = useRef(new FormGroup({ config, mappers })); const formGroupContextMountRef = useRef(false); const addFormWithIndex = index => { formGroupInstance.current.createFormWithIndex({ index, mappers }); }; const addForm = ({ key, params }) => { formGroupInstance.current.addForm({ key, params }); }; const removeForm = ({ key }) => { formGroupInstance.current.removeForm({ key }); }; const getForm = ({ key }) => formGroupInstance.current.getForm({ key }); const printFormGroupInstance = () => { console.log(formGroupInstance.current.printFormGroupInstance()); }; function submitMultipleFormsByIndex(indexes, callback) { return formGroupInstance.current.submitMultipleFormsByIndex(indexes, callback); } useEffect(() => { return () => { if (formGroupContextMountRef.current) { // strictMode made me do this, Beato Carlo Acutis please forgive me // eslint-disable-next-line react-hooks/exhaustive-deps formGroupInstance.current.destroy(); } }; }, []); useEffect(() => { return () => { formGroupContextMountRef.current = true; }; }, []); return { addFormWithIndex, addForm, getForm, removeForm, mappers, formGroupInstance: formGroupInstance.current, printFormGroupInstance, submitMultipleFormsByIndex, debugMode: _debugMode, active: false }; }; /** * context provider to wrap form-engine adapter elements * * @param {PropsWithChildren<TFormContextProvider>} param context parameters * @returns {ReactElement} */ const FormGroupContextProvider = ({ children, mappers, debugMode: _debugMode2 = false, config }) => { const contextValue = IsolatedContext({ mappers, debugMode: _debugMode2, config }); contextValue.debugMode = _debugMode2; contextValue.active = true; contextValue.config = config; return jsxs(FormGroupContext.Provider, { value: contextValue, children: [_debugMode2 && jsxs(Fragment, { children: [jsx("button", { onClick: () => console.log(contextValue.formGroupInstance), children: "printGroupInstance" }), jsx("br", {}), jsx("hr", {})] }), children] }); }; /** * FormGroup context hook to handle context or isolated context implementations * * @param {TFormContextProvider} props form group context parameters * @returns {TFormContext} */ const useFormGroupContext = props => { const context = useContext(FormGroupContext); if (Object.keys(context).length === 0 && props) { return IsolatedContext({ debugMode: props.debugMode, mappers: props.mappers }); } return context; }; /** * Renders the React element defined on the mappers configuration * * @param param component props, field instance and children to render * @returns */ const FieldWrapperComponentRender = ({ props, fieldInstance, children, mapper }) => { var _a, _b, _c; const Component = (mapper === null || mapper === void 0 ? void 0 : mapper.component) || ((_a = fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.mapper) === null || _a === void 0 ? void 0 : _a.component); const Asynccomponent = (mapper === null || mapper === void 0 ? void 0 : mapper.asynccomponent) || ((_b = fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.mapper) === null || _b === void 0 ? void 0 : _b.asynccomponent); if (Component) return jsx(Component, Object.assign({}, props, { children: children && children })); if (Asynccomponent) return jsx(Suspense, { children: jsx(Asynccomponent, Object.assign({}, props, { children: children && children })) }); return jsx("div", { children: `failed to render field ${fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.name} with componentName:${(_c = fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.mapper) === null || _c === void 0 ? void 0 : _c.componentName}, please check mappers` }); }; /** * Base field Wrapper to render the component with the necessary configurations from the schema * and mapper configuration * * @param {TFieldWrapperProps} param FieldWrapper params * @returns {ReactElement} */ const FieldWrapper = ({ name, formIndex, children, props, context, mounted, mapper, visibility, component }) => { var _a, _b, _c; const localContext = useFormGroupContext({}); const fieldMapper = mapper || ((_a = localContext.mappers) === null || _a === void 0 ? void 0 : _a.find(mapper => mapper.componentName === component)); /** * picks the right context prioritizing the context passed as prop */ const { formGroupInstance, debugMode } = useMemo(() => context ? context : localContext, [context, localContext]); /** * retrieves the field instance when it's mounted */ const fieldInstance = useMemo(() => { var _a; return (_a = formGroupInstance.getForm({ key: formIndex })) === null || _a === void 0 ? void 0 : _a.getField({ key: name }); }, [mounted]); /** * props with templates stripped until the field isn't ready on the instance */ const filteredProps = useMemo(() => { return FormField.filterProps(props); }, []); /** * sends props changed on the adapter for comparison to effectively change props for the instance */ useEffect(() => { if (fieldInstance && props) fieldInstance.adapterProps = props; }, [props]); const [valueState, setValueState] = useState((fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.stateValue) || { [((_b = fieldMapper === null || fieldMapper === void 0 ? void 0 : fieldMapper.events) === null || _b === void 0 ? void 0 : _b.setValue) || 'value']: '' }); const [state, setState] = useState({ visibility: typeof visibility === 'boolean' ? visibility : true, props: filteredProps }); /** * handles the mounting and unmounting logic onto the field instance */ useEffect(() => { if (!fieldInstance || (fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.mounted)) return; fieldInstance.mountField({ valueSubscription: value => { setValueState(value); }, propsSubscription: ({ visibility, props, errors }) => { setState(prev => Object.assign(Object.assign({}, prev), { visibility, props, errors })); } }); }, [fieldInstance]); /** * recycle effect to remove the field Subscriptions due to memory leaks */ useEffect(() => { return () => { (fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.mounted) && (fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.destroyField()); }; }, []); /** * handles the value change onto the field instance */ const handleChange = useCallback(event => { if (!mounted) return; fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.emitValue({ value: event, event: 'ON_FIELD_CHANGE' }); }, [mounted]); /** * handles the event emission onto the field instance */ const handleEvent = useCallback(event => { if (!mounted) return; fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.emitEvents({ event }); }, [mounted]); /** * handles the mappers configuration to bind the event submission callback * to the corresponding prop defined on the mappers */ const mapProps = useMemo(() => { const events = fieldMapper === null || fieldMapper === void 0 ? void 0 : fieldMapper.events; const props = {}; if (events === null || events === void 0 ? void 0 : events.onBlur) props[events.onBlur] = () => handleEvent('ON_FIELD_BLUR'); if (events === null || events === void 0 ? void 0 : events.getValue) props[events.getValue] = handleChange; if (events === null || events === void 0 ? void 0 : events.onFocus) props[events.onFocus] = () => handleEvent('ON_FIELD_FOCUS'); if (events === null || events === void 0 ? void 0 : events.onClick) props[events.onClick] = () => handleEvent('ON_FIELD_CLICK'); if (events === null || events === void 0 ? void 0 : events.onSubmit) props[events.onSubmit] = () => handleEvent('ON_FORM_SUBMIT'); if (events === null || events === void 0 ? void 0 : events.onKeyUp) props[events.onKeyUp] = () => handleEvent('ON_FIELD_KEYUP'); if (events === null || events === void 0 ? void 0 : events.onKeyDown) props[events.onKeyDown] = () => handleEvent('ON_FIELD_KEYDOWN'); return props; }, [mounted]); return state.visibility ? jsxs(Fragment, { children: [debugMode && jsxs("div", { style: { width: '100%', display: 'flex', flexDirection: 'column' }, children: [jsxs("b", { style: { padding: '0px', margin: '0px' }, children: ["name:", name] }), jsx("br", {}), jsx("hr", {})] }), jsx(FieldWrapperComponentRender, { props: Object.assign(Object.assign(Object.assign(Object.assign({}, mapProps), state.props), state.errors), valueState), fieldInstance: fieldInstance, mapper: fieldMapper, children: children ? children : ((_c = state === null || state === void 0 ? void 0 : state.props) === null || _c === void 0 ? void 0 : _c.children) ? state === null || state === void 0 ? void 0 : state.props.children : null })] }) : jsx(Fragment, {}); }; /** * Component Wrapper to render form fields without the Form component, gets additional formId and mapper since * it won't rely on them and needs to be manually declared * * @param {TAsFormFieldBuilderProps} props JSON schema props along with FieldWrapper props and mapper props * @returns {ReactElement} */ const AsFormFieldBuilder = props => { const context = useFormGroupContext({}); /** * state to track the field instance mounting (client-side reactivity activation) */ const [mounted, setMounted] = useState(false); const mountedRef = useRef(false); /** * initializer to create or add a form instance to the formGroup by it's formId * and add the field to the form instance */ useEffect(() => { var _a, _b; if (mountedRef.current) return; if (typeof props.formMounted === 'undefined' && !((_a = context.formGroupInstance) === null || _a === void 0 ? void 0 : _a.forms.has(props.formIndex))) { context.addFormWithIndex(props.formIndex); } if (props.formMounted || typeof props.formMounted === 'undefined') { const formInstance = context.formGroupInstance.forms.get(props.formIndex); if (formInstance && !formInstance.fields.has(props.name)) { const fieldSchema = Object.assign(Object.assign({}, props), { component: ((_b = props.mapper) === null || _b === void 0 ? void 0 : _b.componentName) || props.component, children: undefined }); formInstance.addField({ fieldSchema, mapperElement: props.mapper }); } setMounted(true); mountedRef.current = true; } }, [props.formMounted]); /** * recycle effect to remove the field from the form instance when the field leaves the vDOM * and the subscriptions */ useEffect(() => { return () => { var _a, _b; if ((_b = (_a = context.getForm({ key: props.formIndex })) === null || _a === void 0 ? void 0 : _a.getField({ key: props.name })) === null || _b === void 0 ? void 0 : _b.mounted) context.formGroupInstance.removeField({ formIndex: props.formIndex, fieldIndex: props.name }); }; }, []); /** * allows to control field selected value on each event */ useEffect(() => { var _a; if (!props.onSelected) return; const callback = payload => { if (props.onSelected) { props.onSelected(payload); } }; const sub = (_a = context.formGroupInstance.forms.get(props.formIndex)) === null || _a === void 0 ? void 0 : _a.subscribeFieldEvent({ callback }); return () => sub === null || sub === void 0 ? void 0 : sub.unsubscribe(); }, [props.onSelected]); return jsx(FieldWrapper, { formIndex: props.formIndex, name: props.name, props: props.props, context: !context.active ? context : null, mounted: mounted, mapper: props.mapper, visibility: props.visibility, component: props.component, children: props.children && props.children }); }; const BuildSchemaAsFields = ({ components, mappers, formIndex, mountedForm }) => { return components && components.map(_a => { var { component } = _a, componentEl = __rest(_a, ["component"]); const mapper = mappers === null || mappers === void 0 ? void 0 : mappers.find(el => el.componentName === component); return mapper ? jsx(AsFormFieldBuilder, Object.assign({ formIndex: formIndex, mapper: mapper, formMounted: mountedForm }, componentEl, { children: componentEl.children && componentEl.children.length > 0 && jsx(BuildSchemaAsFields, { formIndex: formIndex, mappers: mappers, components: componentEl.children, mountedForm: mountedForm }) }), componentEl.name) : jsx("div", { children: `component mapper not found for ${component} from field name ${componentEl.name} on form ${formIndex}` }, componentEl.name); }); }; /** * events mapping to aid function callback binding */ const eventsMapping = { ON_FIELD_CHANGE: 'onChange', ON_FIELD_BLUR: 'onBlur', ON_FIELD_FOCUS: 'onFocus', ON_FIELD_KEYDOWN: 'onKeyDown', ON_FIELD_KEYUP: 'onKeyUp', ON_FIELD_MOUNT: 'onMount', ON_API_FIELD_REQUEST: 'onApiRequest', ON_API_FIELD_RESPONSE: 'onApiResponse', ON_FIELD_CLICK: 'onClick', ON_FIELD_CLEARED: 'onCleared', ON_FIELD_UNMOUNT: 'onUnmount' }; const uniqueIdGen = () => { const timestamp = +new Date(); const rand = Math.random() * 1e5; const finalNumb = timestamp * rand; const string = (Math.random() + 1).toString(36).substring(7); const mixedID = finalNumb + string + '-'; const shuffle = str => [...str].sort(() => Math.random() - 0.5).join(''); return shuffle(mixedID); }; /** * Hook to register events callback functions */ function useForm(_a, deps) { var { id, index, onData, onSubmit, onFormMount, onValid, iVars, initialValues, stopEventsOnSubmit } = _a, rest = __rest(_a, ["id", "index", "onData", "onSubmit", "onFormMount", "onValid", "iVars", "initialValues", "stopEventsOnSubmit"]); const { formGroupInstance } = useFormGroupContext({}); const useFormIndex = index || id; if (!useFormIndex) { throw new Error('useForm hook must have an id or an index'); } useEffect(() => { if (!formGroupInstance.forms.has(useFormIndex)) { console.log('failed to set stopEventsOnSubmit due to no form instance'); return; } if (typeof stopEventsOnSubmit === 'boolean') { formGroupInstance.getForm({ key: useFormIndex }).stopEventsOnSubmit = stopEventsOnSubmit; } }, []); /** * iVars change tracker to update iVars onto form instance */ useEffect(() => { if (!formGroupInstance.forms.has(useFormIndex)) { console.log('failed to add iVars due to no form instance'); return; } if (iVars) formGroupInstance.forms.get(useFormIndex).iVars = iVars; // if (iVars) formInstance.current.iVars = iVars; }, [iVars]); /** * initialValues setter for async initialValues */ useEffect(() => { if (!formGroupInstance.forms.has(useFormIndex)) { console.log('failed to add initialValues due to no form instance'); return; } if (initialValues) formGroupInstance.forms.get(useFormIndex).initialValues = initialValues; // if (initialValues) formInstance.current.initialValues = initialValues; }, [initialValues]); /** * reference to store all updated callback functions rerendered by props change */ const callbackRefs = useRef(rest); useEffect(() => { callbackRefs.current = rest; }, [rest]); const formValuesCallbackRefs = useRef({ onData, onSubmit, onFormMount, onValid }); useEffect(() => { formValuesCallbackRefs.current = { onData, onSubmit, onFormMount, onValid }; }, [onData, onSubmit, onFormMount, onValid]); /** * handle function call after the debounce occurs on the form instance field event * subject in order to call the most updated function with the updated function * reference to avoid events get outdated values */ useEffect(() => { var _a, _b, _c, _d, _e; const callback = payload => { var _a, _b; const eventMapping = eventsMapping[payload.event]; if (eventMapping && callbackRefs.current[eventMapping]) { (_b = (_a = callbackRefs.current)[eventMapping]) === null || _b === void 0 ? void 0 : _b.call(_a, payload); } }; const sub = (_a = formGroupInstance.getForm({ key: useFormIndex })) === null || _a === void 0 ? void 0 : _a.subscribeFieldEvent({ callback }); const mountCallback = payload => { var _a, _b; if (formValuesCallbackRefs.current.onMount) { (_b = (_a = formValuesCallbackRefs.current).onFormMount) === null || _b === void 0 ? void 0 : _b.call(_a, payload); } }; const mountSub = (_b = formGroupInstance.getForm({ key: useFormIndex })) === null || _b === void 0 ? void 0 : _b.subscribeOnMount(mountCallback); const dataCallback = payload => { var _a, _b; if (formValuesCallbackRefs.current.onData) { (_b = (_a = formValuesCallbackRefs.current).onData) === null || _b === void 0 ? void 0 : _b.call(_a, payload); } }; const dataSub = (_c = formGroupInstance.getForm({ key: useFormIndex })) === null || _c === void 0 ? void 0 : _c.subscribeData(dataCallback); const validationCallback = payload => { if (formValuesCallbackRefs.current.onValid) { formValuesCallbackRefs.current.onValid(payload); } }; const validSub = (_d = formGroupInstance.getForm({ key: useFormIndex })) === null || _d === void 0 ? void 0 : _d.subscribeFormValidation(validationCallback); const submitCallback = payload => { var _a, _b; if (formValuesCallbackRefs.current.onSubmit) { (_b = (_a = formValuesCallbackRefs.current).onSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, payload); } }; const submitSub = (_e = formGroupInstance.getForm({ key: useFormIndex })) === null || _e === void 0 ? void 0 : _e.subscribeOnSubmit(submitCallback); return () => { sub === null || sub === void 0 ? void 0 : sub.unsubscribe(); mountSub === null || mountSub === void 0 ? void 0 : mountSub.unsubscribe(); dataSub === null || dataSub === void 0 ? void 0 : dataSub.unsubscribe(); validSub === null || validSub === void 0 ? void 0 : validSub.unsubscribe(); submitSub === null || submitSub === void 0 ? void 0 : submitSub.unsubscribe(); }; }, [deps]); return; } /** * * @param {TFormProps} param form properties initializor * @returns {ReactElement} */ function Form({ schema, index, initialValues, iVars, action, method, config, prefetchedData, onSubmit, onFormMount, onData, onBlur, onChange, onApiResponse, onClick, onFocus, onKeyDown, onKeyUp, onMount, onValid, stopEventsOnSubmit, children }) { const { getForm, mappers, debugMode, formGroupInstance } = useFormGroupContext({}); const schemaIndex = useRef(index || (schema === null || schema === void 0 ? void 0 : schema.index) || 'defaultChange').current; const [mounted, setMounted] = useState(false); const mountedRef = useRef(false); /** * Synchronously ensures the form instance exists during render. * This allows the component tree to render meaningful HTML during SSR. * Subscriptions and reactivity are still deferred to useEffect (client-only). * * Note: iVars and initialValues are also set in useForm hook (via useEffect), * but they need to be here as well so the FormCore constructor has them available * during the synchronous render phase for SSR template resolution. */ useMemo(() => { if (formGroupInstance.getForm({ key: schemaIndex })) return; const schemaWithPrefetch = prefetchedData && schema ? Object.assign(Object.assign({}, schema), { prefetchedData }) : schema; formGroupInstance.addForm({ key: schemaIndex, params: { schema: schemaWithPrefetch, action: action || (schema === null || schema === void 0 ? void 0 : schema.action), method: method || (schema === null || schema === void 0 ? void 0 : schema.method), index: schemaIndex, mappers, iVars: iVars || (schema === null || schema === void 0 ? void 0 : schema.iVars), initialValues: initialValues || (schema === null || schema === void 0 ? void 0 : schema.initialValues), config: config || formGroupInstance.config } }); }, []); /** * effect to notify the recursive generated fields of the form's mounted status * and activate client-side reactivity (subscriptions, events, etc.) */ useEffect(() => { if (mountedRef.current) return; setMounted(true); mountedRef.current = true; }, []); /** * hook usage to keep event bindings updated on callback functions passed as props */ useForm({ index: schemaIndex, initialValues, iVars, stopEventsOnSubmit, onApiResponse, onBlur, onChange, onClick, onFocus, onKeyDown, onKeyUp, onMount, onSubmit, onFormMount, onData, onValid }); /* @TODO move this logic inside form-core, add action and method onto form element, might need a ref of the form to collect all the form parameters when there is a normal form submition with redirect */ const handleSubmit = event => { const formElement = getForm({ key: schemaIndex }); if (!(formElement === null || formElement === void 0 ? void 0 : formElement.action) && event) { event.preventDefault(); } formElement === null || formElement === void 0 ? void 0 : formElement.submit(); }; return jsxs(Fragment, { children: [debugMode && jsxs(Fragment, { children: [jsx("b", { style: { padding: '0px', margin: '0px' }, children: `form index: ${schemaIndex}` }), jsx("button", { onClick: () => console.log(getForm({ key: schemaIndex })), children: "print form instance" }), jsx("button", { onClick: () => { var _a; return console.log((_a = getForm({ key: schemaIndex })) === null || _a === void 0 ? void 0 : _a.getFormValues()); }, children: "print form values" }), jsx("button", { onClick: () => { var _a; return (_a = getForm({ key: schemaIndex })) === null || _a === void 0 ? void 0 : _a.submit(); }, children: "try submit" }), jsx("br", {}), jsx("hr", {})] }), jsxs("form", { onSubmit: handleSubmit, children: [jsx(BuildSchemaAsFields, { formIndex: schemaIndex, mappers: mappers, components: schema === null || schema === void 0 ? void 0 : schema.components, mountedForm: mounted }), children] })] }); } /** * Component wrapper to aid building schemas with react without writting a JSON schema * along with BuildAsFormFieldTree inside a Form component, FieldWrapper gets this props * to build the component as it does with a JSON schema, this component only works inside * the Form component * * @param {TAsFormFieldProps} props JSON schema props * @returns {ReactNode} */ const AsFormField = props => { return props.children; }; function useFormGroup({ ids, onData, onValid, onSubmit }, deps) { const { formGroupInstance } = useFormGroupContext({}); const onDataCallbackRef = useRef(onData); const onValidCallbackRef = useRef(onValid); const onSubmitCallbackRef = useRef(onSubmit); useEffect(() => { onDataCallbackRef.current = onData; }, [onData]); useEffect(() => { onValidCallbackRef.current = onValid; }, [onValid]); useEffect(() => { onSubmitCallbackRef.current = onSubmit; }, [onSubmit]); useEffect(() => { const onDataCallback = payload => { var _a; if (onDataCallbackRef.current) { (_a = onDataCallbackRef.current) === null || _a === void 0 ? void 0 : _a.call(onDataCallbackRef, payload); } }; const onDataSub = formGroupInstance.onDataSubscription({ ids, callback: onDataCallback }); const onValidCallback = payload => { if (onValidCallbackRef.current) { onValidCallbackRef.current(payload); } }; const onValidSub = formGroupInstance.onValidSubscription({ ids, callback: onValidCallback }); const onSubmitCallback = payload => { var _a; if (onSubmitCallbackRef.current) { (_a = onSubmitCallbackRef.current) === null || _a === void 0 ? void 0 : _a.call(onSubmitCallbackRef, payload); } }; const onSubmitSub = formGroupInstance.onSubmitSubscription({ ids, callback: onSubmitCallback }); return () => { onDataSub.unsubscribe(); onValidSub.unsubscribe(); onSubmitSub.unsubscribe(); }; }, [deps]); return; } /** * Adapter do manage repeated list elements on form * * @param {TAsFormFieldRepeaterProps} props Repeater properties to configure the elements repeater * @returns {ReactElement} */ const AsFormFieldRepeater = ({ RepeaterComponent, addFieldName, removeFieldName, existingElements, initialElements, stateUpdater, formPrefix, RepeaterFooter }) => { const { getForm, formGroupInstance } = useFormGroupContext(); const [elements, setElements] = useState(typeof existingElements === 'object' && existingElements !== null ? Object.keys(existingElements).map(() => uniqueIdGen()) : typeof initialElements === 'number' ? Array.from(Array(initialElements), () => uniqueIdGen()) : []); const mountedRef = useRef(false); const prevElements = useRef(0); const REPEATER_FOOTER_ID = useMemo(() => uniqueIdGen(), []); const listeningElements = useMemo(() => { return [...elements, REPEATER_FOOTER_ID]; }, [elements]); useFormGroup({ ids: listeningElements, onData: payload => { if (stateUpdater) { if (formPrefix) { stateUpdater(elements.reduce((acc, curr, index) => { const value = payload[curr]; acc[`${formPrefix}${index + 1}`] = value; return acc; }, {})); return; } stateUpdater(payload); } } }, [listeningElements]); useEffect(() => { // @TODO: refactor, quick workarount to emit data when removing a form from the list if (prevElements.current > elements.length) { formGroupInstance.dataSubject$.next({ event: 'ON_FIELD_UNMOUNT', fieldIndex: 'unmounted_repeater_form', formIndex: elements[0] }); } if (!mountedRef.current) { elements.forEach((key, index) => { const form = getForm({ key }); if (form && (existingElements === null || existingElements === void 0 ? void 0 : existingElements[index])) { form.initialValues = existingElements[index]; } }); } const subs = listeningElements.map(key => { var _a; return (_a = getForm({ key })) === null || _a === void 0 ? void 0 : _a.subscribeFieldEvent({ callback({ event, fieldName, fieldInstance }) { if (fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.formIndex) { if (event === 'ON_FIELD_CLICK' && fieldInstance.formIndex === REPEATER_FOOTER_ID) { setElements(prev => { prev.push(uniqueIdGen()); return [...prev]; }); return; } if (event === 'ON_FIELD_CLICK' && fieldName === addFieldName) { setElements(prev => { const index = prev.indexOf(fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.formIndex); prev.splice(index + 1, 0, uniqueIdGen()); return [...prev]; }); } if (event === 'ON_FIELD_CLICK' && fieldName === removeFieldName) { if ((fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.formIndex) && elements.length > 0) setElements(prev => { const index = prev.indexOf(fieldInstance === null || fieldInstance === void 0 ? void 0 : fieldInstance.formIndex); prev.splice(index, 1); return [...prev]; }); } } } }); }); mountedRef.current = true; prevElements.current = elements.length; return () => { subs.map(sub => sub === null || sub === void 0 ? void 0 : sub.unsubscribe()); }; }, [elements]); return jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [elements.map((el, index) => jsx(RepeaterComponent, { formIndex: el, index: index }, el)), RepeaterFooter && jsx(RepeaterFooter, { formIndex: REPEATER_FOOTER_ID })] }); }; const defaultChangeEvent = event => { return event.currentTarget.value; }; const checkedChangeEvent = event => { return event.currentTarget.checked; }; const valueChangeEvent = event => { return event.target.value; }; const numberInputChangeEvent = number => { if (number) { return Number(number); } }; const datepickerChangeEvent = event => event; const dropdownChangeEvent = (event, opts) => { if (typeof event === 'object' && event !== null && 'value' in event && 'id' in event) { return { _value: event === null || event === void 0 ? void 0 : event.id, _metadata: event }; } else if ((typeof event === 'string' || typeof event === 'number') && typeof opts === 'object' && 'props' in opts && typeof opts.props === 'object' && 'optionList' in opts.props && Array.isArray(opts.props.optionList)) { const option = opts.props.optionList.find(el => el.value === event); if (option) { return { _value: option.id, _metadata: option }; } else { return { _value: event, _metadata: event }; } } else { return { _value: event, _metadata: event }; } }; export { AsFormField, AsFormFieldBuilder, AsFormFieldRepeater, Form, FormGroupContext, FormGroupContextProvider, checkedChangeEvent, datepickerChangeEvent, defaultChangeEvent, dropdownChangeEvent, numberInputChangeEvent, useForm, useFormGroup, useFormGroupContext, valueChangeEvent };