@goutham1494/form-engine
Version:
A schema-driven dynamic form builder for React
1,436 lines (1,424 loc) • 62.6 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
DynamicForm: () => DynamicForm_default,
FIELD_TYPES: () => FIELD_TYPES,
FieldRenderer: () => FormEngine_default,
FormWizard: () => FormWizard_default
});
module.exports = __toCommonJS(index_exports);
// src/DynamicForm/DynamicForm.tsx
var import_react9 = require("react");
var import_react_hook_form9 = require("react-hook-form");
// src/FormEngine.tsx
var import_react8 = __toESM(require("react"), 1);
var import_react_hook_form8 = require("react-hook-form");
// src/DynamicForm/FieldTypes/CheckboxField.tsx
var import_react = __toESM(require("react"), 1);
var import_react_hook_form = require("react-hook-form");
var import_jsx_runtime = require("react/jsx-runtime");
var CheckboxFieldComponent = ({
field,
name,
error
}) => {
const { control, setValue, getValues, trigger } = (0, import_react_hook_form.useFormContext)();
const debounceTimer = (0, import_react.useRef)(null);
const [loading, setLoading] = (0, import_react.useState)(false);
const isDarkMode = field.theme === "dark";
import_react.default.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const {
field: controllerField,
fieldState: { error: controllerError }
} = (0, import_react_hook_form.useController)({
name,
control,
defaultValue: field.defaultValue ?? (field.options ? [] : false),
rules: {
required: field.required,
validate: field.validation?.custom
}
});
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
default:
return "Invalid selection";
}
};
const handleDebounced = (value) => {
if (field.onValueChangeDebounced) {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setLoading(true);
debounceTimer.current = setTimeout(async () => {
await field.onValueChangeDebounced?.(value, {
setValue,
getValues,
trigger
});
setLoading(false);
}, field.debounceMs ?? 500);
}
};
const handleChange = (e, value) => {
const isGroup2 = !!field.options?.length;
const newValue = isGroup2 ? e.target.checked ? [...controllerField.value || [], value] : controllerField.value.filter((v) => v !== value) : e.target.checked;
controllerField.onChange(newValue);
field.onChange?.(e);
field.onValueChange?.(newValue, { setValue, getValues, trigger });
handleDebounced(newValue);
if ((error || controllerError) && field.showErrorOnBlur) {
trigger(name);
}
};
const inputStyle = {
width: "16px",
height: "16px",
accentColor: "#004DB2",
cursor: field.disabled ? "not-allowed" : "pointer",
...field.inputStyle
};
const labelStyle = field.labelStyle ?? {
fontSize: "14px",
color: isDarkMode ? "#e5e7eb" : "#333"
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: isDarkMode ? "#9ca3af" : "#6b7280"
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const optionWrapperStyle = field.optionWrapperStyle ?? {
display: "flex",
alignItems: "center",
gap: "8px"
};
const checkBoxGroupStyle = field.checkBoxGroupStyle ?? {
display: "flex",
flexDirection: "column",
gap: "0.75rem"
};
const isGroup = !!field.options?.length;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
(field.checkboxLabel ?? field.label) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: field.checkboxLabel ?? field.label
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
className: field.checkboxGroupClass ?? "",
style: isGroup ? checkBoxGroupStyle : optionWrapperStyle,
children: isGroup ? field.options?.map((opt) => {
const isChecked = controllerField.value?.includes(opt.value);
const inputId = `${name}-${opt.value}`;
const helpTextAlignment = opt.helpTextAlignment ?? "underLabel";
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
style: { display: "flex", flexDirection: "column" },
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"label",
{
htmlFor: inputId,
className: field.optionWrapperClass ?? "",
style: {
display: "flex",
alignItems: "center",
gap: "8px",
cursor: field.disabled ? "not-allowed" : "pointer"
},
title: opt.tooltip,
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: inputId,
type: "checkbox",
value: opt.value,
checked: isChecked,
onChange: (e) => handleChange(e, opt.value),
onBlur: () => {
if (field.showErrorOnBlur) trigger(name);
},
className: field.inputClass ?? "",
style: inputStyle,
disabled: field.disabled || opt.disabled,
"aria-describedby": opt.helpText ? `${inputId}-desc` : void 0
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: opt.label })
]
}
),
opt.helpText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
id: `${inputId}-desc`,
role: "note",
style: {
...helpTextStyle,
marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: opt.helpText
}
)
]
},
opt.value
);
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
"label",
{
className: field.optionWrapperClass ?? "",
style: optionWrapperStyle,
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"input",
{
id: name,
type: "checkbox",
className: field.inputClass ?? "",
style: inputStyle,
checked: !!controllerField.value,
onChange: (e) => handleChange(e),
onBlur: () => {
if (field.showErrorOnBlur) trigger(name);
},
disabled: field.disabled,
"aria-describedby": field.helpText ? `${name}-description` : void 0
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: field.label })
]
}
)
}
),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || controllerError) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || controllerError) || (error || controllerError)?.message || getAutoErrorMessage(error || controllerError) })
] });
};
var CheckboxField_default = import_react.default.memo(CheckboxFieldComponent);
// src/DynamicForm/FieldTypes/GroupField.tsx
var import_react2 = __toESM(require("react"), 1);
var import_react_hook_form2 = require("react-hook-form");
var import_jsx_runtime2 = require("react/jsx-runtime");
function getErrorMessage(error, field) {
if (!error) return "";
if (field?.errorText) return field.errorText;
if (typeof error === "string") return error;
if (field?.getErrorMessage && typeof field.getErrorMessage === "function") {
return field.getErrorMessage(error);
}
if (typeof error === "object") {
if ("message" in error && typeof error.message === "string") {
return error.message;
}
const entries = Object.entries(error);
for (const [, val] of entries) {
if (val?.message) return val.message;
}
}
return `${field?.label || "Field"} is invalid`;
}
var GroupFieldComponent = ({
field,
name,
error
}) => {
const { control, trigger } = (0, import_react_hook_form2.useFormContext)();
const {
field: groupField,
formState: { errors }
} = (0, import_react_hook_form2.useController)({
name,
control,
rules: {
required: field.required,
validate: field.validation?.custom
},
defaultValue: field.defaultValue ?? {}
});
const watchedGroupValue = (0, import_react_hook_form2.useWatch)({ name, control });
const groupError = errors?.[name];
const isDarkMode = field.theme === "dark";
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? {
display: "block",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: isDarkMode ? "#e5e7eb" : "#333"
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: isDarkMode ? "#9ca3af" : "#6b7280"
};
(0, import_react2.useEffect)(() => {
const hasChanged = JSON.stringify(watchedGroupValue) !== JSON.stringify(groupField.value);
if (hasChanged) groupField.onChange(watchedGroupValue);
}, [watchedGroupValue, groupField]);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
(field.checkboxLabel ?? field.label) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: field.checkboxLabel ?? field.label
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
id: name,
className: field.layoutClass ?? "",
style: field.layoutStyle,
children: field.children?.map((child) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
FormEngine_default,
{
field: child,
parentName: name
},
child.name
))
}
),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || groupError) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: getErrorMessage(error || groupError, field) })
] });
};
var GroupField_default = import_react2.default.memo(GroupFieldComponent);
// src/DynamicForm/FieldTypes/NumberField.tsx
var import_react3 = __toESM(require("react"), 1);
var import_react_hook_form3 = require("react-hook-form");
var import_jsx_runtime3 = require("react/jsx-runtime");
var NumberFieldComponent = ({
field,
name,
error
}) => {
const { setValue, getValues, trigger, control } = (0, import_react_hook_form3.useFormContext)();
const { field: controllerField } = (0, import_react_hook_form3.useController)({
name,
control,
defaultValue: field.defaultValue ?? "",
rules: {
required: field.required,
pattern: field.validation?.pattern,
minLength: field.validation?.minLength,
maxLength: field.validation?.maxLength,
validate: field.validation?.custom
}
});
const debounceTimer = (0, import_react3.useRef)(null);
const [loading, setLoading] = (0, import_react3.useState)(false);
const isDarkMode = field.theme === "dark";
const isNumberField = field.type === "number";
import_react3.default.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
case "minLength":
return `${field.label} must be at least ${field.validation?.minLength?.value} digits`;
case "maxLength":
return `${field.label} must be at most ${field.validation?.maxLength?.value} digits`;
case "pattern":
return `${field.label} format is invalid`;
default:
return "Invalid value";
}
};
const handleChange = (0, import_react3.useCallback)(
(e) => {
const value = isNumberField ? +e.target.value : e.target.value;
controllerField.onChange(e);
field.onChange?.(e);
if (field.onValueChange) {
field.onValueChange(value, { setValue, getValues, trigger });
}
if (error && field.showErrorOnBlur) {
trigger(name);
}
if (field.onValueChangeDebounced) {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setLoading(true);
debounceTimer.current = setTimeout(async () => {
await field.onValueChangeDebounced?.(value, {
setValue,
getValues,
trigger
});
setLoading(false);
}, field.debounceMs ?? 500);
}
},
[
controllerField,
field,
isNumberField,
setValue,
getValues,
trigger,
error,
name
]
);
const handleBlur = (e) => {
field.onBlur?.(e);
if (field.showErrorOnBlur) {
trigger(name);
}
};
const getDefaultInputStyle = (isDark, hasError) => ({
padding: "10px",
border: "1px solid",
borderColor: hasError ? "#f87171" : isDark ? "#4b5563" : "#ccc",
borderRadius: "6px",
fontSize: "14px",
width: "100%",
backgroundColor: isDark ? "#1f2937" : "#fff",
color: isDark ? "#f9fafb" : "#111827",
outline: "none",
boxSizing: "border-box",
transition: "border-color 0.2s ease-in-out",
opacity: field.disabled ? 0.6 : 1,
cursor: field.disabled ? "not-allowed" : "text"
});
const getDefaultLabelStyle = (isDark) => ({
display: "flex",
alignItems: "center",
gap: "6px",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: isDark ? "#e5e7eb" : "#333"
});
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? getDefaultLabelStyle(isDarkMode);
const inputStyle = {
...getDefaultInputStyle(isDarkMode, Boolean(error)),
...field.inputStyle
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: isDarkMode ? "#9ca3af" : "#6b7280"
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const renderLabel = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [
field.icon && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.icon }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: field.tooltip, children: field.label }),
loading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" })
] });
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
renderLabel(),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"input",
{
id: name,
type: isNumberField ? "number" : "text",
inputMode: field.inputMode ?? (isNumberField ? "numeric" : "text"),
placeholder: field.placeholder,
"aria-invalid": Boolean(error),
"aria-describedby": field.helpText ? `${name}-description` : void 0,
value: controllerField.value ?? "",
onChange: handleChange,
onBlur: handleBlur,
onKeyDown: (e) => {
if (field.allowedPattern) {
const char = e.key;
const isControlKey = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Tab"
].includes(char);
if (!isControlKey && !field.allowedPattern.test(char)) {
e.preventDefault();
}
}
field.onKeyDown?.(e);
},
className: field.inputClass ?? "",
style: inputStyle,
disabled: field.disabled
}
),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: {
...helpTextStyle,
marginLeft: field.helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: field.helpText
}
),
error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var NumberField_default = import_react3.default.memo(NumberFieldComponent);
// src/DynamicForm/FieldTypes/RadioField.tsx
var import_react4 = __toESM(require("react"), 1);
var import_react_hook_form4 = require("react-hook-form");
var import_jsx_runtime4 = require("react/jsx-runtime");
var RadioFieldComponent = ({
field,
name,
register,
error
}) => {
const { control, setValue, getValues, trigger } = (0, import_react_hook_form4.useFormContext)();
const debounceTimer = (0, import_react4.useRef)(null);
const [loading, setLoading] = (0, import_react4.useState)(false);
const isDarkMode = field.theme === "dark";
import_react4.default.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const {
field: controllerField,
fieldState: { error: fieldError }
} = (0, import_react_hook_form4.useController)({
name,
control,
rules: { required: field.required }
});
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
default:
return "Invalid selection";
}
};
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? {
display: "block",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: isDarkMode ? "#e5e7eb" : "#333"
};
const radioGroupStyle = field.radioGroupStyle ?? {
display: "flex",
flexDirection: field.inline ? "row" : "column",
gap: "0.75rem",
alignItems: field.inline ? "center" : "flex-start",
flexWrap: field.inline ? "wrap" : "nowrap"
};
const radioInputStyle = {
width: "16px",
height: "16px",
accentColor: "#004DB2",
cursor: field.disabled ? "not-allowed" : "pointer",
transition: "all 0.2s ease",
...field.inputStyle
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
color: isDarkMode ? "#9ca3af" : "#6b7280",
marginTop: "4px"
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const handleChange = (value, event) => {
controllerField.onChange(value);
field.onChange?.(event);
field.onValueChange?.(value, { setValue, getValues, trigger });
if ((error || fieldError) && field.showErrorOnBlur) {
trigger(name);
}
if (field.onValueChangeDebounced) {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setLoading(true);
debounceTimer.current = setTimeout(async () => {
await field.onValueChangeDebounced?.(value, {
setValue,
getValues,
trigger
});
setLoading(false);
}, field.debounceMs ?? 500);
}
};
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: field.labelClass ?? "", style: labelStyle, children: [
field.label,
loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" })
] }),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: field.radioGroupClass ?? "", style: radioGroupStyle, children: field.options?.map((option) => {
const helpTextAlignment = option.helpTextAlignment ?? "underLabel";
const inputId = `${name}-${option.value}`;
const isSelected = controllerField.value === option.value;
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
"div",
{
style: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start"
},
children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
"label",
{
htmlFor: inputId,
className: field.optionWrapperClass,
style: {
display: "flex",
alignItems: "center",
gap: "8px",
cursor: field.disabled ? "not-allowed" : "pointer"
},
title: option.tooltip,
children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"input",
{
id: inputId,
type: "radio",
value: option.value,
checked: isSelected,
onChange: (e) => handleChange(e.target.value, e),
onBlur: () => {
if (field.showErrorOnBlur) trigger(name);
},
className: field.inputClass,
style: radioInputStyle,
disabled: field.disabled || option.disabled,
"aria-describedby": option.helpText ? `${inputId}-desc` : void 0
}
),
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"span",
{
style: {
fontSize: "14px",
fontWeight: isSelected ? 600 : 400,
color: isSelected ? "#004DB2" : void 0
},
children: option.label
}
)
]
}
),
option.helpText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"div",
{
id: `${inputId}-desc`,
role: "note",
style: {
...helpTextStyle,
marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: option.helpText
}
)
]
},
option.value
);
}) }),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || fieldError) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) })
] });
};
var RadioField_default = import_react4.default.memo(RadioFieldComponent);
// src/DynamicForm/FieldTypes/SelectField.tsx
var import_react5 = __toESM(require("react"), 1);
var import_react_hook_form5 = require("react-hook-form");
var import_jsx_runtime5 = require("react/jsx-runtime");
var SelectFieldComponent = ({
field,
name,
error
}) => {
const { control, setValue, getValues, trigger } = (0, import_react_hook_form5.useFormContext)();
const {
field: controllerField,
fieldState: { error: fieldError }
} = (0, import_react_hook_form5.useController)({
name,
control,
defaultValue: field.defaultValue ?? "",
rules: { required: field.required }
});
const parentValue = (0, import_react_hook_form5.useWatch)({ name: field.dependsOn || "__no_field__" });
const [dynamicOptions, setDynamicOptions] = (0, import_react5.useState)(field.options || []);
const [loading, setLoading] = (0, import_react5.useState)(false);
const [fetchError, setFetchError] = (0, import_react5.useState)(null);
(0, import_react5.useEffect)(() => {
let isMounted = true;
const updateOptions = async () => {
if (field.getOptions) {
setLoading(true);
setFetchError(null);
try {
const result = field.getOptions(parentValue);
const resolved = result instanceof Promise ? await result : result;
if (isMounted) setDynamicOptions(resolved || []);
} catch (err) {
if (isMounted) {
setFetchError("Failed to load options.");
setDynamicOptions([]);
}
} finally {
if (isMounted) setLoading(false);
}
} else {
setDynamicOptions(field.options || []);
}
};
updateOptions();
return () => {
isMounted = false;
};
}, [field, parentValue]);
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
default:
return "Invalid selection";
}
};
const handleChange = (e) => {
const value = e.target.value;
controllerField.onChange(value);
field.onChange?.(e);
field.onValueChange?.(value, { setValue, getValues, trigger });
field.onValueChangeDebounced?.(value, {
setValue,
getValues,
trigger
});
if ((error || fieldError) && field.showErrorOnBlur) {
trigger(name);
}
};
const handleBlur = () => {
if (field.showErrorOnBlur) {
trigger(name);
}
};
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? {
display: "block",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: "#333"
};
const selectStyle = {
padding: "10px",
border: "1px solid",
borderColor: error || fieldError ? "#f87171" : "#ccc",
borderRadius: "6px",
fontSize: "14px",
width: "100%",
backgroundColor: "#fff",
color: "#111827",
outline: "none",
boxSizing: "border-box",
appearance: "none",
WebkitAppearance: "none",
MozAppearance: "none",
paddingRight: "2rem",
cursor: field.disabled ? "not-allowed" : "pointer",
...field.inputStyle
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: "#6b7280"
};
const selectedOption = dynamicOptions.find(
(opt) => opt.value === controllerField.value
);
if (typeof field.render === "function") {
return field.render({
name,
error,
register: void 0,
defaultValue: field.defaultValue
});
}
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
loading && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { position: "relative", width: "100%" }, children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
"select",
{
id: name,
...controllerField,
onChange: handleChange,
onBlur: handleBlur,
className: field.inputClass ?? "",
style: selectStyle,
disabled: field.disabled || loading,
"aria-describedby": field.helpText ? `${name}-description` : void 0,
"aria-invalid": !!(error || fieldError),
children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: loading ? "Loading..." : "Select..." }),
dynamicOptions.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"option",
{
value: opt.value,
disabled: opt.disabled,
title: opt.tooltip,
children: opt.label
},
opt.value
))
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"span",
{
style: {
position: "absolute",
right: "0.75rem",
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
fontSize: "1rem",
color: "#6b7280"
},
"aria-hidden": "true",
children: "\u25BC"
}
)
] }),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
selectedOption?.helpText && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...helpTextStyle, marginTop: "2px" }, children: selectedOption.helpText }),
fetchError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { color: "orange", fontSize: "0.875rem", marginTop: "4px" }, children: fetchError }),
(error || fieldError) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) })
] });
};
var SelectField_default = import_react5.default.memo(SelectFieldComponent);
// src/DynamicForm/FieldTypes/TextAreaField.tsx
var import_react6 = __toESM(require("react"), 1);
var import_react_hook_form6 = require("react-hook-form");
var import_jsx_runtime6 = require("react/jsx-runtime");
var TextAreaFieldComponent = ({
field,
name,
error,
register
}) => {
const { setValue, getValues, trigger, control } = (0, import_react_hook_form6.useFormContext)();
const { field: controllerField } = (0, import_react_hook_form6.useController)({
name,
control,
defaultValue: field.defaultValue ?? "",
rules: {
required: field.required,
pattern: field.validation?.pattern,
minLength: field.validation?.minLength,
maxLength: field.validation?.maxLength,
validate: field.validation?.custom
}
});
const debounceTimer = (0, import_react6.useRef)(null);
const [loading, setLoading] = (0, import_react6.useState)(false);
import_react6.default.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? {
display: "block",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: "#333"
};
const inputStyle = {
padding: "10px",
border: "1px solid",
borderColor: error ? "#f87171" : "#ccc",
borderRadius: "6px",
fontSize: "14px",
width: "100%",
minHeight: "100px",
backgroundColor: "#fff",
color: "#111827",
outline: "none",
boxSizing: "border-box",
transition: "border-color 0.2s ease-in-out",
opacity: field.disabled ? 0.6 : 1,
cursor: field.disabled ? "not-allowed" : "text",
...field.inputStyle
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: "#6b7280"
};
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
case "minLength":
return `${field.label} must be at least ${field.validation?.minLength?.value} characters`;
case "maxLength":
return `${field.label} must be at most ${field.validation?.maxLength?.value} characters`;
case "pattern":
return `${field.label} format is invalid`;
default:
return "Invalid value";
}
};
const handleChange = (e) => {
const value2 = e.target.value;
controllerField.onChange(e);
field.onChange?.(e);
if (field.onValueChange) {
field.onValueChange(value2, { setValue, getValues, trigger });
}
if (error && field.showErrorOnBlur) {
trigger(name);
}
if (field.onValueChangeDebounced) {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setLoading(true);
debounceTimer.current = setTimeout(async () => {
await field.onValueChangeDebounced?.(value2, {
setValue,
getValues,
trigger
});
setLoading(false);
}, field.debounceMs ?? 500);
}
};
const value = controllerField.value ?? "";
const wordCount = value.trim() === "" ? 0 : value.trim().split(/\s+/).length;
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
field.tooltip && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"span",
{
title: field.tooltip,
style: { marginLeft: "6px", cursor: "help" },
children: "\u2139\uFE0F"
}
),
loading && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "0.75rem", marginLeft: "6px" }, children: "\u23F3" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "flex-start" }, children: [
field.prefixIcon && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginRight: "6px" }, children: field.prefixIcon }),
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"textarea",
{
id: name,
"aria-invalid": !!error,
"aria-required": field.required,
"aria-disabled": field.disabled,
"aria-describedby": field.helpText ? `${name}-description` : void 0,
value,
minLength: field.minLength,
maxLength: field.maxLength,
onChange: handleChange,
onBlur: (e) => {
controllerField.onBlur();
field.onBlur?.(e);
if (field.showErrorOnBlur) {
trigger(name);
}
},
className: field.inputClass ?? "",
style: inputStyle,
disabled: field.disabled
}
),
field.suffixIcon && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { marginLeft: "6px" }, children: field.suffixIcon })
] }),
field.helpText && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: { ...helpTextStyle, marginLeft: "0" },
children: field.helpText
}
),
(field.maxLength || field.showWordCount) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
"div",
{
style: {
fontSize: "12px",
marginTop: "4px",
textAlign: "right",
color: "#9ca3af"
},
children: [
field.showWordCount && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
wordCount,
" words"
] }),
field.maxLength && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
" ",
value.length,
"/",
field.maxLength
] })
]
}
),
error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var TextAreaField_default = import_react6.default.memo(TextAreaFieldComponent);
// src/DynamicForm/FieldTypes/TextField.tsx
var import_react7 = __toESM(require("react"), 1);
var import_react_hook_form7 = require("react-hook-form");
var import_jsx_runtime7 = require("react/jsx-runtime");
var TextFieldComponent = ({
field,
name,
error
}) => {
const { setValue, getValues, trigger, control } = (0, import_react_hook_form7.useFormContext)();
const { field: controllerField } = (0, import_react_hook_form7.useController)({
name,
control,
defaultValue: field.defaultValue ?? "",
rules: {
required: field.required,
pattern: field.validation?.pattern,
minLength: field.validation?.minLength,
maxLength: field.validation?.maxLength,
validate: field.validation?.custom
}
});
const debounceTimer = (0, import_react7.useRef)(null);
const [loading, setLoading] = (0, import_react7.useState)(false);
import_react7.default.useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const helpTextPosition = field.helpTextAlignment ?? "underLabel";
const handleChange = (0, import_react7.useCallback)(
(e) => {
const value = e.target.value;
controllerField.onChange(e);
field.onChange?.(e);
if (field.onValueChange) {
field.onValueChange(value, { setValue, getValues, trigger });
}
if (error && field.showErrorOnBlur) {
trigger(name);
}
if (field.onValueChangeDebounced) {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
setLoading(true);
debounceTimer.current = setTimeout(async () => {
await field.onValueChangeDebounced?.(value, {
setValue,
getValues,
trigger
});
setLoading(false);
}, field.debounceMs ?? 500);
}
},
[field, setValue, getValues, trigger, controllerField]
);
const wrapperStyle = field.wrapperStyle ?? { marginBottom: "1rem" };
const labelStyle = field.labelStyle ?? {
display: "flex",
alignItems: "center",
gap: "0.4rem",
marginBottom: "6px",
fontWeight: 500,
fontSize: "14px",
color: "#333"
};
const inputStyle = {
padding: "10px",
border: "1px solid",
borderColor: error ? "#f87171" : "#ccc",
borderRadius: "6px",
fontSize: "14px",
width: "100%",
backgroundColor: "#fff",
color: "#111827",
outline: "none",
boxSizing: "border-box",
transition: "border-color 0.2s ease-in-out",
opacity: field.disabled ? 0.6 : 1,
cursor: field.disabled ? "not-allowed" : "text",
...field.inputStyle
};
const helpTextStyle = field.helpTextStyle ?? {
fontSize: "12px",
marginTop: "4px",
color: "#6b7280"
};
const errorStyle = field.errorStyle ?? {
color: "#d93025",
marginTop: "6px",
fontSize: "13px"
};
const Tooltip = field.tooltip ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { title: field.tooltip, style: { cursor: "help", fontSize: "13px" }, children: "\u2753" }) : null;
const LabelIcon = field.icon ?? null;
const getAutoErrorMessage = (error2) => {
switch (error2?.type) {
case "required":
return `${field.label} is required`;
case "minLength":
return `${field.label} must be at least ${field.validation?.minLength?.value} characters`;
case "maxLength":
return `${field.label} must be at most ${field.validation?.maxLength?.value} characters`;
case "pattern":
return `${field.label} format is invalid`;
default:
return "Invalid value";
}
};
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
" ",
LabelIcon,
" ",
Tooltip,
loading && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" })
]
}
),
helpTextPosition === "underLabel" && field.helpText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: {
...helpTextStyle
},
children: field.helpText
}
),
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
"input",
{
id: name,
type: field.type,
placeholder: field.placeholder,
"aria-invalid": Boolean(error),
"aria-describedby": field.helpText ? `${name}-description` : void 0,
inputMode: field.inputMode,
value: controllerField.value ?? "",
minLength: field.minLength,
maxLength: field.maxLength,
onChange: handleChange,
onBlur: (e) => {
field.onBlur?.(e);
if (field.showErrorOnBlur) {
trigger(name);
}
},
onKeyDown: (e) => {
if (field.allowedPattern) {
const char = e.key;
const isControlKey = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"Tab"
].includes(char);
if (!isControlKey && !field.allowedPattern.test(char)) {
e.preventDefault();
}
}
field.onKeyDown?.(e);
},
className: field.inputClass ?? "",
style: inputStyle,
disabled: field.disabled
}
),
helpTextPosition !== "underLabel" && field.helpText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var TextField_default = import_react7.default.memo(TextFieldComponent);
// src/FormEngine.tsx
var import_jsx_runtime8 = require("react/jsx-runtime");
var resolveNestedValue = (obj, path) => path.split(".").reduce((acc, key) => acc?.[key], obj);
var BaseFieldRenderer = ({ field, parentName }) => {
const { register, formState, control, setValue, getValues } = (0, import_react_hook_form8.useFormContext)();
const qualifiedFieldName = parentName ? `${parentName}.${field.name}` : field.name;
const error = resolveNestedValue(formState.errors, qualifiedFieldName);
const formValues = (0, import_react_hook_form8.useWatch)({ control });
let shouldRenderField = true;
if (field.visibleWhen?.conditions?.length) {
const { conditions, logic = "AND" } = field.visibleWhen;
const results = conditions.map(({ field: target, value, operator }) => {
const currentVal = resolveNestedValue(formValues, target);
switch (operator) {
case "equals":
return currentVal === value;
case "notEquals":
return currentVal !== value;
case "in":
return Array.isArray(value) && value.includes(currentVal);
case "notIn":
return Array.isArray(value) && !value.includes(currentVal);
case "exists":
return currentVal !== void 0 && currentVal !== null && currentVal !== "";
case "notExists":
return currentVal === void 0 || currentVal === null || currentVal === "";
default:
return false;
}
});
shouldRenderField = logic === "AND" ? results.every(Boolean) : results.some(Boolean);
}
(0, import_react8.useEffect)(() => {
if (!shouldRenderField && !field.preserveValue) {
const existing = resolveNestedValue(getValues(), qualifiedFieldName);
if (existing !== void 0) {
const fallback = field.defaultValue ?? "";
setValue(qualifiedFieldName, fallback);
}
}
}, [
shouldRenderField,
qualifiedFieldName,
field.defaultValue,
field.preserveValue,
getValues,
setValue
]);
if (!shouldRenderField) return null;
const sharedFieldProps = {
field,
name: qualifiedFieldName,
error,
register
};
if (typeof field.render === "function") {
return field.render({
name: qualifiedFieldName,
error,
register,
defaultValue: field.defaultValue
});
}
if (field.overrideComponent) {
const OverrideComponent = field.overrideComponent;
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
OverrideComponent,
{
...field.overrideComponentProps ?? {},
field,
name: qualifiedFieldName,
error,
register
}
);
}
switch (field.type) {
case "text":
case "email":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TextField_default, { ...sharedFieldProps });
case "number":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(NumberField_default, { ...sharedFieldProps });
case "textarea":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TextAreaField_default, { ...sharedFieldProps });
case "select":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectField_default, { ...sharedFieldProps });
case "radio":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RadioField_default, { ...sharedFieldProps });
case "checkbox":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
CheckboxField_default,
{
field,
name: qualifiedFieldName,
register
}
);
case "group":
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GroupField_default, { ...sharedFieldProps });
default:
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { children: [
"Unsupported field type: ",
field.type
] });
}
};
var FormEngine_default = import_react8.default.memo(BaseFieldRenderer);
// src/DynamicForm/DynamicForm.tsx
var import_jsx_runtime9 = require("react/jsx-runtime");
var defaultButtonStyle = {
padding: "0.75rem 1.5rem",
background: "#2563eb",
color: "#fff",
border: "none",
borderRadius: "0.5rem",
fontWeight: 600,
boxShadow: "0 1px 2px 0 rgba(30,41,59,.06)",
transition: "background 0.18s",
cursor: "pointer"
};
var defaultFormStyle = {
background: "#fff",
boxShadow: "0 4px 24px 0 rgba(30, 41, 59, 0.10)",
borderRadius: "1rem",
padding: "1.5rem",
display: "grid",
gri