UNPKG

@goutham1494/form-engine

Version:

A schema-driven dynamic form builder for React

1,526 lines (1,515 loc) 57.8 kB
// 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