react-smart-fields 1.1.5 → 2.1.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/README.md +218 -130
- package/dist/index.cjs +594 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -2
- package/dist/index.js +557 -1
- package/package.json +25 -14
- package/.gitattributes +0 -2
- package/dist/components/DynamicFields.d.ts +0 -31
- package/dist/components/DynamicFields.js +0 -97
- package/src/components/DynamicFields.tsx +0 -259
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -13
package/dist/index.js
CHANGED
|
@@ -1 +1,557 @@
|
|
|
1
|
-
|
|
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-gray-900 text-gray-800 dark:text-gray-200 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm dark:shadow-2xl",
|
|
81
|
+
title: "text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100",
|
|
82
|
+
description: "text-sm text-gray-500 dark:text-gray-400",
|
|
83
|
+
fieldsContainer: "space-y-3",
|
|
84
|
+
field: "w-full",
|
|
85
|
+
label: "flex text-sm font-medium text-gray-800 dark:text-gray-200 mb-1.5",
|
|
86
|
+
help: "text-xs text-gray-500 dark:text-gray-400",
|
|
87
|
+
control: "w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors",
|
|
88
|
+
textarea: "w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors min-h-24",
|
|
89
|
+
select: "w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors",
|
|
90
|
+
option: "text-gray-800 dark:text-gray-200",
|
|
91
|
+
checkbox: "h-4 w-4 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none",
|
|
92
|
+
radio: "h-4 w-4 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none",
|
|
93
|
+
error: "text-sm 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 text-gray-900 dark:text-gray-100",
|
|
98
|
+
description: "text-sm text-gray-500 dark:text-gray-400",
|
|
99
|
+
fieldsContainer: "space-y-3",
|
|
100
|
+
field: "w-full",
|
|
101
|
+
label: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1",
|
|
102
|
+
help: "text-xs text-gray-500 dark:text-gray-400",
|
|
103
|
+
control: "w-full rounded-md border border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
104
|
+
textarea: "w-full rounded-md border border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-24",
|
|
105
|
+
select: "w-full rounded-md border border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
106
|
+
option: "text-gray-900 dark:text-gray-100",
|
|
107
|
+
checkbox: "h-4 w-4 rounded border border-gray-300 dark:border-gray-600 bg-transparent focus:outline-none",
|
|
108
|
+
radio: "h-4 w-4 rounded-full border border-gray-300 dark:border-gray-600 bg-transparent focus:outline-none",
|
|
109
|
+
error: "text-sm text-red-500"
|
|
110
|
+
},
|
|
111
|
+
filled: {
|
|
112
|
+
wrapper: "w-full max-w-2xl mx-auto p-6 bg-gray-50 dark:bg-gray-900/50 text-gray-800 dark:text-gray-200 rounded-xl border border-gray-100 dark:border-gray-800",
|
|
113
|
+
title: "text-2xl font-semibold text-gray-900 dark:text-gray-100",
|
|
114
|
+
description: "text-sm text-gray-500 dark:text-gray-400",
|
|
115
|
+
fieldsContainer: "space-y-3",
|
|
116
|
+
field: "w-full",
|
|
117
|
+
label: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5",
|
|
118
|
+
help: "text-xs text-gray-500 dark:text-gray-400",
|
|
119
|
+
control: "w-full rounded-lg border border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
120
|
+
textarea: "w-full rounded-lg border border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-24",
|
|
121
|
+
select: "w-full rounded-lg border border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
122
|
+
option: "text-gray-900 dark:text-gray-100",
|
|
123
|
+
checkbox: "h-4 w-4 rounded border border-transparent bg-white dark:bg-gray-800 focus:outline-none",
|
|
124
|
+
radio: "h-4 w-4 rounded-full border border-transparent bg-white dark:bg-gray-800 focus:outline-none",
|
|
125
|
+
error: "text-sm text-red-500"
|
|
126
|
+
},
|
|
127
|
+
underline: {
|
|
128
|
+
wrapper: "w-full max-w-2xl mx-auto p-4",
|
|
129
|
+
title: "text-xl font-semibold text-gray-900 dark:text-gray-100",
|
|
130
|
+
description: "text-sm text-gray-500 dark:text-gray-400",
|
|
131
|
+
fieldsContainer: "space-y-4",
|
|
132
|
+
field: "w-full",
|
|
133
|
+
label: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1",
|
|
134
|
+
help: "text-xs text-gray-500 dark:text-gray-400",
|
|
135
|
+
control: "w-full border-0 border-b border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 rounded-none focus:outline-none focus:ring-0 focus:border-blue-500",
|
|
136
|
+
textarea: "w-full border-0 border-b border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 rounded-none focus:outline-none focus:ring-0 focus:border-blue-500 min-h-24",
|
|
137
|
+
select: "w-full border-0 border-b border-gray-300 dark:border-gray-600 bg-transparent text-gray-900 dark:text-gray-100 rounded-none focus:outline-none focus:ring-0 focus:border-blue-500",
|
|
138
|
+
option: "text-gray-900 dark:text-gray-100",
|
|
139
|
+
checkbox: "h-4 w-4 rounded border border-gray-300 dark:border-gray-600",
|
|
140
|
+
radio: "h-4 w-4 rounded-full border border-gray-300 dark:border-gray-600",
|
|
141
|
+
error: "text-sm text-red-500"
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var sizeMap = {
|
|
145
|
+
sm: {
|
|
146
|
+
control: "px-3 py-1.5 text-xs",
|
|
147
|
+
label: "text-xs",
|
|
148
|
+
help: "text-[11px]",
|
|
149
|
+
error: "text-xs"
|
|
150
|
+
},
|
|
151
|
+
md: {
|
|
152
|
+
control: "px-3.5 py-2 text-sm",
|
|
153
|
+
label: "text-sm",
|
|
154
|
+
help: "text-xs",
|
|
155
|
+
error: "text-sm"
|
|
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,
|
|
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 controlStateClass = buildStateClassName(field, Boolean(error), disabled, readOnly);
|
|
402
|
+
if (field.type === "textarea") {
|
|
403
|
+
return /* @__PURE__ */ React.createElement(
|
|
404
|
+
"textarea",
|
|
405
|
+
{
|
|
406
|
+
id: field.name,
|
|
407
|
+
name: field.name,
|
|
408
|
+
value: value2 ?? "",
|
|
409
|
+
placeholder: field.placeholder,
|
|
410
|
+
onChange: (event) => handleChange(event.target.value),
|
|
411
|
+
onBlur: handleBlur,
|
|
412
|
+
disabled,
|
|
413
|
+
readOnly,
|
|
414
|
+
className: cn(classes.textarea, controlStateClass)
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (field.type === "select") {
|
|
419
|
+
return /* @__PURE__ */ React.createElement(
|
|
420
|
+
"select",
|
|
421
|
+
{
|
|
422
|
+
id: field.name,
|
|
423
|
+
name: field.name,
|
|
424
|
+
value: value2 ?? "",
|
|
425
|
+
onChange: (event) => handleChange(event.target.value),
|
|
426
|
+
onBlur: handleBlur,
|
|
427
|
+
disabled: disabled || isLoadingOptions,
|
|
428
|
+
className: cn(classes.select, controlStateClass)
|
|
429
|
+
},
|
|
430
|
+
/* @__PURE__ */ React.createElement("option", { value: "", className: classes.option }, isLoadingOptions ? "Loading options..." : field.placeholder || "Select an option"),
|
|
431
|
+
options.map((option) => /* @__PURE__ */ React.createElement(
|
|
432
|
+
"option",
|
|
433
|
+
{
|
|
434
|
+
key: `${field.name}-${String(option.value)}`,
|
|
435
|
+
value: String(option.value ?? ""),
|
|
436
|
+
disabled: option.disabled,
|
|
437
|
+
className: classes.option
|
|
438
|
+
},
|
|
439
|
+
option.label
|
|
440
|
+
))
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (field.type === "radio") {
|
|
444
|
+
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(
|
|
445
|
+
"label",
|
|
446
|
+
{
|
|
447
|
+
key: `${field.name}-${String(option.value)}`,
|
|
448
|
+
htmlFor: `${field.name}-${String(option.value)}`,
|
|
449
|
+
className: "flex items-center gap-2 cursor-pointer"
|
|
450
|
+
},
|
|
451
|
+
/* @__PURE__ */ React.createElement(
|
|
452
|
+
"input",
|
|
453
|
+
{
|
|
454
|
+
id: `${field.name}-${String(option.value)}`,
|
|
455
|
+
type: "radio",
|
|
456
|
+
name: field.name,
|
|
457
|
+
value: String(option.value ?? ""),
|
|
458
|
+
checked: toComparable(value2) === toComparable(option.value),
|
|
459
|
+
onChange: () => handleChange(option.value),
|
|
460
|
+
onBlur: handleBlur,
|
|
461
|
+
disabled: disabled || option.disabled,
|
|
462
|
+
readOnly,
|
|
463
|
+
className: cn(classes.radio, controlStateClass)
|
|
464
|
+
}
|
|
465
|
+
),
|
|
466
|
+
/* @__PURE__ */ React.createElement("span", { className: "text-sm text-gray-800 dark:text-gray-200" }, option.label)
|
|
467
|
+
)));
|
|
468
|
+
}
|
|
469
|
+
if (field.type === "checkbox") {
|
|
470
|
+
return /* @__PURE__ */ React.createElement(
|
|
471
|
+
"input",
|
|
472
|
+
{
|
|
473
|
+
id: field.name,
|
|
474
|
+
name: field.name,
|
|
475
|
+
type: "checkbox",
|
|
476
|
+
checked: Boolean(value2),
|
|
477
|
+
onChange: (event) => handleChange(event.target.checked),
|
|
478
|
+
onBlur: handleBlur,
|
|
479
|
+
disabled,
|
|
480
|
+
readOnly,
|
|
481
|
+
className: cn(classes.checkbox, controlStateClass)
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return /* @__PURE__ */ React.createElement(
|
|
486
|
+
"input",
|
|
487
|
+
{
|
|
488
|
+
id: field.name,
|
|
489
|
+
name: field.name,
|
|
490
|
+
type: field.type,
|
|
491
|
+
value: value2 ?? "",
|
|
492
|
+
placeholder: field.placeholder,
|
|
493
|
+
onChange: (event) => {
|
|
494
|
+
if (field.type === "number") {
|
|
495
|
+
const nextValue = event.target.value === "" ? "" : Number(event.target.value);
|
|
496
|
+
handleChange(Number.isNaN(nextValue) ? "" : nextValue);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
handleChange(event.target.value);
|
|
500
|
+
},
|
|
501
|
+
onBlur: handleBlur,
|
|
502
|
+
disabled,
|
|
503
|
+
readOnly,
|
|
504
|
+
className: cn(classes.control, controlStateClass)
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
};
|
|
508
|
+
return /* @__PURE__ */ React.createElement("div", { className: resolveUi().wrapper }, title && /* @__PURE__ */ React.createElement("div", { className: "mb-4" }, /* @__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) => {
|
|
509
|
+
const classes = resolveUi(field);
|
|
510
|
+
const storedValue = getIn(values, field.name);
|
|
511
|
+
const renderedValue = field.transformIn ? field.transformIn(storedValue, values) : storedValue;
|
|
512
|
+
const options = loadedOptions[field.name] ?? field.options ?? [];
|
|
513
|
+
const isLoadingOptions = Boolean(loadingOptionsMap[field.name]);
|
|
514
|
+
const disabled = field.disableWhen ? field.disableWhen(values) : Boolean(field.disabled);
|
|
515
|
+
const readOnly = Boolean(field.readOnly);
|
|
516
|
+
const error = combinedErrors[field.name];
|
|
517
|
+
const canShowError = Boolean(error) && shouldShowError(field.name);
|
|
518
|
+
const isTouched = Boolean(touchedMap[field.name]);
|
|
519
|
+
const isDirty = Boolean(dirtyMap[field.name]);
|
|
520
|
+
const context = {
|
|
521
|
+
field,
|
|
522
|
+
value: renderedValue,
|
|
523
|
+
values,
|
|
524
|
+
error,
|
|
525
|
+
isTouched,
|
|
526
|
+
isDirty,
|
|
527
|
+
disabled,
|
|
528
|
+
readOnly,
|
|
529
|
+
options,
|
|
530
|
+
isLoadingOptions,
|
|
531
|
+
classes,
|
|
532
|
+
handleChange: (nextValue) => handleFieldChange(field, nextValue),
|
|
533
|
+
handleBlur: () => handleFieldBlur(field)
|
|
534
|
+
};
|
|
535
|
+
const requiredMark = field.required || field.requiredWhen?.(values);
|
|
536
|
+
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 dark:text-red-400 ml-1" }, "*")) : null;
|
|
537
|
+
const helpNode = renderDescription ? renderDescription(context) : field.description ? /* @__PURE__ */ React.createElement("p", { className: classes.help }, field.description) : null;
|
|
538
|
+
const errorNode = canShowError ? renderError ? renderError(context) : /* @__PURE__ */ React.createElement("p", { className: classes.error, role: "alert" }, error) : null;
|
|
539
|
+
const controlNode = field.renderControl?.(context) ?? renderControl?.(context) ?? renderDefaultControl(context);
|
|
540
|
+
if (renderField) {
|
|
541
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, { key: field.name }, renderField({
|
|
542
|
+
...context,
|
|
543
|
+
labelNode,
|
|
544
|
+
helpNode,
|
|
545
|
+
errorNode,
|
|
546
|
+
controlNode
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
if (field.type === "checkbox") {
|
|
550
|
+
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" }, controlNode, /* @__PURE__ */ React.createElement("span", { className: "text-sm text-gray-800 dark:text-gray-200" }, field.label, requiredMark && /* @__PURE__ */ React.createElement("span", { className: "text-red-500 dark:text-red-400 ml-1" }, "*"))), helpNode, errorNode);
|
|
551
|
+
}
|
|
552
|
+
return /* @__PURE__ */ React.createElement("div", { key: field.name, className: classes.field }, labelNode, helpNode, controlNode, errorNode);
|
|
553
|
+
})));
|
|
554
|
+
};
|
|
555
|
+
export {
|
|
556
|
+
DynamicFields
|
|
557
|
+
};
|
package/package.json
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-smart-fields",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.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": "
|
|
8
|
-
"main": "dist/index.
|
|
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": "
|
|
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
|
-
"
|
|
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,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>;
|