react-smart-fields 1.1.6 → 2.2.0

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/dist/index.js CHANGED
@@ -1 +1,558 @@
1
- export { DynamicFields } from "./components/DynamicFields";
1
+ // src/components/DynamicFields.tsx
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
+ var EMPTY_OBJECT = {};
4
+ var cn = (...values) => values.filter(Boolean).join(" ").trim();
5
+ var parsePath = (path) => {
6
+ const segments = path.match(/[^.[\]]+/g) ?? [];
7
+ return segments.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
8
+ };
9
+ var getIn = (source, path) => {
10
+ const segments = parsePath(path);
11
+ let current = source;
12
+ for (const segment of segments) {
13
+ if (current === null || current === void 0) {
14
+ return void 0;
15
+ }
16
+ current = current[segment];
17
+ }
18
+ return current;
19
+ };
20
+ var setIn = (source, path, value) => {
21
+ const segments = parsePath(path);
22
+ if (!segments.length) {
23
+ return source;
24
+ }
25
+ const result = Array.isArray(source) ? [...source] : { ...source };
26
+ let currentResult = result;
27
+ let currentSource = source;
28
+ for (let index = 0; index < segments.length - 1; index += 1) {
29
+ const segment = segments[index];
30
+ const nextSegment = segments[index + 1];
31
+ const sourceValue = currentSource?.[segment];
32
+ let nextValue;
33
+ if (Array.isArray(sourceValue)) {
34
+ nextValue = [...sourceValue];
35
+ } else if (sourceValue && typeof sourceValue === "object") {
36
+ nextValue = { ...sourceValue };
37
+ } else {
38
+ nextValue = typeof nextSegment === "number" ? [] : {};
39
+ }
40
+ currentResult[segment] = nextValue;
41
+ currentResult = nextValue;
42
+ currentSource = sourceValue;
43
+ }
44
+ const lastSegment = segments[segments.length - 1];
45
+ currentResult[lastSegment] = value;
46
+ return result;
47
+ };
48
+ var isEmptyValue = (value) => {
49
+ if (value === null || value === void 0) {
50
+ return true;
51
+ }
52
+ if (typeof value === "string") {
53
+ return value.trim() === "";
54
+ }
55
+ if (Array.isArray(value)) {
56
+ return value.length === 0;
57
+ }
58
+ return false;
59
+ };
60
+ var toComparable = (value) => {
61
+ try {
62
+ return JSON.stringify(value);
63
+ } catch {
64
+ return String(value);
65
+ }
66
+ };
67
+ var buildInitialValues = (fields, defaultValues, controlledValues) => {
68
+ const seed = { ...defaultValues ?? EMPTY_OBJECT, ...controlledValues ?? EMPTY_OBJECT };
69
+ return fields.reduce((accumulator, field) => {
70
+ const existing = getIn(accumulator, field.name);
71
+ if (existing !== void 0) {
72
+ return accumulator;
73
+ }
74
+ const fallbackValue = field.defaultValue !== void 0 ? field.defaultValue : field.type === "checkbox" ? false : "";
75
+ return setIn(accumulator, field.name, fallbackValue);
76
+ }, seed);
77
+ };
78
+ var baseTheme = {
79
+ default: {
80
+ wrapper: "w-full max-w-2xl mx-auto p-6 bg-white dark:bg-zinc-950 rounded-lg border border-zinc-200 dark:border-zinc-800 shadow-sm",
81
+ title: "text-2xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
82
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
83
+ fieldsContainer: "space-y-4",
84
+ field: "w-full space-y-2",
85
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
86
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
87
+ control: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
88
+ textarea: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
89
+ select: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
90
+ option: "text-zinc-950 dark:text-zinc-50 bg-white dark:bg-zinc-950",
91
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-900 dark:border-zinc-50 bg-white dark:bg-zinc-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50",
92
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-900 dark:border-zinc-50 bg-white dark:bg-zinc-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-white dark:ring-offset-zinc-950 disabled:cursor-not-allowed disabled:opacity-50",
93
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
94
+ },
95
+ minimal: {
96
+ wrapper: "w-full max-w-2xl mx-auto",
97
+ title: "text-xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
98
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
99
+ fieldsContainer: "space-y-4",
100
+ field: "w-full space-y-2",
101
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
102
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
103
+ control: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
104
+ textarea: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
105
+ select: "w-full rounded-md border border-zinc-200 dark:border-zinc-800 bg-transparent text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
106
+ option: "text-zinc-950 dark:text-zinc-50",
107
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-800 bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
108
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-800 bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
109
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
110
+ },
111
+ filled: {
112
+ wrapper: "w-full max-w-2xl mx-auto p-6 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-100 dark:border-zinc-800",
113
+ title: "text-2xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
114
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
115
+ fieldsContainer: "space-y-4",
116
+ field: "w-full space-y-2",
117
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
118
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
119
+ control: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
120
+ textarea: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors min-h-[80px] resize-none",
121
+ select: "w-full rounded-md border border-zinc-100 dark:border-zinc-800 bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
122
+ option: "text-zinc-950 dark:text-zinc-50 bg-white dark:bg-zinc-900",
123
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50",
124
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 ring-offset-zinc-50 dark:ring-offset-zinc-900 disabled:cursor-not-allowed disabled:opacity-50",
125
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
126
+ },
127
+ underline: {
128
+ wrapper: "w-full max-w-2xl mx-auto py-4",
129
+ title: "text-xl font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
130
+ description: "text-sm text-zinc-500 dark:text-zinc-400",
131
+ fieldsContainer: "space-y-4",
132
+ field: "w-full space-y-2",
133
+ label: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50",
134
+ help: "text-[0.8rem] text-zinc-500 dark:text-zinc-400",
135
+ control: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 rounded-none focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-0",
136
+ textarea: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 rounded-none focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-0 min-h-[80px] resize-none",
137
+ select: "w-full border-0 border-b border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-950 dark:text-zinc-50 focus-visible:outline-none focus-visible:ring-0 focus-visible:border-zinc-950 dark:focus-visible:border-zinc-300 disabled:cursor-not-allowed disabled:opacity-50 transition-colors rounded-none px-0",
138
+ option: "text-zinc-950 dark:text-zinc-50",
139
+ checkbox: "h-4 w-4 shrink-0 rounded-sm border border-zinc-200 dark:border-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
140
+ radio: "h-4 w-4 shrink-0 rounded-full border border-zinc-200 dark:border-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 dark:focus-visible:ring-zinc-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
141
+ error: "text-[0.8rem] font-medium text-red-500 dark:text-red-400"
142
+ }
143
+ };
144
+ var sizeMap = {
145
+ sm: {
146
+ control: "px-3 py-1.5 text-xs",
147
+ label: "text-xs",
148
+ help: "text-[0.7rem]",
149
+ error: "text-xs"
150
+ },
151
+ md: {
152
+ control: "px-3 py-2 text-sm",
153
+ label: "text-sm",
154
+ help: "text-[0.8rem]",
155
+ error: "text-[0.8rem]"
156
+ },
157
+ lg: {
158
+ control: "px-4 py-2.5 text-base",
159
+ label: "text-base",
160
+ help: "text-sm",
161
+ error: "text-sm"
162
+ }
163
+ };
164
+ var DynamicFields = ({
165
+ fields,
166
+ onChange,
167
+ value,
168
+ defaultValues,
169
+ resolver,
170
+ validateMode = "change",
171
+ showErrorWhen = "touched",
172
+ headless = false,
173
+ theme = "default",
174
+ size = "md",
175
+ ui,
176
+ stateClassNames,
177
+ className = "",
178
+ inputClassName = "",
179
+ labelClassName = "",
180
+ mainFieldClassName = "",
181
+ fieldClassName = "",
182
+ errorClassName = "",
183
+ selectClassName = "",
184
+ optionClassName = "",
185
+ checkboxClassName = "",
186
+ radioClassName = "",
187
+ title,
188
+ description,
189
+ renderLabel,
190
+ renderDescription,
191
+ renderError,
192
+ renderControl,
193
+ renderField
194
+ }) => {
195
+ const isControlled = value !== void 0;
196
+ const initialValuesRef = useRef(buildInitialValues(fields, defaultValues, value));
197
+ const [internalValues, setInternalValues] = useState(initialValuesRef.current);
198
+ const [fieldErrors, setFieldErrors] = useState({});
199
+ const [resolverErrors, setResolverErrors] = useState({});
200
+ const [touchedMap, setTouchedMap] = useState({});
201
+ const [dirtyMap, setDirtyMap] = useState({});
202
+ const [loadedOptions, setLoadedOptions] = useState({});
203
+ const [loadingOptionsMap, setLoadingOptionsMap] = useState({});
204
+ const resolverRunId = useRef(0);
205
+ const values = isControlled ? value ?? EMPTY_OBJECT : internalValues;
206
+ useEffect(() => {
207
+ if (!isControlled) {
208
+ const rebuilt = buildInitialValues(fields, defaultValues, void 0);
209
+ initialValuesRef.current = rebuilt;
210
+ setInternalValues(rebuilt);
211
+ setDirtyMap({});
212
+ setTouchedMap({});
213
+ setFieldErrors({});
214
+ setResolverErrors({});
215
+ }
216
+ }, [fields, defaultValues, isControlled]);
217
+ const visibleFields = useMemo(
218
+ () => fields.filter((field) => field.showWhen ? field.showWhen(values) : true),
219
+ [fields, values]
220
+ );
221
+ const runFieldValidation = (field, nextValues) => {
222
+ const valueAtPath = getIn(nextValues, field.name);
223
+ const isRequired = field.requiredWhen ? field.requiredWhen(nextValues) : Boolean(field.required);
224
+ if (isRequired && isEmptyValue(valueAtPath)) {
225
+ return field.requiredMessage || "This field is required";
226
+ }
227
+ if (isEmptyValue(valueAtPath)) {
228
+ return void 0;
229
+ }
230
+ if (typeof valueAtPath === "number") {
231
+ if (typeof field.min === "number" && valueAtPath < field.min) {
232
+ return `Minimum value is ${field.min}`;
233
+ }
234
+ if (typeof field.max === "number" && valueAtPath > field.max) {
235
+ return `Maximum value is ${field.max}`;
236
+ }
237
+ }
238
+ if (typeof valueAtPath === "string") {
239
+ if (typeof field.minLength === "number" && valueAtPath.length < field.minLength) {
240
+ return `Minimum length is ${field.minLength}`;
241
+ }
242
+ if (typeof field.maxLength === "number" && valueAtPath.length > field.maxLength) {
243
+ return `Maximum length is ${field.maxLength}`;
244
+ }
245
+ if (field.pattern && !field.pattern.test(valueAtPath)) {
246
+ return field.patternMessage || "Invalid format";
247
+ }
248
+ }
249
+ if (field.validate) {
250
+ return field.validate(valueAtPath, nextValues) || void 0;
251
+ }
252
+ return void 0;
253
+ };
254
+ const validateVisibleFields = (nextValues) => {
255
+ const nextErrors = {};
256
+ visibleFields.forEach((field) => {
257
+ const error = runFieldValidation(field, nextValues);
258
+ if (error) {
259
+ nextErrors[field.name] = error;
260
+ }
261
+ });
262
+ return nextErrors;
263
+ };
264
+ useEffect(() => {
265
+ const fieldsToLoad = fields.filter((field) => field.type === "select" && field.loadOptions && field.loadOptionsOn !== "change");
266
+ if (!fieldsToLoad.length) {
267
+ return;
268
+ }
269
+ fieldsToLoad.forEach((field) => {
270
+ const fieldName = field.name;
271
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: true }));
272
+ field.loadOptions?.({ values, field }).then((result) => {
273
+ setLoadedOptions((previous) => ({ ...previous, [fieldName]: result }));
274
+ }).finally(() => {
275
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: false }));
276
+ });
277
+ });
278
+ }, [fields]);
279
+ useEffect(() => {
280
+ const fieldsToLoad = fields.filter((field) => field.type === "select" && field.loadOptions && field.loadOptionsOn === "change");
281
+ if (!fieldsToLoad.length) {
282
+ return;
283
+ }
284
+ fieldsToLoad.forEach((field) => {
285
+ const fieldName = field.name;
286
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: true }));
287
+ field.loadOptions?.({ values, field }).then((result) => {
288
+ setLoadedOptions((previous) => ({ ...previous, [fieldName]: result }));
289
+ }).finally(() => {
290
+ setLoadingOptionsMap((previous) => ({ ...previous, [fieldName]: false }));
291
+ });
292
+ });
293
+ }, [fields, values]);
294
+ useEffect(() => {
295
+ if (!resolver) {
296
+ setResolverErrors({});
297
+ return;
298
+ }
299
+ const runId = resolverRunId.current + 1;
300
+ resolverRunId.current = runId;
301
+ Promise.resolve(resolver(values)).then((errors) => {
302
+ if (resolverRunId.current === runId) {
303
+ setResolverErrors(errors ?? {});
304
+ }
305
+ }).catch(() => {
306
+ if (resolverRunId.current === runId) {
307
+ setResolverErrors({});
308
+ }
309
+ });
310
+ }, [resolver, values]);
311
+ const combinedErrors = useMemo(() => ({ ...fieldErrors, ...resolverErrors }), [fieldErrors, resolverErrors]);
312
+ useEffect(() => {
313
+ onChange(values, {
314
+ errors: combinedErrors,
315
+ touched: touchedMap,
316
+ dirty: dirtyMap
317
+ });
318
+ }, [values, combinedErrors, touchedMap, dirtyMap, onChange]);
319
+ const updateValues = (nextValues) => {
320
+ if (!isControlled) {
321
+ setInternalValues(nextValues);
322
+ }
323
+ if (validateMode === "change") {
324
+ setFieldErrors(validateVisibleFields(nextValues));
325
+ }
326
+ };
327
+ const handleFieldChange = (field, rawValue) => {
328
+ const transformedValue = field.transformOut ? field.transformOut(rawValue, values) : rawValue;
329
+ const nextValues = setIn(values, field.name, transformedValue);
330
+ const initialValue = getIn(initialValuesRef.current, field.name);
331
+ const isDirty = toComparable(initialValue) !== toComparable(transformedValue);
332
+ setDirtyMap((previous) => ({ ...previous, [field.name]: isDirty }));
333
+ updateValues(nextValues);
334
+ };
335
+ const handleFieldBlur = (field) => {
336
+ setTouchedMap((previous) => ({ ...previous, [field.name]: true }));
337
+ if (validateMode === "blur") {
338
+ const nextError = runFieldValidation(field, values);
339
+ setFieldErrors((previous) => {
340
+ const updated = { ...previous };
341
+ if (nextError) {
342
+ updated[field.name] = nextError;
343
+ } else {
344
+ delete updated[field.name];
345
+ }
346
+ return updated;
347
+ });
348
+ }
349
+ };
350
+ const defaultUi = headless ? {
351
+ wrapper: "",
352
+ title: "",
353
+ description: "",
354
+ fieldsContainer: "",
355
+ field: "",
356
+ label: "",
357
+ help: "",
358
+ control: "",
359
+ textarea: "",
360
+ select: "",
361
+ option: "",
362
+ checkbox: "",
363
+ radio: "",
364
+ error: ""
365
+ } : baseTheme[theme];
366
+ const sizeClasses = sizeMap[size];
367
+ const resolveUi = (field) => ({
368
+ wrapper: cn(defaultUi.wrapper, ui?.wrapper, className),
369
+ title: cn(defaultUi.title, ui?.title),
370
+ description: cn(defaultUi.description, ui?.description),
371
+ fieldsContainer: cn(defaultUi.fieldsContainer, ui?.fieldsContainer, mainFieldClassName),
372
+ field: cn(defaultUi.field, fieldClassName, field?.className, ui?.field, field?.ui?.field),
373
+ label: cn(defaultUi.label, sizeClasses.label, labelClassName, ui?.label, field?.labelClassName, field?.ui?.label),
374
+ help: cn(defaultUi.help, sizeClasses.help, ui?.help, field?.ui?.help),
375
+ control: cn(defaultUi.control, sizeClasses.control, inputClassName, ui?.control, field?.inputClassName, field?.ui?.control),
376
+ textarea: cn(defaultUi.textarea, sizeClasses.control, inputClassName, ui?.textarea, field?.inputClassName, field?.ui?.textarea),
377
+ select: cn(defaultUi.select, sizeClasses.control, selectClassName, ui?.select, field?.ui?.select),
378
+ option: cn(defaultUi.option, optionClassName, ui?.option, field?.ui?.option),
379
+ checkbox: cn(defaultUi.checkbox, checkboxClassName, ui?.checkbox, field?.ui?.checkbox),
380
+ radio: cn(defaultUi.radio, radioClassName, ui?.radio, field?.ui?.radio),
381
+ error: cn(defaultUi.error, sizeClasses.error, errorClassName, field?.errorClassName, ui?.error, field?.ui?.error)
382
+ });
383
+ const buildStateClassName = (field, hasError, disabled, readOnly) => cn(
384
+ hasError && (stateClassNames?.invalid ?? "border-red-500 dark:border-red-500 focus-visible:ring-red-500 dark:focus-visible:ring-red-500"),
385
+ disabled && stateClassNames?.disabled,
386
+ readOnly && stateClassNames?.readonly,
387
+ dirtyMap[field.name] && stateClassNames?.dirty,
388
+ touchedMap[field.name] && stateClassNames?.touched
389
+ );
390
+ const shouldShowError = (fieldName) => {
391
+ if (showErrorWhen === "always") {
392
+ return true;
393
+ }
394
+ if (showErrorWhen === "dirty") {
395
+ return Boolean(dirtyMap[fieldName]);
396
+ }
397
+ return Boolean(touchedMap[fieldName]);
398
+ };
399
+ const renderDefaultControl = (context) => {
400
+ const { field, value: value2, options, isLoadingOptions, classes, handleChange, handleBlur, disabled, readOnly, error } = context;
401
+ const showsError = Boolean(error) && shouldShowError(field.name);
402
+ const controlStateClass = buildStateClassName(field, showsError, disabled, readOnly);
403
+ if (field.type === "textarea") {
404
+ return /* @__PURE__ */ React.createElement(
405
+ "textarea",
406
+ {
407
+ id: field.name,
408
+ name: field.name,
409
+ value: value2 ?? "",
410
+ placeholder: field.placeholder,
411
+ onChange: (event) => handleChange(event.target.value),
412
+ onBlur: handleBlur,
413
+ disabled,
414
+ readOnly,
415
+ className: cn(classes.textarea, controlStateClass)
416
+ }
417
+ );
418
+ }
419
+ if (field.type === "select") {
420
+ return /* @__PURE__ */ React.createElement(
421
+ "select",
422
+ {
423
+ id: field.name,
424
+ name: field.name,
425
+ value: value2 ?? "",
426
+ onChange: (event) => handleChange(event.target.value),
427
+ onBlur: handleBlur,
428
+ disabled: disabled || isLoadingOptions,
429
+ className: cn(classes.select, controlStateClass)
430
+ },
431
+ /* @__PURE__ */ React.createElement("option", { value: "", className: classes.option }, isLoadingOptions ? "Loading options..." : field.placeholder || "Select an option"),
432
+ options.map((option) => /* @__PURE__ */ React.createElement(
433
+ "option",
434
+ {
435
+ key: `${field.name}-${String(option.value)}`,
436
+ value: String(option.value ?? ""),
437
+ disabled: option.disabled,
438
+ className: classes.option
439
+ },
440
+ option.label
441
+ ))
442
+ );
443
+ }
444
+ if (field.type === "radio") {
445
+ return /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3" }, options.map((option) => /* @__PURE__ */ React.createElement(
446
+ "label",
447
+ {
448
+ key: `${field.name}-${String(option.value)}`,
449
+ htmlFor: `${field.name}-${String(option.value)}`,
450
+ className: "flex items-center gap-2 cursor-pointer select-none"
451
+ },
452
+ /* @__PURE__ */ React.createElement(
453
+ "input",
454
+ {
455
+ id: `${field.name}-${String(option.value)}`,
456
+ type: "radio",
457
+ name: field.name,
458
+ value: String(option.value ?? ""),
459
+ checked: toComparable(value2) === toComparable(option.value),
460
+ onChange: () => handleChange(option.value),
461
+ onBlur: handleBlur,
462
+ disabled: disabled || option.disabled,
463
+ readOnly,
464
+ className: cn(classes.radio, controlStateClass)
465
+ }
466
+ ),
467
+ /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70" }, option.label)
468
+ )));
469
+ }
470
+ if (field.type === "checkbox") {
471
+ return /* @__PURE__ */ React.createElement(
472
+ "input",
473
+ {
474
+ id: field.name,
475
+ name: field.name,
476
+ type: "checkbox",
477
+ checked: Boolean(value2),
478
+ onChange: (event) => handleChange(event.target.checked),
479
+ onBlur: handleBlur,
480
+ disabled,
481
+ readOnly,
482
+ className: cn(classes.checkbox, controlStateClass)
483
+ }
484
+ );
485
+ }
486
+ return /* @__PURE__ */ React.createElement(
487
+ "input",
488
+ {
489
+ id: field.name,
490
+ name: field.name,
491
+ type: field.type,
492
+ value: value2 ?? "",
493
+ placeholder: field.placeholder,
494
+ onChange: (event) => {
495
+ if (field.type === "number") {
496
+ const nextValue = event.target.value === "" ? "" : Number(event.target.value);
497
+ handleChange(Number.isNaN(nextValue) ? "" : nextValue);
498
+ return;
499
+ }
500
+ handleChange(event.target.value);
501
+ },
502
+ onBlur: handleBlur,
503
+ disabled,
504
+ readOnly,
505
+ className: cn(classes.control, controlStateClass)
506
+ }
507
+ );
508
+ };
509
+ return /* @__PURE__ */ React.createElement("div", { className: resolveUi().wrapper }, title && /* @__PURE__ */ React.createElement("div", { className: "flex flex-col space-y-1.5 pb-6 mb-6 border-b border-zinc-200 dark:border-zinc-800" }, /* @__PURE__ */ React.createElement("h2", { className: resolveUi().title }, title), description && /* @__PURE__ */ React.createElement("p", { className: resolveUi().description }, description)), /* @__PURE__ */ React.createElement("div", { className: resolveUi().fieldsContainer }, visibleFields.map((field) => {
510
+ const classes = resolveUi(field);
511
+ const storedValue = getIn(values, field.name);
512
+ const renderedValue = field.transformIn ? field.transformIn(storedValue, values) : storedValue;
513
+ const options = loadedOptions[field.name] ?? field.options ?? [];
514
+ const isLoadingOptions = Boolean(loadingOptionsMap[field.name]);
515
+ const disabled = field.disableWhen ? field.disableWhen(values) : Boolean(field.disabled);
516
+ const readOnly = Boolean(field.readOnly);
517
+ const error = combinedErrors[field.name];
518
+ const canShowError = Boolean(error) && shouldShowError(field.name);
519
+ const isTouched = Boolean(touchedMap[field.name]);
520
+ const isDirty = Boolean(dirtyMap[field.name]);
521
+ const context = {
522
+ field,
523
+ value: renderedValue,
524
+ values,
525
+ error,
526
+ isTouched,
527
+ isDirty,
528
+ disabled,
529
+ readOnly,
530
+ options,
531
+ isLoadingOptions,
532
+ classes,
533
+ handleChange: (nextValue) => handleFieldChange(field, nextValue),
534
+ handleBlur: () => handleFieldBlur(field)
535
+ };
536
+ const requiredMark = field.required || field.requiredWhen?.(values);
537
+ const labelNode = field.type === "checkbox" ? null : renderLabel ? renderLabel(context) : field.label ? /* @__PURE__ */ React.createElement("label", { htmlFor: field.name, className: classes.label }, field.label, requiredMark && /* @__PURE__ */ React.createElement("span", { className: "text-red-500 ml-1" }, "*")) : null;
538
+ const helpNode = renderDescription ? renderDescription(context) : field.description ? /* @__PURE__ */ React.createElement("p", { className: classes.help }, field.description) : null;
539
+ const errorNode = canShowError ? renderError ? renderError(context) : /* @__PURE__ */ React.createElement("p", { className: classes.error, role: "alert" }, error) : null;
540
+ const controlNode = field.renderControl?.(context) ?? renderControl?.(context) ?? renderDefaultControl(context);
541
+ if (renderField) {
542
+ return /* @__PURE__ */ React.createElement(React.Fragment, { key: field.name }, renderField({
543
+ ...context,
544
+ labelNode,
545
+ helpNode,
546
+ errorNode,
547
+ controlNode
548
+ }));
549
+ }
550
+ if (field.type === "checkbox") {
551
+ return /* @__PURE__ */ React.createElement("div", { key: field.name, className: classes.field }, /* @__PURE__ */ React.createElement("label", { htmlFor: field.name, className: "inline-flex items-start gap-2 cursor-pointer select-none" }, controlNode, /* @__PURE__ */ React.createElement("span", { className: "text-sm font-medium leading-none text-zinc-950 dark:text-zinc-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-70" }, field.label, requiredMark && /* @__PURE__ */ React.createElement("span", { className: "text-red-500 ml-1" }, "*"))), helpNode, errorNode);
552
+ }
553
+ return /* @__PURE__ */ React.createElement("div", { key: field.name, className: classes.field }, labelNode, helpNode, controlNode, errorNode);
554
+ })));
555
+ };
556
+ export {
557
+ DynamicFields
558
+ };
package/package.json CHANGED
@@ -1,23 +1,38 @@
1
1
  {
2
2
  "name": "react-smart-fields",
3
- "version": "1.1.6",
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.",
3
+ "version": "2.2.0",
4
+ "description": "A flexible and highly customizable dynamic form builder for React with validation, conditional fields, theming, and headless rendering.",
5
5
  "license": "MIT",
6
6
  "author": "Pratik Panchal",
7
- "type": "commonjs",
8
- "main": "dist/index.js",
7
+ "type": "module",
8
+ "main": "dist/index.cjs",
9
+ "module": "dist/index.js",
9
10
  "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "sideEffects": false,
10
22
  "scripts": {
11
- "build": "tsc"
23
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
24
+ "build:types": "tsc --emitDeclarationOnly"
12
25
  },
13
26
  "repository": {
14
27
  "type": "git",
15
- "url": "https://github.com/Pratikpanchal25/react-smart-fields"
28
+ "url": "git+https://github.com/Pratikpanchal25/react-smart-fields.git"
16
29
  },
17
30
  "keywords": [
18
31
  "DynamicFields",
19
32
  "React form generator",
20
33
  "dynamic form fields",
34
+ "headless form",
35
+ "react field renderer",
21
36
  "custom input components",
22
37
  "Tailwind CSS forms",
23
38
  "form validation react",
@@ -30,17 +45,13 @@
30
45
  "override styles",
31
46
  "react component"
32
47
  ],
33
- "dependencies": {
34
- "csstype": "^3.1.3",
35
- "js-tokens": "^4.0.0",
36
- "loose-envify": "^1.4.0",
37
- "react": "^18.3.1"
38
- },
48
+ "dependencies": {},
39
49
  "devDependencies": {
50
+ "@types/react": "^18.0.0",
40
51
  "typescript": "^5.0.0",
41
- "@types/react": "^18.0.0"
52
+ "tsup": "^8.1.0"
42
53
  },
43
54
  "peerDependencies": {
44
- "react": "^18.0.0"
55
+ "react": "^18.0.0 || ^19.0.0"
45
56
  }
46
57
  }
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto
@@ -1,31 +0,0 @@
1
- import React from "react";
2
- export interface FieldOption {
3
- label: string;
4
- value: string | number | boolean;
5
- }
6
- export interface FieldConfig {
7
- name: string;
8
- label?: string;
9
- type: "text" | "number" | "select" | "radio" | "checkbox";
10
- options?: FieldOption[];
11
- required?: boolean;
12
- placeholder?: string;
13
- description?: string;
14
- }
15
- export interface DynamicFieldsProps {
16
- fields: FieldConfig[];
17
- onChange: (data: Record<string, any>) => void;
18
- className?: string;
19
- inputClassName?: string;
20
- labelClassName?: string;
21
- mainFieldClassName?: string;
22
- fieldClassName?: string;
23
- errorClassName?: string;
24
- selectClassName?: string;
25
- optionClassName?: string;
26
- checkboxClassName?: string;
27
- radioClassName?: string;
28
- title?: string;
29
- description?: string;
30
- }
31
- export declare const DynamicFields: React.FC<DynamicFieldsProps>;