@proveanything/smartlinks-form-renderer
Version:
SmartLinks form renderer and editor — render forms from JSON schema or build them with the visual editor
314 lines (302 loc) • 14.6 kB
JavaScript
;
var react = require('react');
/******************************************************************************
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 __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 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.useFormEditorActions = useFormEditorActions;
exports.useFormEditorState = useFormEditorState;
//# sourceMappingURL=index.js.map