UNPKG

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
'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