@proveanything/smartlinks-form-renderer
Version:
SmartLinks form renderer and editor — render forms from JSON schema or build them with the visual editor
720 lines (702 loc) • 45.1 kB
JavaScript
;
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var evaluateFieldConditions = function (conditions, showWhen, formData) {
if (conditions === void 0) { conditions = []; }
if (showWhen === void 0) { showWhen = 'all'; }
if (formData === void 0) { formData = {}; }
if (!conditions || conditions.length === 0) {
return true; // No conditions means field is always visible
}
var results = conditions.map(function (condition) {
var targetValue = formData[condition.targetFieldId];
switch (condition.operator) {
case 'equals':
return targetValue === condition.value;
case 'not_equals':
return targetValue !== condition.value;
case 'contains':
return typeof targetValue === 'string' && targetValue.includes(condition.value);
case 'not_contains':
return typeof targetValue === 'string' && !targetValue.includes(condition.value);
case 'greater_than':
return Number(targetValue) > Number(condition.value);
case 'less_than':
return Number(targetValue) < Number(condition.value);
case 'is_empty':
return !targetValue || targetValue === '' || (Array.isArray(targetValue) && targetValue.length === 0);
case 'is_not_empty':
return targetValue && targetValue !== '' && (!Array.isArray(targetValue) || targetValue.length > 0);
case 'is_true':
return targetValue === true;
case 'is_false':
return targetValue === false || targetValue === undefined || targetValue === null;
default:
return true;
}
});
return showWhen === 'all' ? results.every(Boolean) : results.some(Boolean);
};
/******************************************************************************
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 */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
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;
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var TableInput = function (_a) {
var columns = _a.columns, _b = _a.value, value = _b === void 0 ? [] : _b, onChange = _a.onChange, _c = _a.minRows, minRows = _c === void 0 ? 1 : _c, maxRows = _a.maxRows, _d = _a.className, className = _d === void 0 ? '' : _d;
var _e = React.useState([]), data = _e[0], setData = _e[1];
// Initialize data when component mounts or when value prop changes
React.useEffect(function () {
var initializeData = function () {
var initialData = __spreadArray([], value, true);
var _loop_1 = function () {
var newRow = {};
columns.forEach(function (col) {
newRow[col.id] = col.type === 'boolean' ? false : '';
});
initialData.push(newRow);
};
// Ensure minimum rows
while (initialData.length < minRows) {
_loop_1();
}
return initialData;
};
setData(initializeData());
}, [value, columns, minRows]);
var updateData = function (newData) {
setData(newData);
onChange(newData);
};
var addRow = function () {
if (maxRows && data.length >= maxRows)
return;
var newRow = {};
columns.forEach(function (col) {
newRow[col.id] = col.type === 'boolean' ? false : '';
});
updateData(__spreadArray(__spreadArray([], data, true), [newRow], false));
};
var removeRow = function (index) {
if (data.length <= minRows)
return;
var newData = data.filter(function (_, i) { return i !== index; });
updateData(newData);
};
var updateCell = function (rowIndex, columnId, cellValue) {
var _a;
var newData = __spreadArray([], data, true);
newData[rowIndex] = __assign(__assign({}, newData[rowIndex]), (_a = {}, _a[columnId] = cellValue, _a));
updateData(newData);
};
var renderCell = function (row, column, rowIndex) {
var value = row[column.id] || '';
switch (column.type) {
case 'boolean':
return (jsxRuntime.jsx("input", { type: "checkbox", checked: value === true, onChange: function (e) { return updateCell(rowIndex, column.id, e.target.checked); }, className: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }));
case 'number':
return (jsxRuntime.jsx("input", { type: "number", value: value, onChange: function (e) { return updateCell(rowIndex, column.id, parseFloat(e.target.value) || 0); }, className: "w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500", style: { width: column.width || 'auto' } }));
case 'date':
return (jsxRuntime.jsx("input", { type: "date", value: value, onChange: function (e) { return updateCell(rowIndex, column.id, e.target.value); }, className: "w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500", style: { width: column.width || 'auto' } }));
default: // string
return (jsxRuntime.jsx("input", { type: "text", value: value, onChange: function (e) { return updateCell(rowIndex, column.id, e.target.value); }, className: "w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500", style: { width: column.width || 'auto' } }));
}
};
if (columns.length === 0) {
return (jsxRuntime.jsx("div", { className: "p-4 border border-dashed border-gray-300 rounded-lg text-center text-gray-500", children: "No columns configured for this table field." }));
}
return (jsxRuntime.jsxs("div", { className: "space-y-4 ".concat(className), children: [jsxRuntime.jsx("div", { className: "overflow-x-auto", children: jsxRuntime.jsxs("table", { className: "w-full border-collapse border border-gray-300", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { className: "bg-gray-50", children: [columns.map(function (column) { return (jsxRuntime.jsxs("th", { className: "border border-gray-300 px-3 py-2 text-left font-medium text-gray-700", style: { width: column.width || 'auto' }, children: [column.title, column.required && jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, column.id)); }), jsxRuntime.jsx("th", { className: "border border-gray-300 px-3 py-2 text-center font-medium text-gray-700 w-16", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: data.map(function (row, rowIndex) { return (jsxRuntime.jsxs("tr", { className: "hover:bg-gray-50", children: [columns.map(function (column) { return (jsxRuntime.jsx("td", { className: "border border-gray-300 px-3 py-2", children: renderCell(row, column, rowIndex) }, column.id)); }), jsxRuntime.jsx("td", { className: "border border-gray-300 px-3 py-2 text-center", children: jsxRuntime.jsx("button", { type: "button", onClick: function () { return removeRow(rowIndex); }, disabled: data.length <= minRows, className: "text-red-600 hover:text-red-800 disabled:text-gray-400 disabled:cursor-not-allowed px-2 py-1 rounded", children: "\u00D7" }) })] }, rowIndex)); }) })] }) }), jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsx("button", { type: "button", onClick: addRow, disabled: maxRows ? data.length >= maxRows : false, className: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed", children: "+ Add Row" }), jsxRuntime.jsxs("span", { className: "text-sm text-gray-600", children: [data.length, " row", data.length !== 1 ? 's' : '', minRows && " (min: ".concat(minRows, ")"), maxRows && " (max: ".concat(maxRows, ")")] })] })] }));
};
// Default styled components that work without external dependencies
var Button = function (_a) {
var children = _a.children, _b = _a.className, className = _b === void 0 ? '' : _b, _c = _a.variant, variant = _c === void 0 ? 'default' : _c, props = __rest(_a, ["children", "className", "variant"]);
var baseClasses = 'px-4 py-2 rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
var variantClasses = variant === 'outline'
? 'border-2 border-input text-foreground hover:bg-accent hover:text-accent-foreground focus:ring-ring'
: 'bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-ring';
return (jsxRuntime.jsx("button", __assign({ className: "".concat(baseClasses, " ").concat(variantClasses, " ").concat(className) }, props, { children: children })));
};
var Input = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, props = __rest(_a, ["className"]);
return (jsxRuntime.jsx("input", __assign({ className: "w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ".concat(className) }, props)));
};
var Textarea = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, props = __rest(_a, ["className"]);
return (jsxRuntime.jsx("textarea", __assign({ className: "w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring resize-vertical ".concat(className) }, props)));
};
var Label = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, children = _a.children, props = __rest(_a, ["className", "children"]);
return (jsxRuntime.jsx("label", __assign({ className: "block text-sm font-medium text-foreground mb-1 ".concat(className) }, props, { children: children })));
};
var Select = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, placeholder = _a.placeholder, onValueChange = _a.onValueChange, onChange = _a.onChange, children = _a.children, props = __rest(_a, ["className", "placeholder", "onValueChange", "onChange", "children"]);
return (jsxRuntime.jsxs("select", __assign({ className: "w-full px-3 py-2 border border-input bg-background text-foreground rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ".concat(className), onChange: function (e) {
onChange === null || onChange === void 0 ? void 0 : onChange(e);
onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(e.target.value);
} }, props, { children: [placeholder && jsxRuntime.jsx("option", { value: "", children: placeholder }), children] })));
};
var Checkbox = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, onCheckedChange = _a.onCheckedChange, onChange = _a.onChange, props = __rest(_a, ["className", "onCheckedChange", "onChange"]);
return (jsxRuntime.jsx("input", __assign({ type: "checkbox", className: "h-4 w-4 text-primary focus:ring-ring border-input rounded ".concat(className), onChange: function (e) {
onChange === null || onChange === void 0 ? void 0 : onChange(e);
onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(e.target.checked);
} }, props)));
};
var Switch = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, onCheckedChange = _a.onCheckedChange, onChange = _a.onChange, props = __rest(_a, ["className", "onCheckedChange", "onChange"]);
return (jsxRuntime.jsxs("label", { className: "relative inline-flex items-center cursor-pointer ".concat(className), children: [jsxRuntime.jsx("input", __assign({ type: "checkbox", className: "sr-only", onChange: function (e) {
onChange === null || onChange === void 0 ? void 0 : onChange(e);
onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(e.target.checked);
} }, props)), jsxRuntime.jsx("div", { className: "w-11 h-6 bg-input peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-ring/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-background after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:border-input after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary" })] }));
};
var RadioGroup = function (_a) {
var value = _a.value, onValueChange = _a.onValueChange, children = _a.children, _b = _a.className, className = _b === void 0 ? '' : _b;
return (jsxRuntime.jsx("div", { className: "space-y-2 ".concat(className), children: React.Children.map(children, function (child) {
if (React.isValidElement(child)) {
return React.cloneElement(child, __assign(__assign({}, child.props), { checked: child.props.value === value, onChange: function () { return onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(child.props.value); } }));
}
return child;
}) }));
};
var RadioGroupItem = function (_a) {
var _b = _a.className, className = _b === void 0 ? '' : _b, props = __rest(_a, ["className"]);
return (jsxRuntime.jsx("input", __assign({ type: "radio", className: "h-4 w-4 text-primary focus:ring-ring border-input ".concat(className) }, props)));
};
var DefaultComponents = /*#__PURE__*/Object.freeze({
__proto__: null,
Button: Button,
Checkbox: Checkbox,
Input: Input,
Label: Label,
RadioGroup: RadioGroup,
RadioGroupItem: RadioGroupItem,
Select: Select,
Switch: Switch,
TableInput: TableInput,
Textarea: Textarea
});
var validateField = function (fieldId, value, field, isRequired) {
// Required check
if (isRequired) {
if (value === undefined || value === null || value === '')
return { message: 'This field is required' };
if (Array.isArray(value) && value.length === 0)
return { message: 'This field is required' };
}
// Skip further validation if empty and not required
if (value === undefined || value === null || value === '')
return null;
if (field.type === 'string' || field.type === 'array') {
var strVal = typeof value === 'string' ? value : '';
if (field.minLength !== undefined && strVal.length < field.minLength) {
return { message: "Must be at least ".concat(field.minLength, " characters") };
}
if (field.maxLength !== undefined && strVal.length > field.maxLength) {
return { message: "Must be at most ".concat(field.maxLength, " characters") };
}
if (field.pattern) {
try {
if (!new RegExp(field.pattern).test(strVal)) {
return { message: 'Invalid format' };
}
}
catch ( /* invalid regex, skip */_a) { /* invalid regex, skip */ }
}
}
if (field.type === 'number' || field.type === 'integer') {
var numVal = Number(value);
if (isNaN(numVal))
return { message: 'Must be a valid number' };
if (field.minimum !== undefined && numVal < field.minimum) {
return { message: "Must be at least ".concat(field.minimum) };
}
if (field.maximum !== undefined && numVal > field.maximum) {
return { message: "Must be at most ".concat(field.maximum) };
}
}
if (field.type === 'table' && Array.isArray(value)) {
if (field.minRows !== undefined && value.length < field.minRows) {
return { message: "At least ".concat(field.minRows, " row(s) required") };
}
if (field.maxRows !== undefined && value.length > field.maxRows) {
return { message: "At most ".concat(field.maxRows, " row(s) allowed") };
}
}
return null;
};
// =============================================================================
// Component
// =============================================================================
var SchemaFormRenderer = function (_a) {
var config = _a.config, onSubmit = _a.onSubmit, _b = _a.isSubmitting, isSubmitting = _b === void 0 ? false : _b, _c = _a.className, className = _c === void 0 ? '' : _c, _d = _a.initialData, initialData = _d === void 0 ? {} : _d, _e = _a.components, components = _e === void 0 ? {} : _e;
var _f = React.useState({}), formData = _f[0], setFormData = _f[1];
var _g = React.useState(new Set()), touchedFields = _g[0], setTouchedFields = _g[1];
var _h = React.useState({}), errors = _h[0], setErrors = _h[1];
var _j = React.useState(false), submitted = _j[0], setSubmitted = _j[1];
var _k = React.useState(false), isInitialized = _k[0], setIsInitialized = _k[1];
var schemaPropertiesRef = React.useRef('');
var _l = components.Button, Button$1 = _l === void 0 ? Button : _l, _m = components.Input, Input$1 = _m === void 0 ? Input : _m, _o = components.Textarea, Textarea$1 = _o === void 0 ? Textarea : _o, _p = components.Label, Label$1 = _p === void 0 ? Label : _p, _q = components.Select, Select$1 = _q === void 0 ? Select : _q, _r = components.Checkbox, Checkbox$1 = _r === void 0 ? Checkbox : _r, _s = components.RadioGroup, RadioGroup$1 = _s === void 0 ? RadioGroup : _s, _t = components.Switch, Switch$1 = _t === void 0 ? Switch : _t, _u = components.TableInput, TableInput$1 = _u === void 0 ? TableInput : _u;
var schemaPropertiesKey = React.useMemo(function () {
return JSON.stringify(config.schema.properties);
}, [config.schema.properties]);
var initialDataKey = React.useMemo(function () {
return JSON.stringify(initialData);
}, [initialData]);
React.useEffect(function () {
if (schemaPropertiesRef.current === schemaPropertiesKey + initialDataKey && isInitialized) {
return;
}
var combinedData = {};
Object.entries(config.schema.properties).forEach(function (_a) {
var fieldId = _a[0], field = _a[1];
if (field.default !== undefined && (!isInitialized || !touchedFields.has(fieldId))) {
combinedData[fieldId] = field.default;
}
});
Object.entries(initialData).forEach(function (_a) {
var fieldId = _a[0], value = _a[1];
if (config.schema.properties[fieldId] && value !== undefined && (!isInitialized || !touchedFields.has(fieldId))) {
combinedData[fieldId] = value;
}
});
if (!isInitialized || Object.keys(combinedData).length > 0) {
setFormData(function (prev) { return (__assign(__assign({}, prev), combinedData)); });
schemaPropertiesRef.current = schemaPropertiesKey + initialDataKey;
setIsInitialized(true);
}
}, [schemaPropertiesKey, initialDataKey, config.schema.properties, initialData, touchedFields, isInitialized]);
var validateAll = React.useCallback(function () {
var newErrors = {};
var orderedFieldIds = config.fieldOrder || Object.keys(config.schema.properties);
orderedFieldIds.forEach(function (fieldId) {
var _a;
var field = config.schema.properties[fieldId];
if (!field)
return;
if (!evaluateFieldConditions(field.conditions, field.showWhen, formData))
return;
var isRequired = ((_a = config.schema.required) === null || _a === void 0 ? void 0 : _a.includes(fieldId)) || false;
var error = validateField(fieldId, formData[fieldId], field, isRequired);
if (error)
newErrors[fieldId] = error;
});
return newErrors;
}, [config, formData]);
var handleSubmit = function (e) {
e.preventDefault();
setSubmitted(true);
var validationErrors = validateAll();
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0)
return;
var visibleData = {};
Object.entries(config.schema.properties).forEach(function (_a) {
var fieldId = _a[0], field = _a[1];
if (evaluateFieldConditions(field.conditions, field.showWhen, formData)) {
visibleData[fieldId] = formData[fieldId];
}
});
onSubmit(visibleData);
};
var updateFormData = React.useCallback(function (fieldId, value) {
setFormData(function (prev) {
var _a;
return (__assign(__assign({}, prev), (_a = {}, _a[fieldId] = value, _a)));
});
setTouchedFields(function (prev) { return new Set(prev).add(fieldId); });
// Clear error on change if submitted
if (submitted) {
setErrors(function (prev) {
var next = __assign({}, prev);
delete next[fieldId];
return next;
});
}
}, [submitted]);
var renderField = React.useCallback(function (fieldId, field) {
var _a, _b;
if (!evaluateFieldConditions(field.conditions, field.showWhen, formData)) {
return null;
}
var value = (_a = formData[fieldId]) !== null && _a !== void 0 ? _a : '';
var uiConfig = config.uiSchema[fieldId] || {};
var isRequired = ((_b = config.schema.required) === null || _b === void 0 ? void 0 : _b.includes(fieldId)) || false;
var isDisabled = uiConfig['ui:disabled'] === true;
var fieldError = errors[fieldId];
var renderInput = function () {
var _a, _b, _c, _d;
switch (field.type) {
case 'table':
var tableValue = Array.isArray(value) ? value : [];
return (jsxRuntime.jsx(TableInput$1, { columns: field.columns || [], value: tableValue, onChange: function (val) { return updateFormData(fieldId, val); }, minRows: field.minRows, maxRows: field.maxRows }));
case 'boolean':
if (field.format === 'switch') {
return (jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(Switch$1, { id: fieldId, checked: value === true, onCheckedChange: function (checked) { return updateFormData(fieldId, checked); }, disabled: isDisabled }), jsxRuntime.jsx(Label$1, { htmlFor: fieldId, children: field.title })] }));
}
else {
return (jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(Checkbox$1, { id: fieldId, checked: value === true, onCheckedChange: function (checked) { return updateFormData(fieldId, checked); }, disabled: isDisabled }), jsxRuntime.jsx(Label$1, { htmlFor: fieldId, children: field.title })] }));
}
case 'string':
switch (field.format) {
case 'textarea':
return (jsxRuntime.jsx(Textarea$1, { value: value, onChange: function (e) { return updateFormData(fieldId, e.target.value); }, placeholder: uiConfig['ui:placeholder'], rows: ((_a = uiConfig['ui:options']) === null || _a === void 0 ? void 0 : _a.rows) || 4, disabled: isDisabled }));
case 'select':
return (jsxRuntime.jsx(Select$1, { value: value, onValueChange: function (val) { return updateFormData(fieldId, val); }, placeholder: uiConfig['ui:placeholder'] || "Select ".concat((_b = field.title) === null || _b === void 0 ? void 0 : _b.toLowerCase()), disabled: isDisabled, children: (field.enum || []).map(function (option, index) {
var _a;
return (jsxRuntime.jsx("option", { value: String(option), children: ((_a = field.enumNames) === null || _a === void 0 ? void 0 : _a[index]) || String(option) }, index));
}) }));
case 'combobox':
// Combobox rendered as a searchable select using default components
return (jsxRuntime.jsx(Select$1, { value: value, onValueChange: function (val) { return updateFormData(fieldId, val); }, placeholder: uiConfig['ui:placeholder'] || "Search ".concat((_c = field.title) === null || _c === void 0 ? void 0 : _c.toLowerCase()), disabled: isDisabled, children: (field.enum || []).map(function (option, index) {
var _a;
return (jsxRuntime.jsx("option", { value: String(option), children: ((_a = field.enumNames) === null || _a === void 0 ? void 0 : _a[index]) || String(option) }, index));
}) }));
case 'radio':
return (jsxRuntime.jsx(RadioGroup$1, { value: value, onValueChange: function (val) { return updateFormData(fieldId, val); }, children: (field.enum || []).map(function (option, index) {
var _a;
return (jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(RadioGroupItem, { value: String(option), id: "".concat(fieldId, "-").concat(index), disabled: isDisabled }), jsxRuntime.jsx(Label$1, { htmlFor: "".concat(fieldId, "-").concat(index), children: ((_a = field.enumNames) === null || _a === void 0 ? void 0 : _a[index]) || String(option) })] }, index));
}) }));
case 'checkboxes':
case 'multiselect':
var selectedValues_1 = Array.isArray(value) ? value : [];
return (jsxRuntime.jsx("div", { className: "space-y-2", children: (field.enum || []).map(function (option, index) {
var _a;
return (jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(Checkbox$1, { id: "".concat(fieldId, "-").concat(index), checked: selectedValues_1.includes(String(option)), onCheckedChange: function (checked) {
var newValues = checked
? __spreadArray(__spreadArray([], selectedValues_1, true), [String(option)], false) : selectedValues_1.filter(function (v) { return v !== String(option); });
updateFormData(fieldId, newValues);
}, disabled: isDisabled }), jsxRuntime.jsx(Label$1, { htmlFor: "".concat(fieldId, "-").concat(index), children: ((_a = field.enumNames) === null || _a === void 0 ? void 0 : _a[index]) || String(option) })] }, index));
}) }));
case 'file':
return (jsxRuntime.jsx(Input$1, { type: "file", onChange: function (e) {
var _a;
var file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
updateFormData(fieldId, file);
}, accept: (_d = uiConfig['ui:options']) === null || _d === void 0 ? void 0 : _d.accept, disabled: isDisabled }));
default:
return (jsxRuntime.jsx(Input$1, { type: field.format === 'email' ? 'email' : field.format === 'date' ? 'date' : field.format === 'date-time' ? 'datetime-local' : 'text', value: value, onChange: function (e) { return updateFormData(fieldId, e.target.value); }, placeholder: uiConfig['ui:placeholder'], disabled: isDisabled }));
}
case 'number':
case 'integer':
return (jsxRuntime.jsx(Input$1, { type: "number", value: value, onChange: function (e) { return updateFormData(fieldId, field.type === 'integer' ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0); }, placeholder: uiConfig['ui:placeholder'], min: field.minimum, max: field.maximum, step: field.type === 'integer' ? 1 : 'any', disabled: isDisabled }));
default:
return (jsxRuntime.jsx(Input$1, { value: value, onChange: function (e) { return updateFormData(fieldId, e.target.value); }, placeholder: uiConfig['ui:placeholder'], disabled: isDisabled }));
}
};
return (jsxRuntime.jsxs("div", { className: "space-y-2", children: [field.type !== 'boolean' && (jsxRuntime.jsxs(Label$1, { htmlFor: fieldId, children: [field.title || fieldId, isRequired && jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), renderInput(), fieldError && (jsxRuntime.jsx("p", { className: "text-sm text-red-500", children: fieldError.message })), field.description && (jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: field.description })), uiConfig['ui:help'] && (jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: uiConfig['ui:help'] }))] }, fieldId));
}, [formData, config.uiSchema, config.schema.required, updateFormData, errors]);
var orderedFieldIds = config.fieldOrder || Object.keys(config.schema.properties);
var filteredFieldIds = orderedFieldIds.filter(function (fieldId) { return config.schema.properties[fieldId]; });
return (jsxRuntime.jsxs("div", { className: "space-y-6 ".concat(className), children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("h2", { className: "text-2xl font-bold", children: config.title }), config.description && (jsxRuntime.jsx("p", { className: "text-gray-600 mt-2", children: config.description }))] }), jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", noValidate: true, children: [filteredFieldIds.map(function (fieldId) {
return renderField(fieldId, config.schema.properties[fieldId]);
}), jsxRuntime.jsx(Button$1, { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : config.settings.submitButtonText })] })] }));
};
var DEFAULT_FORM_CONFIG = {
title: '',
description: '',
schema: {
type: 'object',
properties: {},
},
uiSchema: {},
settings: {
allowMultipleSubmissions: false,
requireAuthentication: false,
showProgressBar: true,
submitButtonText: 'Submit',
successMessage: 'Thank you for your submission!',
},
styling: {
theme: 'default',
primaryColor: '#3b82f6',
backgroundColor: '#ffffff',
},
storage: {},
fieldOrder: [],
};
/**
* Core form editor state management hook.
*
* Provides all field CRUD operations, validation, reordering, and ID management.
* This hook has zero UI dependencies — consuming apps handle their own toast/notification layer.
*
* @example
* ```tsx
* const editor = useFormEditorState({ isCreating: true });
*
* // Add a field
* editor.addField();
*
* // Validate before save
* const result = editor.validate();
* if (!result.valid) {
* showToast(result.errors[0]);
* }
* ```
*/
var useFormEditorState = function (_a) {
var _b, _c;
var existingForm = _a.existingForm, isCreating = _a.isCreating;
var _d = React.useState(DEFAULT_FORM_CONFIG), formConfig = _d[0], setFormConfig = _d[1];
var _e = React.useState([]), fieldOrder = _e[0], setFieldOrder = _e[1];
React.useEffect(function () {
var _a;
if (existingForm && !isCreating) {
setFormConfig(existingForm);
var savedFieldOrder = existingForm.fieldOrder || Object.keys(((_a = existingForm.schema) === null || _a === void 0 ? void 0 : _a.properties) || {});
setFieldOrder(savedFieldOrder);
}
}, [existingForm, isCreating]);
var validate = React.useCallback(function () {
var _a;
var errors = [];
if (!formConfig.title.trim()) {
errors.push('Form title is required');
}
var fieldIds = Object.keys(((_a = formConfig.schema) === null || _a === void 0 ? void 0 : _a.properties) || {});
if (fieldIds.length === 0) {
errors.push('At least one field is required');
}
return { valid: errors.length === 0, errors: errors };
}, [formConfig]);
var generateUniqueId = React.useCallback(function (baseLabel) {
var _a;
if (baseLabel === void 0) { baseLabel = ''; }
var baseCamelCase = baseLabel
? baseLabel.replace(/[^a-zA-Z0-9]/g, '').replace(/^[A-Z]/, function (c) { return c.toLowerCase(); })
: 'field';
var existingIds = Object.keys(((_a = formConfig.schema) === null || _a === void 0 ? void 0 : _a.properties) || {});
var uniqueId = baseCamelCase;
var counter = 1;
while (existingIds.includes(uniqueId)) {
uniqueId = "".concat(baseCamelCase).concat(counter);
counter++;
}
return uniqueId;
}, [(_b = formConfig.schema) === null || _b === void 0 ? void 0 : _b.properties]);
var addField = React.useCallback(function () {
var fieldId = generateUniqueId();
var newField = {
type: 'string',
title: '',
};
setFormConfig(function (prev) {
var _a, _b, _c;
return (__assign(__assign({}, prev), { schema: __assign(__assign({}, prev.schema), { properties: __assign(__assign({}, prev.schema.properties), (_a = {}, _a[fieldId] = newField, _a)) }), uiSchema: __assign(__assign({}, prev.uiSchema), (_b = {}, _b[fieldId] = { 'ui:widget': 'text' }, _b)), storage: __assign(__assign({}, prev.storage), (_c = {}, _c[fieldId] = 'public', _c)), fieldOrder: __spreadArray(__spreadArray([], (prev.fieldOrder || []), true), [fieldId], false) }));
});
setFieldOrder(function (prev) { return __spreadArray(__spreadArray([], prev, true), [fieldId], false); });
}, [generateUniqueId]);
var updateField = React.useCallback(function (fieldId, updates) {
setFormConfig(function (prev) {
var _a;
return (__assign(__assign({}, prev), { schema: __assign(__assign({}, prev.schema), { properties: __assign(__assign({}, prev.schema.properties), (_a = {}, _a[fieldId] = __assign(__assign({}, prev.schema.properties[fieldId]), updates), _a)) }) }));
});
}, []);
var updateFieldUI = React.useCallback(function (fieldId, uiUpdates) {
setFormConfig(function (prev) {
var _a;
return (__assign(__assign({}, prev), { uiSchema: __assign(__assign({}, prev.uiSchema), (_a = {}, _a[fieldId] = __assign(__assign({}, prev.uiSchema[fieldId]), uiUpdates), _a)) }));
});
}, []);
var updateFieldStorage = React.useCallback(function (fieldId, storage) {
setFormConfig(function (prev) {
var _a;
return (__assign(__assign({}, prev), { storage: __assign(__assign({}, prev.storage), (_a = {}, _a[fieldId] = storage, _a)) }));
});
}, []);
var updateFieldId = React.useCallback(function (oldId, newId) {
var _a;
if (oldId === newId)
return { valid: true, errors: [] };
if (!newId || !/^[a-z][a-zA-Z0-9]*$/.test(newId)) {
return {
valid: false,
errors: ['Field ID must be in camelCase format (e.g., firstName, emailAddress)'],
};
}
var existingIds = Object.keys(((_a = formConfig.schema) === null || _a === void 0 ? void 0 : _a.properties) || {});
if (existingIds.includes(newId) && newId !== oldId) {
return { valid: false, errors: ['A field with this ID already exists'] };
}
setFormConfig(function (prev) {
var _a, _b;
var newProperties = __assign({}, prev.schema.properties);
var fieldData = newProperties[oldId];
delete newProperties[oldId];
newProperties[newId] = fieldData;
var newUISchema = __assign({}, prev.uiSchema);
var uiData = newUISchema[oldId];
delete newUISchema[oldId];
if (uiData)
newUISchema[newId] = uiData;
var newStorage = __assign({}, prev.storage);
var storageData = newStorage[oldId];
delete newStorage[oldId];
newStorage[newId] = storageData || 'public';
var newRequired = (_b = (_a = prev.schema) === null || _a === void 0 ? void 0 : _a.required) === null || _b === void 0 ? void 0 : _b.map(function (id) { return (id === oldId ? newId : id); });
var newFieldOrder = (prev.fieldOrder || []).map(function (id) { return (id === oldId ? newId : id); });
return __assign(__assign({}, prev), { schema: __assign(__assign({}, prev.schema), { properties: newProperties, required: (newRequired === null || newRequired === void 0 ? void 0 : newRequired.length) ? newRequired : undefined }), uiSchema: newUISchema, storage: newStorage, fieldOrder: newFieldOrder });
});
setFieldOrder(function (prev) { return prev.map(function (id) { return (id === oldId ? newId : id); }); });
return { valid: true, errors: [] };
}, [(_c = formConfig.schema) === null || _c === void 0 ? void 0 : _c.properties]);
var removeField = React.useCallback(function (fieldId) {
setFormConfig(function (prev) {
var _a, _b;
var newProperties = __assign({}, prev.schema.properties);
delete newProperties[fieldId];
var newUISchema = __assign({}, prev.uiSchema);
delete newUISchema[fieldId];
var newStorage = __assign({}, prev.storage);
delete newStorage[fieldId];
var newRequired = (_b = (_a = prev.schema) === null || _a === void 0 ? void 0 : _a.required) === null || _b === void 0 ? void 0 : _b.filter(function (id) { return id !== fieldId; });
var newFieldOrder = (prev.fieldOrder || []).filter(function (id) { return id !== fieldId; });
return __assign(__assign({}, prev), { schema: __assign(__assign({}, prev.schema), { properties: newProperties, required: newRequired }), uiSchema: newUISchema, storage: newStorage, fieldOrder: newFieldOrder });
});
setFieldOrder(function (prev) { return prev.filter(function (id) { return id !== fieldId; }); });
}, []);
var toggleRequired = React.useCallback(function (fieldId, required) {
setFormConfig(function (prev) {
var _a;
var currentRequired = ((_a = prev.schema) === null || _a === void 0 ? void 0 : _a.required) || [];
var newRequired = required
? __spreadArray(__spreadArray([], currentRequired.filter(function (id) { return id !== fieldId; }), true), [fieldId], false) : currentRequired.filter(function (id) { return id !== fieldId; });
return __assign(__assign({}, prev), { schema: __assign(__assign({}, prev.schema), { required: newRequired.length > 0 ? newRequired : undefined }) });
});
}, []);
var reorderFields = React.useCallback(function (newFieldOrder) {
setFieldOrder(newFieldOrder);
setFormConfig(function (prev) { return (__assign(__assign({}, prev), { fieldOrder: newFieldOrder })); });
}, []);
return {
formConfig: formConfig,
setFormConfig: setFormConfig,
fieldOrder: fieldOrder,
validate: validate,
generateUniqueId: generateUniqueId,
addField: addField,
updateField: updateField,
updateFieldUI: updateFieldUI,
updateFieldStorage: updateFieldStorage,
updateFieldId: updateFieldId,
removeField: removeField,
toggleRequired: toggleRequired,
reorderFields: reorderFields,
};
};
/**
* Form editor action utilities for import/export.
*
* Returns pure results — consuming apps handle their own toast/notification layer.
*
* @example
* ```tsx
* const actions = useFormEditorActions({ formConfig, setFormConfig });
*
* // Export schema as JSON file download
* actions.exportSchema();
*
* // Import: wire to a file input's onChange
* <input type="file" onChange={(e) => {
* const result = actions.importSchema(e);
* if (result.success) showToast('Imported!');
* else showToast(result.error, 'error');
* }} />
* ```
*/
var useFormEditorActions = function (_a) {
var formConfig = _a.formConfig, setFormConfig = _a.setFormConfig;
var exportSchema = React.useCallback(function () {
var schemaExport = {
schema: formConfig.schema,
uiSchema: formConfig.uiSchema,
title: formConfig.title,
description: formConfig.description,
};
var blob = new Blob([JSON.stringify(schemaExport, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = "".concat(formConfig.title.replace(/\s+/g, '-').toLowerCase() || 'form', "-schema.json");
a.click();
URL.revokeObjectURL(url);
}, [formConfig]);
var importSchema = React.useCallback(function (event) {
var _a;
var file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
if (!file)
return { success: false, error: 'No file selected' };
// Note: FileReader is async, so we kick it off and let it resolve.
// The consuming app should treat this as fire-and-forget with a callback pattern
// or use the returned promise-like pattern.
var reader = new FileReader();
reader.onload = function (e) {
var _a;
try {
var imported_1 = JSON.parse((_a = e.target) === null || _a === void 0 ? void 0 : _a.result);
if (imported_1.schema && imported_1.schema.type === 'object' && imported_1.schema.properties) {
setFormConfig(function (prev) { return (__assign(__assign({}, prev), { schema: imported_1.schema, uiSchema: imported_1.uiSchema || {}, title: imported_1.title || prev.title, description: imported_1.description || prev.description })); });
}
else {
console.error('Invalid schema format during import');
}
}
catch (error) {
console.error('Failed to parse imported schema:', error);
}
};
reader.readAsText(file);
// Reset input
event.target.value = '';
return { success: true };
}, [setFormConfig]);
return { exportSchema: exportSchema, importSchema: importSchema };
};
exports.DefaultComponents = DefaultComponents;
exports.SchemaFormRenderer = SchemaFormRenderer;
exports.evaluateFieldConditions = evaluateFieldConditions;
exports.useFormEditorActions = useFormEditorActions;
exports.useFormEditorState = useFormEditorState;
//# sourceMappingURL=index.js.map