@aot-technologies/formio-react
Version:
React renderer for form.io forms.
271 lines (270 loc) • 12.2 kB
JavaScript
;
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;