@goutham1494/form-engine
Version:
A schema-driven dynamic form builder for React
1,526 lines (1,515 loc) • 57.8 kB
JavaScript
// src/DynamicForm/DynamicForm.tsx
import { useEffect as useEffect4, useState as useState7 } from "react";
import { useForm, FormProvider } from "react-hook-form";
// src/FormEngine.tsx
import React8, { useEffect as useEffect3 } from "react";
import { useFormContext as useFormContext8, useWatch as useWatch3 } from "react-hook-form";
// src/DynamicForm/FieldTypes/CheckboxField.tsx
import React, { useRef, useState } from "react";
import { useFormContext, useController } from "react-hook-form";
import { jsx, jsxs } from "react/jsx-runtime";
var CheckboxFieldComponent = ({
field,
name,
error
}) => {
const { control, setValue, getValues, trigger } = useFormContext();
const debounceTimer = useRef(null);
const [loading, setLoading] = useState(false);
const isDarkMode = field.theme === "dark";
React.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const {
field: controllerField,
fieldState: { error: controllerError }
} = 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__ */ jsxs("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
(field.checkboxLabel ?? field.label) && /* @__PURE__ */ jsx(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: field.checkboxLabel ?? field.label
}
),
/* @__PURE__ */ 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__ */ jsxs(
"div",
{
style: { display: "flex", flexDirection: "column" },
children: [
/* @__PURE__ */ 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__ */ 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__ */ jsx("span", { children: opt.label })
]
}
),
opt.helpText && /* @__PURE__ */ jsx(
"div",
{
id: `${inputId}-desc`,
role: "note",
style: {
...helpTextStyle,
marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: opt.helpText
}
)
]
},
opt.value
);
}) : /* @__PURE__ */ jsxs(
"label",
{
className: field.optionWrapperClass ?? "",
style: optionWrapperStyle,
children: [
/* @__PURE__ */ 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__ */ jsx("span", { children: field.label })
]
}
)
}
),
field.helpText && /* @__PURE__ */ jsx(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || controllerError) && /* @__PURE__ */ 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 = React.memo(CheckboxFieldComponent);
// src/DynamicForm/FieldTypes/GroupField.tsx
import React2, { useEffect } from "react";
import {
useController as useController2,
useFormContext as useFormContext2,
useWatch
} from "react-hook-form";
import { jsx as jsx2, jsxs as jsxs2 } from "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 } = useFormContext2();
const {
field: groupField,
formState: { errors }
} = useController2({
name,
control,
rules: {
required: field.required,
validate: field.validation?.custom
},
defaultValue: field.defaultValue ?? {}
});
const watchedGroupValue = 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"
};
useEffect(() => {
const hasChanged = JSON.stringify(watchedGroupValue) !== JSON.stringify(groupField.value);
if (hasChanged) groupField.onChange(watchedGroupValue);
}, [watchedGroupValue, groupField]);
return /* @__PURE__ */ jsxs2("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
(field.checkboxLabel ?? field.label) && /* @__PURE__ */ jsx2(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: field.checkboxLabel ?? field.label
}
),
/* @__PURE__ */ jsx2(
"div",
{
id: name,
className: field.layoutClass ?? "",
style: field.layoutStyle,
children: field.children?.map((child) => /* @__PURE__ */ jsx2(
FormEngine_default,
{
field: child,
parentName: name
},
child.name
))
}
),
field.helpText && /* @__PURE__ */ jsx2(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || groupError) && /* @__PURE__ */ jsx2("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: getErrorMessage(error || groupError, field) })
] });
};
var GroupField_default = React2.memo(GroupFieldComponent);
// src/DynamicForm/FieldTypes/NumberField.tsx
import React3, { useCallback, useRef as useRef2, useState as useState2 } from "react";
import { useController as useController3, useFormContext as useFormContext3 } from "react-hook-form";
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
var NumberFieldComponent = ({
field,
name,
error
}) => {
const { setValue, getValues, trigger, control } = useFormContext3();
const { field: controllerField } = useController3({
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 = useRef2(null);
const [loading, setLoading] = useState2(false);
const isDarkMode = field.theme === "dark";
const isNumberField = field.type === "number";
React3.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 = 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__ */ jsxs3("label", { htmlFor: name, className: field.labelClass ?? "", style: labelStyle, children: [
field.icon && /* @__PURE__ */ jsx3("span", { children: field.icon }),
/* @__PURE__ */ jsx3("span", { title: field.tooltip, children: field.label }),
loading && /* @__PURE__ */ jsx3("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" })
] });
return /* @__PURE__ */ jsxs3("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
renderLabel(),
/* @__PURE__ */ jsx3(
"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__ */ jsx3(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: {
...helpTextStyle,
marginLeft: field.helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: field.helpText
}
),
error && /* @__PURE__ */ jsx3("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var NumberField_default = React3.memo(NumberFieldComponent);
// src/DynamicForm/FieldTypes/RadioField.tsx
import React4, { useRef as useRef3, useState as useState3 } from "react";
import { useFormContext as useFormContext4, useController as useController4 } from "react-hook-form";
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
var RadioFieldComponent = ({
field,
name,
register,
error
}) => {
const { control, setValue, getValues, trigger } = useFormContext4();
const debounceTimer = useRef3(null);
const [loading, setLoading] = useState3(false);
const isDarkMode = field.theme === "dark";
React4.useEffect(() => {
return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, []);
const {
field: controllerField,
fieldState: { error: fieldError }
} = useController4({
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__ */ jsxs4("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ jsxs4("label", { className: field.labelClass ?? "", style: labelStyle, children: [
field.label,
loading && /* @__PURE__ */ jsx4("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" })
] }),
/* @__PURE__ */ jsx4("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__ */ jsxs4(
"div",
{
style: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start"
},
children: [
/* @__PURE__ */ jsxs4(
"label",
{
htmlFor: inputId,
className: field.optionWrapperClass,
style: {
display: "flex",
alignItems: "center",
gap: "8px",
cursor: field.disabled ? "not-allowed" : "pointer"
},
title: option.tooltip,
children: [
/* @__PURE__ */ jsx4(
"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__ */ jsx4(
"span",
{
style: {
fontSize: "14px",
fontWeight: isSelected ? 600 : 400,
color: isSelected ? "#004DB2" : void 0
},
children: option.label
}
)
]
}
),
option.helpText && /* @__PURE__ */ jsx4(
"div",
{
id: `${inputId}-desc`,
role: "note",
style: {
...helpTextStyle,
marginLeft: helpTextAlignment === "underLabel" ? "24px" : "0"
},
children: option.helpText
}
)
]
},
option.value
);
}) }),
field.helpText && /* @__PURE__ */ jsx4(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
(error || fieldError) && /* @__PURE__ */ jsx4("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) })
] });
};
var RadioField_default = React4.memo(RadioFieldComponent);
// src/DynamicForm/FieldTypes/SelectField.tsx
import React5, { useEffect as useEffect2, useState as useState4 } from "react";
import { useController as useController5, useFormContext as useFormContext5, useWatch as useWatch2 } from "react-hook-form";
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
var SelectFieldComponent = ({
field,
name,
error
}) => {
const { control, setValue, getValues, trigger } = useFormContext5();
const {
field: controllerField,
fieldState: { error: fieldError }
} = useController5({
name,
control,
defaultValue: field.defaultValue ?? "",
rules: { required: field.required }
});
const parentValue = useWatch2({ name: field.dependsOn || "__no_field__" });
const [dynamicOptions, setDynamicOptions] = useState4(field.options || []);
const [loading, setLoading] = useState4(false);
const [fetchError, setFetchError] = useState4(null);
useEffect2(() => {
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__ */ jsxs5("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ jsxs5(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
loading && /* @__PURE__ */ jsx5("span", { style: { fontSize: "0.75rem" }, children: " \u23F3" })
]
}
),
/* @__PURE__ */ jsxs5("div", { style: { position: "relative", width: "100%" }, children: [
/* @__PURE__ */ jsxs5(
"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__ */ jsx5("option", { value: "", children: loading ? "Loading..." : "Select..." }),
dynamicOptions.map((opt) => /* @__PURE__ */ jsx5(
"option",
{
value: opt.value,
disabled: opt.disabled,
title: opt.tooltip,
children: opt.label
},
opt.value
))
]
}
),
/* @__PURE__ */ jsx5(
"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__ */ jsx5(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
selectedOption?.helpText && /* @__PURE__ */ jsx5("div", { style: { ...helpTextStyle, marginTop: "2px" }, children: selectedOption.helpText }),
fetchError && /* @__PURE__ */ jsx5("p", { style: { color: "orange", fontSize: "0.875rem", marginTop: "4px" }, children: fetchError }),
(error || fieldError) && /* @__PURE__ */ jsx5("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error || fieldError) || (error || fieldError)?.message || getAutoErrorMessage(error || fieldError) })
] });
};
var SelectField_default = React5.memo(SelectFieldComponent);
// src/DynamicForm/FieldTypes/TextAreaField.tsx
import React6, { useRef as useRef4, useState as useState5 } from "react";
import { useController as useController6, useFormContext as useFormContext6 } from "react-hook-form";
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
var TextAreaFieldComponent = ({
field,
name,
error,
register
}) => {
const { setValue, getValues, trigger, control } = useFormContext6();
const { field: controllerField } = useController6({
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 = useRef4(null);
const [loading, setLoading] = useState5(false);
React6.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__ */ jsxs6("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ jsxs6(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
field.tooltip && /* @__PURE__ */ jsx6(
"span",
{
title: field.tooltip,
style: { marginLeft: "6px", cursor: "help" },
children: "\u2139\uFE0F"
}
),
loading && /* @__PURE__ */ jsx6("span", { style: { fontSize: "0.75rem", marginLeft: "6px" }, children: "\u23F3" })
]
}
),
/* @__PURE__ */ jsxs6("div", { style: { display: "flex", alignItems: "flex-start" }, children: [
field.prefixIcon && /* @__PURE__ */ jsx6("span", { style: { marginRight: "6px" }, children: field.prefixIcon }),
/* @__PURE__ */ jsx6(
"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__ */ jsx6("span", { style: { marginLeft: "6px" }, children: field.suffixIcon })
] }),
field.helpText && /* @__PURE__ */ jsx6(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: { ...helpTextStyle, marginLeft: "0" },
children: field.helpText
}
),
(field.maxLength || field.showWordCount) && /* @__PURE__ */ jsxs6(
"div",
{
style: {
fontSize: "12px",
marginTop: "4px",
textAlign: "right",
color: "#9ca3af"
},
children: [
field.showWordCount && /* @__PURE__ */ jsxs6("span", { children: [
wordCount,
" words"
] }),
field.maxLength && /* @__PURE__ */ jsxs6("span", { children: [
" ",
value.length,
"/",
field.maxLength
] })
]
}
),
error && /* @__PURE__ */ jsx6("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var TextAreaField_default = React6.memo(TextAreaFieldComponent);
// src/DynamicForm/FieldTypes/TextField.tsx
import React7, { useCallback as useCallback2, useRef as useRef5, useState as useState6 } from "react";
import { useController as useController7, useFormContext as useFormContext7 } from "react-hook-form";
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
var TextFieldComponent = ({
field,
name,
error
}) => {
const { setValue, getValues, trigger, control } = useFormContext7();
const { field: controllerField } = useController7({
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 = useRef5(null);
const [loading, setLoading] = useState6(false);
React7.useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const helpTextPosition = field.helpTextAlignment ?? "underLabel";
const handleChange = useCallback2(
(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__ */ jsx7("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__ */ jsxs7("div", { className: field.wrapperClass ?? "", style: wrapperStyle, children: [
/* @__PURE__ */ jsxs7(
"label",
{
htmlFor: name,
className: field.labelClass ?? "",
style: labelStyle,
children: [
field.label,
" ",
LabelIcon,
" ",
Tooltip,
loading && /* @__PURE__ */ jsx7("span", { style: { fontSize: "0.75rem" }, children: "\u23F3" })
]
}
),
helpTextPosition === "underLabel" && field.helpText && /* @__PURE__ */ jsx7(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: {
...helpTextStyle
},
children: field.helpText
}
),
/* @__PURE__ */ jsx7(
"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__ */ jsx7(
"p",
{
id: `${name}-description`,
className: field.helpTextClass ?? "",
style: helpTextStyle,
children: field.helpText
}
),
error && /* @__PURE__ */ jsx7("p", { className: field.errorClass ?? "", style: errorStyle, role: "alert", children: field.errorText || field.getErrorMessage?.(error) || error.message || getAutoErrorMessage(error) })
] });
};
var TextField_default = React7.memo(TextFieldComponent);
// src/FormEngine.tsx
import { jsx as jsx8, jsxs as jsxs8 } from "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 } = useFormContext8();
const qualifiedFieldName = parentName ? `${parentName}.${field.name}` : field.name;
const error = resolveNestedValue(formState.errors, qualifiedFieldName);
const formValues = useWatch3({ 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);
}
useEffect3(() => {
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__ */ jsx8(
OverrideComponent,
{
...field.overrideComponentProps ?? {},
field,
name: qualifiedFieldName,
error,
register
}
);
}
switch (field.type) {
case "text":
case "email":
return /* @__PURE__ */ jsx8(TextField_default, { ...sharedFieldProps });
case "number":
return /* @__PURE__ */ jsx8(NumberField_default, { ...sharedFieldProps });
case "textarea":
return /* @__PURE__ */ jsx8(TextAreaField_default, { ...sharedFieldProps });
case "select":
return /* @__PURE__ */ jsx8(SelectField_default, { ...sharedFieldProps });
case "radio":
return /* @__PURE__ */ jsx8(RadioField_default, { ...sharedFieldProps });
case "checkbox":
return /* @__PURE__ */ jsx8(
CheckboxField_default,
{
field,
name: qualifiedFieldName,
register
}
);
case "group":
return /* @__PURE__ */ jsx8(GroupField_default, { ...sharedFieldProps });
default:
return /* @__PURE__ */ jsxs8("p", { children: [
"Unsupported field type: ",
field.type
] });
}
};
var FormEngine_default = React8.memo(BaseFieldRenderer);
// src/DynamicForm/DynamicForm.tsx
import { jsx as jsx9, jsxs as jsxs9 } from "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",
gridTemplateColumns: "1fr",
gap: "1.25rem 1.5rem",
maxWidth: 800
};
var DynamicForm = ({
schema,
onSubmit,
formStyle,
formClassName,
submitButtonStyle,
submitButtonClassName,
resetButtonStyle,
resetButtonClassName,
submitLabel = "Submit",
resetLabel = "Reset",
extraActions,
hideSubmitButton = false,
children,
showReset = false,
onReset,
columns,
gap,
maxWidth
}) => {
const methods = useForm();
const [isMd, setIsMd] = useState7(
typeof window !== "undefined" ? window.innerWidth >= 768 : false
);
useEffect4(() => {
const handler = () => setIsMd(window.innerWidth >= 768);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
const gridColumns = columns ?? (isMd ? 2 : 1);
const mergedFormStyle = {
...defaultFormStyle,
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
gap: gap ?? defaultFormStyle.gap,
maxWidth: maxWidth ?? defaultFormStyle.maxWidth,
...formStyle
};
return /* @__PURE__ */ jsx9(FormProvider, { ...methods, children: /* @__PURE__ */ jsxs9(
"form",
{
onSubmit: methods.handleSubmit(onSubmit),
className: formClassName,
style: mergedFormStyle,
children: [
children,
schema.map((field) => /* @__PURE__ */ jsx9(FormEngine_default, { field }, field.name)),
(showReset || !hideSubmitButton) && /* @__PURE__ */ jsx9(
"div",
{
style: {
gridColumn: "1 / -1",
marginTop: "1rem",
display: "flex",
gap: "1rem"
},
children: (showReset || !hideSubmitButton || extraActions?.length) && /* @__PURE__ */ jsxs9(
"div",
{
style: {
gridColumn: "1 / -1",
marginTop: "1rem",
display: "flex",
gap: "1rem",
flexWrap: "wrap"
},
children: [
extraActions?.map((action, index) => /* @__PURE__ */ jsxs9(
"button",
{
type: action.type ?? "button",
onClick: action.onClick,
style: { ...defaultButtonStyle, ...action.style },
className: action.className,
children: [
action.icon && /* @__PURE__ */ jsx9("span", { style: { marginRight: 8 }, children: action.icon }),
action.label
]
},
`extra-btn-${index}`
)),
showReset && /* @__PURE__ */ jsx9(
"button",
{
type: "button",
style: { ...defaultButtonStyle, ...resetButtonStyle },
className: resetButtonClassName,
onClick: () => {
methods.reset();
onReset?.();
},
children: resetLabel
}
),
!hideSubmitButton && /* @__PURE__ */ jsx9(
"button",
{
type: "submit",
style: { ...defaultButtonStyle, ...submitButtonStyle },
className: submitButtonClassName,
onMouseOver: (e) => e.currentTarget.style.background = "#1d4ed8",
onMouseOut: (e) => e.currentTarget.style.background = "#2563eb",
children: submitLabel
}
)
]
}
)
}
)
]
}
) });
};
var DynamicForm_default = DynamicForm;
// src/DynamicForm/types/constant.ts
var FIELD_TYPES = {
TEXT: "text",
NUMBER: "number",
EMAIL: "email",
SELECT: "select",
CHECKBOX: "checkbox