mui-smart-form-builder
Version:
A reusable React component for dynamically rendering MUI forms from JSON configuration with Formik integration
294 lines (288 loc) • 15.8 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var material = require('@mui/material');
var React = require('react');
var xDatePickers = require('@mui/x-date-pickers');
var dayjs = require('dayjs');
/**
* Normalizes options to ensure consistent FieldOption format
*/
const normalizeOptions = (options) => {
if (!options)
return [];
return options.map(option => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
};
/**
* Gets the display value for form fields
*/
const getDisplayValue = (value, type) => {
if (value === null || value === undefined) {
return type === 'multiCheckbox' ? [] : '';
}
switch (type) {
case 'multiCheckbox':
return Array.isArray(value) ? value : [];
case 'checkbox':
return Boolean(value);
case 'switch':
return Boolean(value);
case 'slider':
return Number(value) || 0;
case 'number':
return value === '' ? '' : Number(value);
default:
return value;
}
};
/**
* Validates if a field value is empty for required field validation
*/
const isFieldEmpty = (value, type) => {
if (value === null || value === undefined)
return true;
switch (type) {
case 'multiCheckbox':
return !Array.isArray(value) || value.length === 0;
case 'checkbox':
case 'switch':
return false; // These always have a boolean value
case 'file':
return !value || (value instanceof FileList && value.length === 0);
default:
return String(value).trim() === '';
}
};
/**
* Debounce function for search inputs
*/
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
const FieldRenderer = ({ field, formik, value, error, helperText, onChange }) => {
const { name = '', label, type, placeholder, options = [], required = false, variant = 'outlined', muiProps = {}, onSearch, min, max, step, marks = false, freeSolo = false, multiple = false, accept, render } = field;
const displayValue = getDisplayValue(value, type);
const normalizedOptions = normalizeOptions(options);
if (render) {
return render({
fieldProps: {
name,
label,
value: displayValue,
error: Boolean(error),
helperText: error || helperText,
onChange: (newValue) => onChange(newValue),
...muiProps
},
formik
});
}
const baseProps = {
label,
error: Boolean(error),
helperText: error || helperText,
variant,
...muiProps
};
switch (type) {
case 'text':
case 'password':
return (jsxRuntime.jsx(material.TextField, { ...baseProps, type: type, value: displayValue, placeholder: placeholder, onChange: (e) => onChange(e.target.value), fullWidth: true }));
case 'textarea':
return (jsxRuntime.jsx(material.TextField, { ...baseProps, multiline: true, rows: 4, value: displayValue, placeholder: placeholder, onChange: (e) => onChange(e.target.value), fullWidth: true }));
case 'number':
return (jsxRuntime.jsx(material.TextField, { ...baseProps, type: "text", value: displayValue, placeholder: placeholder, onChange: (e) => {
const value = e.target.value;
if (value === '' || /^-?\d*\.?\d*$/.test(value)) {
onChange(value === '' ? '' : Number(value));
}
}, fullWidth: true }));
case 'checkbox':
return (jsxRuntime.jsx(material.FormControlLabel, { control: jsxRuntime.jsx(material.Checkbox, { checked: displayValue, onChange: (e) => onChange(e.target.checked), ...muiProps }), label: label }));
case 'multiCheckbox':
return (jsxRuntime.jsxs(material.FormControl, { component: "fieldset", error: Boolean(error), children: [jsxRuntime.jsx(material.FormLabel, { component: "legend", children: label }), jsxRuntime.jsx(material.FormGroup, { children: normalizedOptions.map((option) => (jsxRuntime.jsx(material.FormControlLabel, { control: jsxRuntime.jsx(material.Checkbox, { checked: displayValue.includes(option.value), onChange: (e) => {
const newValue = e.target.checked
? [...displayValue, option.value]
: displayValue.filter((v) => v !== option.value);
onChange(newValue);
}, ...muiProps }), label: option.label }, option.value))) }), (error || helperText) && jsxRuntime.jsx(material.FormHelperText, { children: error || helperText })] }));
case 'radio':
return (jsxRuntime.jsxs(material.FormControl, { component: "fieldset", error: Boolean(error), children: [jsxRuntime.jsx(material.FormLabel, { component: "legend", children: label }), jsxRuntime.jsx(material.RadioGroup, { value: displayValue, onChange: (e) => onChange(e.target.value), children: normalizedOptions.map((option) => (jsxRuntime.jsx(material.FormControlLabel, { value: option.value, control: jsxRuntime.jsx(material.Radio, { ...muiProps }), label: option.label }, option.value))) }), (error || helperText) && jsxRuntime.jsx(material.FormHelperText, { children: error || helperText })] }));
case 'select':
return (jsxRuntime.jsxs(material.FormControl, { fullWidth: true, variant: variant, error: Boolean(error), children: [jsxRuntime.jsx(material.InputLabel, { children: label }), jsxRuntime.jsx(material.Select, { value: displayValue, label: label, onChange: (e) => onChange(e.target.value), multiple: multiple, ...muiProps, children: normalizedOptions.map((option) => (jsxRuntime.jsx(material.MenuItem, { value: option.value, children: option.label }, option.value))) }), (error || helperText) && jsxRuntime.jsx(material.FormHelperText, { children: error || helperText })] }));
case 'autocomplete':
return (jsxRuntime.jsx(AutocompleteField, { field: field, value: displayValue, onChange: onChange, error: Boolean(error), helperText: error || helperText, options: normalizedOptions, onSearch: onSearch, freeSolo: freeSolo, multiple: multiple, muiProps: muiProps }));
case 'date':
case 'time':
case 'dateTime':
const parseValue = (val) => {
if (!val)
return null;
if (dayjs.isDayjs(val))
return val;
return dayjs(val);
};
const handleDateChange = (newValue) => {
if (!newValue) {
onChange('');
return;
}
if (type === 'time') {
onChange(newValue.format('HH:mm'));
}
else if (type === 'date') {
onChange(newValue.format('YYYY-MM-DD'));
}
else {
onChange(newValue.format('YYYY-MM-DDTHH:mm'));
}
};
const renderDateTimePicker = () => {
const commonProps = {
value: parseValue(displayValue),
onChange: handleDateChange,
slotProps: {
textField: {
...baseProps,
fullWidth: true,
error: Boolean(error),
helperText: error || helperText,
}
}
};
switch (type) {
case 'date':
return jsxRuntime.jsx(xDatePickers.DatePicker, { ...commonProps });
case 'time':
return jsxRuntime.jsx(xDatePickers.TimePicker, { ...commonProps });
case 'dateTime':
return jsxRuntime.jsx(xDatePickers.DateTimePicker, { ...commonProps });
default:
return jsxRuntime.jsx(xDatePickers.DatePicker, { ...commonProps });
}
};
return renderDateTimePicker();
case 'switch':
return (jsxRuntime.jsx(material.FormControlLabel, { control: jsxRuntime.jsx(material.Switch, { checked: displayValue, onChange: (e) => onChange(e.target.checked), ...muiProps }), label: label }));
case 'slider':
return (jsxRuntime.jsxs(material.Box, { sx: { px: 2 }, children: [jsxRuntime.jsx(material.Typography, { gutterBottom: true, children: label }), jsxRuntime.jsx(material.Slider, { value: displayValue, onChange: (_, newValue) => onChange(newValue), min: min, max: max, step: step, marks: marks, valueLabelDisplay: "auto", ...muiProps }), (error || helperText) && (jsxRuntime.jsx(material.FormHelperText, { error: Boolean(error), children: error || helperText }))] }));
case 'file':
return (jsxRuntime.jsx(material.TextField, { ...baseProps, type: "file", onChange: (e) => {
const files = e.target.files;
onChange(multiple ? files : files?.[0] || null);
}, InputLabelProps: { shrink: true }, inputProps: { accept, multiple }, fullWidth: true }));
case 'empty':
return jsxRuntime.jsx(material.Box, {});
default:
return (jsxRuntime.jsx(material.TextField, { ...baseProps, value: displayValue, placeholder: placeholder, onChange: (e) => onChange(e.target.value), fullWidth: true }));
}
};
const AutocompleteField = ({ field, value, onChange, error, helperText, options, onSearch, freeSolo, multiple, muiProps }) => {
const [searchOptions, setSearchOptions] = React.useState(options);
const [loading, setLoading] = React.useState(false);
const debouncedSearch = React.useMemo(() => debounce(async (searchTerm) => {
if (onSearch && searchTerm.length > 0) {
setLoading(true);
try {
const results = await onSearch(searchTerm);
setSearchOptions(results);
}
catch (error) {
console.error('Search error:', error);
}
finally {
setLoading(false);
}
}
else {
setSearchOptions(options);
}
}, 300), [onSearch, options]);
return (jsxRuntime.jsx(material.Autocomplete, { value: value, onChange: (_, newValue) => onChange(newValue), onInputChange: (_, inputValue) => {
if (onSearch) {
debouncedSearch(inputValue);
}
}, options: searchOptions, getOptionLabel: (option) => {
if (typeof option === 'string')
return option;
return option?.label || '';
}, isOptionEqualToValue: (option, value) => {
if (typeof option === 'string' && typeof value === 'string') {
return option === value;
}
return option?.value === value?.value;
}, loading: loading, freeSolo: freeSolo, multiple: multiple, renderTags: (tagValue, getTagProps) => tagValue.map((option, index) => (React.createElement(material.Chip, { label: typeof option === 'string' ? option : option.label, ...getTagProps({ index }), key: index }))), renderInput: (params) => (jsxRuntime.jsx(material.TextField, { ...params, label: field.label, error: error, helperText: helperText, variant: field.variant, placeholder: field.placeholder, ...muiProps })) }));
};
const SmartFormBuilder = ({ formik, fields, buttons = [], title, className, sx, gridSpacing = 2 }) => {
const handleFieldChange = (fieldName, value, field) => {
formik.setFieldValue(fieldName, value);
formik.setFieldTouched(fieldName, true, false);
if (field.onChange) {
field.onChange(value, formik);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
const touchedFields = {};
fields.forEach((field) => {
if (field.name) {
touchedFields[field.name] = true;
}
});
await formik.setTouched(touchedFields, true);
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
formik.setErrors(errors);
return;
}
formik.handleSubmit();
};
const getFieldError = (fieldName) => {
const isFieldTouched = formik.touched[fieldName];
const hasFormBeenSubmitted = formik.submitCount > 0;
const hasFieldError = formik.errors[fieldName];
const shouldShowError = isFieldTouched || hasFormBeenSubmitted;
return shouldShowError && hasFieldError ? String(formik.errors[fieldName]) : undefined;
};
const getFieldHelperText = (field, fieldError) => {
return fieldError;
};
return (jsxRuntime.jsxs(material.Box, { className: className, sx: sx, children: [title && (jsxRuntime.jsx(material.Typography, { variant: "h4", component: "h1", gutterBottom: true, children: title })), jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [jsxRuntime.jsx(material.Grid, { container: true, spacing: gridSpacing, children: fields.map((field, index) => {
const fieldName = field.name || `field_${index}`;
const fieldValue = field.name ? formik.values[field.name] : undefined;
const fieldError = field.name ? getFieldError(field.name) : undefined;
const helperText = getFieldHelperText(field, fieldError);
const gridProps = {
item: true,
xs: field.grid?.xs || 12
};
if (field.grid?.sm !== undefined)
gridProps.sm = field.grid.sm;
if (field.grid?.md !== undefined)
gridProps.md = field.grid.md;
if (field.grid?.lg !== undefined)
gridProps.lg = field.grid.lg;
if (field.grid?.xl !== undefined)
gridProps.xl = field.grid.xl;
return (jsxRuntime.jsx(material.Grid, { ...gridProps, children: jsxRuntime.jsx(FieldRenderer, { field: field, formik: formik, value: fieldValue, error: fieldError, helperText: helperText, onChange: (value) => {
if (field.name) {
handleFieldChange(field.name, value, field);
}
} }) }, fieldName));
}) }), buttons.length > 0 && (jsxRuntime.jsx(material.Box, { sx: { mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }, children: buttons.map((button, index) => (jsxRuntime.jsx(material.Button, { type: button.type || 'button', variant: button.variant || 'contained', color: button.color || 'primary', onClick: button.onClick ? () => button.onClick(formik) : undefined, ...button.muiProps, children: button.label }, index))) }))] })] }));
};
exports.SmartFormBuilder = SmartFormBuilder;
exports.debounce = debounce;
exports.getDisplayValue = getDisplayValue;
exports.isFieldEmpty = isFieldEmpty;
exports.normalizeOptions = normalizeOptions;
//# sourceMappingURL=index.js.map