UNPKG

@aot-technologies/formio-react

Version:

React renderer for form.io forms.

271 lines (270 loc) 12.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FormBuilder = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const formiojs_1 = require("@aot-technologies/formiojs"); const structured_clone_1 = __importDefault(require("@ungap/structured-clone")); function createCustomConditions(component, operator, value, existingCustomConditional) { let condition = ''; switch (operator) { case 'isEqual': condition = `data.${component} && data.${component} == '${value}'`; break; case 'isNotEqual': condition = `data.${component} && data.${component} != '${value}'`; break; case 'isEmpty': condition = `!data.${component}`; break; case 'isNotEmpty': condition = `!!data.${component}`; break; case 'includes': condition = `data.${component} && data.${component}.includes('${value}')`; break; case 'notIncludes': condition = `data.${component} && !data.${component}.includes('${value}')`; break; case 'endsWith': condition = `data.${component} && data.${component}.endsWith('${value}')`; break; default: break; } if (existingCustomConditional) { if (existingCustomConditional.endsWith(';')) { return existingCustomConditional.slice(0, -1) + ' && ' + condition; } return existingCustomConditional + ' && ' + condition; } return `show = ${condition}`; } function iterateConditionsAndSetLogic(components) { components.forEach((comp) => { var _a; if ((_a = comp === null || comp === void 0 ? void 0 : comp.conditional) === null || _a === void 0 ? void 0 : _a.conditions) { comp.conditional.conditions.forEach((condition) => { comp.customConditional = createCustomConditions(condition.component, condition.operator, condition.value, comp.customConditional); }); } if (comp.customConditional && !comp.customConditional.endsWith(';')) { comp.customConditional = comp.customConditional.concat(';'); } }); return components; } // Read the builder's current form safely: guard that the instance is still // alive (.events is deleted by teardown() on destroy) and that form is truthy. const readForm = (builderRef) => { const inst = builderRef.instance; if (!inst || !inst.events) return undefined; const form = inst.form; return form !== null && form !== void 0 ? form : undefined; }; const createBuilderInstance = (BuilderConstructor_1, formSource_1, element_1, ...args_1) => __awaiter(void 0, [BuilderConstructor_1, formSource_1, element_1, ...args_1], void 0, function* (BuilderConstructor, formSource, element, options = {}, setBuiderStatus = () => { }) { const builder = BuilderConstructor ? new BuilderConstructor(element, formSource, options) : new formiojs_1.FormBuilder(element, formSource, options); setBuiderStatus(builder, false); yield builder.ready; setBuiderStatus(builder, true); return builder; }); const FormBuilder = ({ options, Builder, initialForm, onBuilderReady, onChange, onSaveComponent, onAddComponent, onEditComponent, onUpdateComponent, onDeleteComponent, }) => { // Memoize so the useEffect([builderInstance, handlers]) doesn't re-run (and detach/re-attach // all 8 formio event listeners) on every parent render. Without this, any parent state // change (e.g. a Redux update triggered by onChange) causes needless listener churn. const handlers = (0, react_1.useMemo)(() => ({ onChange, onSaveComponent, onAddComponent, onEditComponent, onUpdateComponent, onDeleteComponent, }), [ onChange, onSaveComponent, onAddComponent, onEditComponent, onUpdateComponent, onDeleteComponent, ]); const renderElement = (0, react_1.useRef)(null); const [builderInstance, setBuilderInstance] = (0, react_1.useState)(null); const isMounted = (0, react_1.useRef)(false); const currentFormSourceJsonProp = (0, react_1.useRef)(null); const pendingBuilder = (0, react_1.useRef)(null); (0, react_1.useEffect)(() => { return () => { var _a; if (builderInstance) { (_a = builderInstance.instance) === null || _a === void 0 ? void 0 : _a.destroy(true); builderInstance.destroy(true); } }; }, [builderInstance]); (0, react_1.useEffect)(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); (0, react_1.useEffect)(() => { if (typeof initialForm === 'object' && currentFormSourceJsonProp.current && formiojs_1.Utils._.isEqual(currentFormSourceJsonProp.current, initialForm)) { return; } const createInstance = () => __awaiter(void 0, void 0, void 0, function* () { var _a, _b; if (!renderElement.current) { console.warn('FormBuilder render element not found, cannot render builder.'); return; } currentFormSourceJsonProp.current = initialForm && typeof initialForm !== 'string' ? (0, structured_clone_1.default)(initialForm) : null; // destroy prev builder that is not ready before to create the new one if (pendingBuilder.current) { const prevBuilder = pendingBuilder.current; // wait the prev builder to be ready before destroying it yield prevBuilder.ready; (_a = prevBuilder.instance) === null || _a === void 0 ? void 0 : _a.destroy(true); prevBuilder.destroy(true); pendingBuilder.current = null; } const builder = yield createBuilderInstance(Builder, currentFormSourceJsonProp.current || initialForm, renderElement.current, options, (builder, ready) => { pendingBuilder.current = ready ? null : builder; }); if (builder) { if (!isMounted.current) { (_b = builder.instance) === null || _b === void 0 ? void 0 : _b.destroy(true); builder.destroy(true); } if (onBuilderReady) { onBuilderReady(builder); } setBuilderInstance((prevInstance) => { var _a; if (prevInstance) { (_a = prevInstance.instance) === null || _a === void 0 ? void 0 : _a.destroy(true); prevInstance.destroy(true); } return builder; }); } else { console.warn('Failed to create form builder instance'); } }); createInstance(); }, [Builder, initialForm, onBuilderReady, options]); (0, react_1.useEffect)(() => { if (!builderInstance) return; const inst = builderInstance.instance; // Guard: instance must exist and not yet be destroyed (teardown deletes .events) if (!inst || !inst.events) return; const { onSaveComponent, onEditComponent, onUpdateComponent, onDeleteComponent, onChange, } = handlers; let live = true; // Read form safely: check .events (destroyed guard) then the form value itself. const guard = () => { if (!live) return; const form = readForm(builderInstance); if (form) { if (form.components) { form.components = iterateConditionsAndSetLogic(form.components); } onChange === null || onChange === void 0 ? void 0 : onChange((0, structured_clone_1.default)(form)); } }; // Named handler functions — the exact same reference must be passed to both // .on() and .off(). Anonymous functions passed to .off() never match the stored // listener (compared by reference) so the call is always a no-op. const onSaveHandler = (component, original, parent, path, index, isNew, originalComponentSchema) => { if (!live) return; onSaveComponent === null || onSaveComponent === void 0 ? void 0 : onSaveComponent(component, parent, index, originalComponentSchema, path, isNew); guard(); }; const onUpdateHandler = (component) => { if (!live) return; onUpdateComponent === null || onUpdateComponent === void 0 ? void 0 : onUpdateComponent(component); guard(); }; const onRemoveHandler = (component, parent, path, index) => { if (!live) return; onDeleteComponent === null || onDeleteComponent === void 0 ? void 0 : onDeleteComponent(component, parent, path, index); guard(); }; const onCancelHandler = (component) => { if (!live) return; onUpdateComponent === null || onUpdateComponent === void 0 ? void 0 : onUpdateComponent(component); guard(); }; const onEditHandler = (component) => { if (!live) return; onEditComponent === null || onEditComponent === void 0 ? void 0 : onEditComponent(component); // Do NOT call onChange here: opening the edit modal does not mutate the form schema. // Firing onChange here triggers an unnecessary Redux update + React re-render on every // component click and every drag-drop, causing visible lag. }; const onAddHandler = () => { if (!live) return; guard(); }; const onPdfHandler = () => { if (!live) return; guard(); }; const onDisplayHandler = () => { if (!live) return; guard(); }; inst.on('saveComponent', onSaveHandler); inst.on('updateComponent', onUpdateHandler); inst.on('removeComponent', onRemoveHandler); inst.on('cancelComponent', onCancelHandler); inst.on('editComponent', onEditHandler); inst.on('addComponent', onAddHandler); inst.on('pdfUploaded', onPdfHandler); inst.on('setDisplay', onDisplayHandler); return () => { live = false; inst.off('saveComponent', onSaveHandler); inst.off('updateComponent', onUpdateHandler); inst.off('removeComponent', onRemoveHandler); inst.off('cancelComponent', onCancelHandler); inst.off('editComponent', onEditHandler); inst.off('addComponent', onAddHandler); inst.off('pdfUploaded', onPdfHandler); inst.off('setDisplay', onDisplayHandler); }; }, [builderInstance, handlers]); return (0, jsx_runtime_1.jsx)("div", { ref: renderElement }); }; exports.FormBuilder = FormBuilder;