react-smart-fields 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-smart-fields",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "> A flexible, customizable, and developer-friendly component to generate dynamic form fields in React. Supports all HTML inputs, validation, and styling out of the box.",
5
5
  "license": "MIT",
6
6
  "author": "Pratik Panchal",
@@ -0,0 +1,259 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+
3
+ export interface FieldOption {
4
+ label: string;
5
+ value: string | number | boolean;
6
+ }
7
+
8
+ export interface FieldConfig {
9
+ name: string;
10
+ label?: string;
11
+ type: "text" | "number" | "select" | "radio" | "checkbox";
12
+ options?: FieldOption[];
13
+ required?: boolean;
14
+ placeholder?: string;
15
+ description?: string;
16
+ }
17
+
18
+ export interface DynamicFieldsProps {
19
+ fields: FieldConfig[];
20
+ onChange: (data: Record<string, any>) => void;
21
+ className?: string;
22
+ inputClassName?: string;
23
+ labelClassName?: string;
24
+ mainFieldClassName?: string;
25
+ fieldClassName?: string;
26
+ errorClassName?: string;
27
+ selectClassName?: string;
28
+ optionClassName?: string;
29
+ checkboxClassName?: string;
30
+ radioClassName?: string;
31
+ title?: string;
32
+ description?: string;
33
+ }
34
+
35
+ export const DynamicFields: React.FC<DynamicFieldsProps> = ({
36
+ fields,
37
+ onChange,
38
+ className = "",
39
+ inputClassName = "",
40
+ labelClassName = "",
41
+ mainFieldClassName = "",
42
+ fieldClassName = "",
43
+ errorClassName = "",
44
+ selectClassName = "",
45
+ optionClassName = "",
46
+ checkboxClassName = "",
47
+ radioClassName = "",
48
+ title,
49
+ description
50
+ }) => {
51
+ const [formData, setFormData] = useState<Record<string, any>>({});
52
+ const [formErrors, setFormErrors] = useState<Record<string, string>>({});
53
+ const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
54
+ const dropdownRefs = useRef<Record<string, HTMLDivElement | null>>({});
55
+
56
+ // Consistent styling for all components with dark mode support
57
+
58
+ const defaultInputClassName = "w-full rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-4 py-2 text-sm text-gray-800 dark:text-gray-200 placeholder:text-gray-500 dark:placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 shadow-sm transition-colors";
59
+
60
+ const defaultLabelClassName = "flex text-sm font-medium text-gray-800 dark:text-gray-200 leading-none mb-2";
61
+
62
+ const defaultFieldClassName = "w-full";
63
+
64
+ const defaultErrorClassName = "text-sm font-medium text-red-500 dark:text-red-400";
65
+
66
+ const defaultSelectClassName = "w-full rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 cursor-pointer shadow-sm";
67
+
68
+ const defaultCheckboxClassName = "h-4 w-4 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-sm focus:outline-none transition-colors checked:bg-blue-500 dark:checked:bg-blue-400 checked:border-blue-500 dark:checked:border-blue-400";
69
+
70
+ const defaultRadioClassName = "select-none h-4 w-4 rounded-full border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-sm focus:outline-none transition-colors checked:bg-blue-500 dark:checked:bg-blue-400 checked:border-blue-500 dark:checked:border-blue-400";
71
+
72
+ const defaultOptionClassName = "py-2 px-4 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer first:rounded-t-lg last:rounded-b-lg transition-colors";
73
+
74
+ useEffect(() => {
75
+ const initialData: Record<string, any> = {};
76
+ fields.forEach((f) => {
77
+ initialData[f.name] = f.type === "checkbox" ? false : "";
78
+ });
79
+ setFormData(initialData);
80
+ }, [fields]);
81
+
82
+ useEffect(() => {
83
+ const handleClickOutside = (event: MouseEvent) => {
84
+ Object.keys(dropdownRefs.current).forEach((fieldName) => {
85
+ const ref = dropdownRefs.current[fieldName];
86
+ if (ref && !ref.contains(event.target as Node)) {
87
+ setOpenDropdowns(prev => ({ ...prev, [fieldName]: false }));
88
+ }
89
+ });
90
+ };
91
+
92
+ document.addEventListener('mousedown', handleClickOutside);
93
+ return () => document.removeEventListener('mousedown', handleClickOutside);
94
+ }, []);
95
+
96
+ const toggleDropdown = (fieldName: string) => {
97
+ setOpenDropdowns(prev => ({ ...prev, [fieldName]: !prev[fieldName] }));
98
+ };
99
+
100
+ const selectOption = (fieldName: string, value: any) => {
101
+ handleChange(fieldName, value);
102
+ setOpenDropdowns(prev => ({ ...prev, [fieldName]: false }));
103
+ };
104
+
105
+ const handleChange = (name: string, value: any) => {
106
+ const updatedData = { ...formData, [name]: value };
107
+ setFormData(updatedData);
108
+ onChange(updatedData);
109
+
110
+ const field = fields.find((f) => f.name === name);
111
+ if (field?.required && (value === "" || value === null || value === undefined)) {
112
+ setFormErrors((prev) => ({ ...prev, [name]: "This field is required" }));
113
+ } else {
114
+ setFormErrors((prev) => {
115
+ const newErrors = { ...prev };
116
+ delete newErrors[name];
117
+ return newErrors;
118
+ });
119
+ }
120
+ };
121
+
122
+ const renderField = (field: FieldConfig) => {
123
+ const error = formErrors[field.name];
124
+ const finalInputClassName = `${defaultInputClassName} ${inputClassName || ""}`.trim();
125
+ const finalLabelClassName = `${defaultLabelClassName} ${labelClassName || ""}`.trim();
126
+ const finalFieldClassName = `${defaultFieldClassName} ${fieldClassName || ""}`.trim();
127
+ const finalErrorClassName = `${defaultErrorClassName} ${errorClassName || ""}`.trim();
128
+ const finalSelectClassName = `${defaultSelectClassName} ${selectClassName || ""}`.trim();
129
+ const finalCheckboxClassName = `${defaultCheckboxClassName} ${checkboxClassName || ""}`.trim();
130
+ const finalRadioClassName = `${defaultRadioClassName} ${radioClassName || ""}`.trim();
131
+ const finalOptionClassName = `${defaultOptionClassName} ${optionClassName || ""}`.trim();
132
+ const inputClassWithError = `${finalInputClassName} ${error ? "border-red-500 dark:border-red-400 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500 dark:focus:border-red-400" : ""}`;
133
+ const selectClassWithError = `${finalSelectClassName} ${error ? "border-red-500 dark:border-red-400 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500 dark:focus:border-red-400" : ""}`;
134
+
135
+ return (
136
+ <div key={field.name} className={finalFieldClassName}>
137
+ {field.label && field.type !== "checkbox" && (
138
+ <label htmlFor={field.name} className={finalLabelClassName}>
139
+ {field.label}
140
+ {field.required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
141
+ </label>
142
+ )}
143
+
144
+ {field.description && field.type !== "checkbox" && (
145
+ <p className="text-sm text-gray-500 dark:text-gray-400">{field.description}</p>
146
+ )}
147
+
148
+ {field.type === "select" && field.options ? (
149
+ <div
150
+ ref={el => { dropdownRefs.current[field.name] = el; }}
151
+ className="relative"
152
+ >
153
+ <div
154
+ className={`${selectClassWithError} flex items-center justify-between`}
155
+ onClick={() => toggleDropdown(field.name)}
156
+ >
157
+ <span className={formData[field.name] ? "text-gray-800 dark:text-gray-200" : "text-gray-500 dark:text-gray-400"}>
158
+ {formData[field.name]
159
+ ? field.options.find(opt => String(opt.value) === String(formData[field.name]))?.label
160
+ : field.placeholder || "Select an option..."
161
+ }
162
+ </span>
163
+ <svg
164
+ className={`w-4 h-4 text-gray-400 dark:text-gray-500 transform transition-transform ${openDropdowns[field.name] ? 'rotate-180' : ''}`}
165
+ fill="none"
166
+ stroke="currentColor"
167
+ viewBox="0 0 24 24"
168
+ >
169
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
170
+ </svg>
171
+ </div>
172
+
173
+ {openDropdowns[field.name] && (
174
+ <div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg dark:shadow-2xl overflow-hidden">
175
+ {field.options.map((opt, index) => (
176
+ <div
177
+ key={String(opt.value)}
178
+ className={`${finalOptionClassName} ${index === 0 ? 'rounded-t-lg' : ''} ${index === field.options!.length - 1 ? 'rounded-b-lg' : ''}`}
179
+ onClick={() => selectOption(field.name, opt.value)}
180
+ >
181
+ {opt.label}
182
+ </div>
183
+ ))}
184
+ </div>
185
+ )}
186
+ </div>
187
+ ) : field.type === "radio" && field.options ? (
188
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
189
+ {field.options.map((opt) => (
190
+ <div key={String(opt.value)} className="flex items-center space-x-2">
191
+ <input
192
+ type="radio"
193
+ id={`${field.name}-${opt.value}`}
194
+ name={field.name}
195
+ value={String(opt.value)}
196
+ checked={formData[field.name] === opt.value}
197
+ onChange={() => handleChange(field.name, opt.value)}
198
+ className={finalRadioClassName}
199
+ />
200
+ <label
201
+ htmlFor={`${field.name}-${opt.value}`}
202
+ className="select-none text-sm font-medium text-gray-800 dark:text-gray-200 leading-none cursor-pointer"
203
+ >
204
+ {opt.label}
205
+ </label>
206
+ </div>
207
+ ))}
208
+ </div>
209
+ ) : field.type === "checkbox" ? (
210
+ <div className="flex items-center space-x-2">
211
+ <input
212
+ type="checkbox"
213
+ id={field.name}
214
+ checked={formData[field.name] || false}
215
+ onChange={(e) => handleChange(field.name, e.target.checked)}
216
+ className={finalCheckboxClassName}
217
+ />
218
+ <label htmlFor={field.name} className="text-sm font-medium text-gray-800 dark:text-gray-200 leading-none cursor-pointer">
219
+ {field.label}
220
+ {field.required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
221
+ </label>
222
+ {field.description && (
223
+ <p className="text-sm text-gray-500 dark:text-gray-400 ml-6">{field.description}</p>
224
+ )}
225
+ </div>
226
+ ) : (
227
+ <input
228
+ type={field.type}
229
+ id={field.name}
230
+ name={field.name}
231
+ placeholder={field.placeholder}
232
+ className={inputClassWithError}
233
+ value={formData[field.name] ?? ""}
234
+ onChange={(e) => handleChange(field.name, field.type === "number" ? Number(e.target.value) || "" : e.target.value)}
235
+ />
236
+ )}
237
+
238
+ {error && <p className={finalErrorClassName}>{error}</p>}
239
+ </div>
240
+ );
241
+ };
242
+
243
+ return (
244
+ <div className={`w-full max-w-2xl mx-auto p-6 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm dark:shadow-2xl ${className}`}>
245
+ {title && (
246
+ <div>
247
+ <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-100 tracking-tight">{title}</h2>
248
+ {description && (
249
+ <p className="text-sm text-gray-500 dark:text-gray-400">{description}</p>
250
+ )}
251
+ </div>
252
+ )}
253
+
254
+ <div className={`space-y-2 ${mainFieldClassName}`}>
255
+ {fields.map(renderField)}
256
+ </div>
257
+ </div>
258
+ );
259
+ };
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { DynamicFields } from "./components/DynamicField";
2
- export type { FieldConfig, FieldOption } from "./components/DynamicField";
1
+ export { DynamicFields } from "./components/DynamicFields";
2
+ export type { FieldConfig, FieldOption } from "./components/DynamicFields";
@@ -1,193 +0,0 @@
1
- import React, { useState } from "react";
2
-
3
- type FieldType =
4
- | "text"
5
- | "number"
6
- | "email"
7
- | "password"
8
- | "date"
9
- | "checkbox"
10
- | "radio"
11
- | "select";
12
-
13
- export interface FieldOption {
14
- label: string;
15
- value: string | number | boolean;
16
- }
17
-
18
- export interface FieldConfig {
19
- name: string;
20
- label?: string;
21
- type: FieldType;
22
- required?: boolean;
23
- defaultValue?: any;
24
- customValidation?: (value: any) => string | null;
25
- options?: FieldOption[];
26
- style?: React.CSSProperties;
27
- className?: string;
28
- }
29
-
30
- interface DynamicFieldsProps {
31
- fields: FieldConfig[];
32
- onChange?: (
33
- formData: { [key: string]: any },
34
- errors: { [key: string]: string | null }
35
- ) => void;
36
- }
37
-
38
- export const DynamicFields: React.FC<DynamicFieldsProps> = ({
39
- fields,
40
- onChange,
41
- }) => {
42
- const [formData, setFormData] = useState<{ [key: string]: any }>(() => {
43
- const initial: any = {};
44
- fields.forEach((f) => {
45
- initial[f.name] =
46
- f.type === "checkbox"
47
- ? f.defaultValue ?? false
48
- : f.defaultValue ?? "";
49
- });
50
- return initial;
51
- });
52
-
53
- const [errors, setErrors] = useState<{ [key: string]: string | null }>({});
54
-
55
- const validateField = (field: FieldConfig, value: any) => {
56
- if (field.required && (value === "" || value === null || value === undefined || value === false)) {
57
- return "This field is required";
58
- }
59
- if (field.customValidation) return field.customValidation(value);
60
- return null;
61
- };
62
-
63
- const handleChange = (
64
- name: string,
65
- value: any,
66
- field: FieldConfig
67
- ) => {
68
- const newFormData = { ...formData, [name]: value };
69
- const error = validateField(field, value);
70
- const newErrors = { ...errors, [name]: error };
71
-
72
- setFormData(newFormData);
73
- setErrors(newErrors);
74
-
75
- if (onChange) onChange(newFormData, newErrors);
76
- };
77
-
78
- const defaultInputClass =
79
- "padding: 0.6rem 1rem; border: 1px solid #ccc; border-radius: 8px; width: 100%; font-size: 1rem; transition: border-color 0.2s ease-in-out; outline: none;";
80
- const defaultInputFocus =
81
- "focus: border-color: #007BFF;";
82
-
83
- return (
84
- <>
85
- {fields.map((field) => (
86
- <div key={field.name} style={{ marginBottom: "1.25rem" }}>
87
- {field.label && (
88
- <label
89
- htmlFor={field.name}
90
- style={{ display: "block", marginBottom: "0.5rem", fontWeight: 600 }}
91
- >
92
- {field.label}
93
- </label>
94
- )}
95
-
96
- {/* Select Dropdown */}
97
- {field.type === "select" && (
98
- <select
99
- name={field.name}
100
- className={field.className}
101
- style={{
102
- ...field.style,
103
- ...{
104
- padding: "0.6rem 1rem",
105
- border: "1px solid #ccc",
106
- borderRadius: "8px",
107
- width: "100%",
108
- fontSize: "1rem",
109
- },
110
- }}
111
- value={formData[field.name]}
112
- onChange={(e) =>
113
- handleChange(field.name, e.target.value, field)
114
- }
115
- >
116
- <option value="">Select an option</option>
117
- {field.options?.map((opt) => (
118
- <option key={String(opt.value)} value={String(opt.value)}>
119
- {opt.label}
120
- </option>
121
- ))}
122
- </select>
123
- )}
124
-
125
- {/* Radio Buttons */}
126
- {field.type === "radio" && (
127
- <div style={{ display: "flex", gap: "1rem" }}>
128
- {field.options?.map((opt) => (
129
- <label key={String(opt.value)} style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
130
- <input
131
- type="radio"
132
- name={field.name}
133
- value={String(opt.value)}
134
- checked={formData[field.name] === opt.value}
135
- onChange={() => handleChange(field.name, opt.value, field)}
136
- />
137
- {opt.label}
138
- </label>
139
- ))}
140
- </div>
141
- )}
142
-
143
- {/* Checkbox */}
144
- {field.type === "checkbox" && (
145
- <label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
146
- <input
147
- type="checkbox"
148
- name={field.name}
149
- checked={formData[field.name]}
150
- className={field.className}
151
- style={field.style}
152
- onChange={(e) =>
153
- handleChange(field.name, e.target.checked, field)
154
- }
155
- />
156
- {field.label}
157
- </label>
158
- )}
159
-
160
- {/* Text, Email, Number, etc. */}
161
- {!["select", "radio", "checkbox"].includes(field.type) && (
162
- <input
163
- type={field.type}
164
- name={field.name}
165
- value={formData[field.name]}
166
- className={field.className}
167
- style={{
168
- ...{
169
- padding: "0.6rem 1rem",
170
- border: "1px solid #ccc",
171
- borderRadius: "8px",
172
- width: "100%",
173
- fontSize: "1rem",
174
- },
175
- ...field.style,
176
- }}
177
- onChange={(e) =>
178
- handleChange(field.name, e.target.value, field)
179
- }
180
- />
181
- )}
182
-
183
- {/* Error Display */}
184
- {errors[field.name] && (
185
- <p style={{ color: "red", marginTop: "0.4rem", fontSize: "0.9rem" }}>
186
- {errors[field.name]}
187
- </p>
188
- )}
189
- </div>
190
- ))}
191
- </>
192
- );
193
- };