pdyform 2.0.1 → 2.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 +1 -1
- package/packages/core/dist/chunk-TP3IHKWV.js +69 -0
- package/packages/core/dist/{chunk-GQASS6PM.js → chunk-WEDHXOHH.js} +22 -0
- package/packages/core/dist/formState.cjs +187 -0
- package/packages/core/dist/formState.d.cts +17 -0
- package/packages/core/dist/formState.d.ts +17 -0
- package/packages/core/dist/formState.js +15 -0
- package/packages/core/dist/index.cjs +92 -2
- package/packages/core/dist/index.d.cts +2 -1
- package/packages/core/dist/index.d.ts +2 -1
- package/packages/core/dist/index.js +21 -3
- package/packages/core/dist/utils.cjs +27 -2
- package/packages/core/dist/utils.d.cts +4 -1
- package/packages/core/dist/utils.d.ts +4 -1
- package/packages/core/dist/utils.js +9 -3
- package/packages/core/node_modules/.vite/vitest/results.json +1 -1
- package/packages/core/src/formState.ts +79 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/utils.ts +24 -0
- package/packages/core/test/formState.test.ts +71 -0
- package/packages/core/test/utils.test.ts +80 -1
- package/packages/core/tsup.config.ts +1 -1
- package/packages/react/dist/index.cjs +14 -43
- package/packages/react/dist/index.js +20 -43
- package/packages/react/node_modules/.vite/vitest/results.json +1 -1
- package/packages/react/src/DynamicForm.tsx +20 -44
- package/packages/react/src/components/InputRenderer.tsx +2 -6
- package/packages/vue/dist/index.js +5 -5
- package/packages/vue/dist/index.mjs +769 -787
- package/packages/vue/node_modules/.vite/vitest/results.json +1 -1
- package/packages/vue/src/DynamicForm.vue +18 -36
- package/packages/vue/src/components/InputRenderer.vue +2 -6
- package/packages/core/dist/parser.cjs +0 -1
- package/packages/core/dist/parser.d.cts +0 -2
- package/packages/core/dist/parser.d.ts +0 -2
- package/packages/core/dist/parser.js +0 -0
- package/packages/core/src/parser.ts +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { FormField } from './types';
|
|
2
|
+
import { getDefaultValues, normalizeFieldValue, validateFieldByName, validateForm } from './utils';
|
|
3
|
+
|
|
4
|
+
export interface FormRuntimeState {
|
|
5
|
+
values: Record<string, any>;
|
|
6
|
+
errors: Record<string, string>;
|
|
7
|
+
isSubmitting: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createFormRuntimeState(fields: FormField[]): FormRuntimeState {
|
|
11
|
+
return {
|
|
12
|
+
values: getDefaultValues(fields),
|
|
13
|
+
errors: {},
|
|
14
|
+
isSubmitting: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setSubmitting(state: FormRuntimeState, isSubmitting: boolean): FormRuntimeState {
|
|
19
|
+
return {
|
|
20
|
+
...state,
|
|
21
|
+
isSubmitting,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function applyFieldChange(
|
|
26
|
+
fields: FormField[],
|
|
27
|
+
state: FormRuntimeState,
|
|
28
|
+
name: string,
|
|
29
|
+
rawValue: unknown
|
|
30
|
+
): FormRuntimeState {
|
|
31
|
+
const field = fields.find((f) => f.name === name);
|
|
32
|
+
const normalizedValue = field ? normalizeFieldValue(field, rawValue) : rawValue;
|
|
33
|
+
|
|
34
|
+
const values = {
|
|
35
|
+
...state.values,
|
|
36
|
+
[name]: normalizedValue,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const error = validateFieldByName(fields, name, normalizedValue);
|
|
40
|
+
const errors = {
|
|
41
|
+
...state.errors,
|
|
42
|
+
[name]: error || '',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
values,
|
|
48
|
+
errors,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function applyFieldBlur(fields: FormField[], state: FormRuntimeState, name: string): FormRuntimeState {
|
|
53
|
+
const error = validateFieldByName(fields, name, state.values[name]);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...state,
|
|
57
|
+
errors: {
|
|
58
|
+
...state.errors,
|
|
59
|
+
[name]: error || '',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function runSubmitValidation(
|
|
65
|
+
fields: FormField[],
|
|
66
|
+
state: FormRuntimeState
|
|
67
|
+
): { state: FormRuntimeState; hasError: boolean } {
|
|
68
|
+
const errors = validateForm(fields, state.values);
|
|
69
|
+
const hasError = Object.keys(errors).length > 0;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
state: {
|
|
73
|
+
...state,
|
|
74
|
+
errors,
|
|
75
|
+
isSubmitting: false,
|
|
76
|
+
},
|
|
77
|
+
hasError,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -7,6 +7,15 @@ function parseNumberish(value: unknown): number | null {
|
|
|
7
7
|
return Number.isNaN(parsed) ? null : parsed;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function normalizeFieldValue(field: FormField, value: unknown): unknown {
|
|
11
|
+
if (field.type !== 'number') return value;
|
|
12
|
+
|
|
13
|
+
if (value === '' || value === undefined || value === null) return '';
|
|
14
|
+
|
|
15
|
+
const numericValue = parseNumberish(value);
|
|
16
|
+
return numericValue === null ? value : numericValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export function validateField(value: any, field: FormField): string | null {
|
|
11
20
|
if (!field.validations) return null;
|
|
12
21
|
|
|
@@ -72,6 +81,21 @@ export function validateField(value: any, field: FormField): string | null {
|
|
|
72
81
|
return null;
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
export function validateFieldByName(fields: FormField[], name: string, value: unknown): string | null {
|
|
85
|
+
const field = fields.find((f) => f.name === name);
|
|
86
|
+
if (!field) return null;
|
|
87
|
+
return validateField(value, field);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateForm(fields: FormField[], values: Record<string, any>): Record<string, string> {
|
|
91
|
+
const errors: Record<string, string> = {};
|
|
92
|
+
for (const field of fields) {
|
|
93
|
+
const error = validateField(values[field.name], field);
|
|
94
|
+
if (error) errors[field.name] = error;
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
|
|
75
99
|
export function getDefaultValues(fields: FormField[]): Record<string, any> {
|
|
76
100
|
return fields.reduce((acc, field) => {
|
|
77
101
|
acc[field.name] = field.defaultValue !== undefined ? field.defaultValue : (field.type === 'checkbox' ? [] : '');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createFormRuntimeState,
|
|
4
|
+
setSubmitting,
|
|
5
|
+
applyFieldChange,
|
|
6
|
+
applyFieldBlur,
|
|
7
|
+
runSubmitValidation,
|
|
8
|
+
} from '../src';
|
|
9
|
+
import type { FormField } from '../src/types';
|
|
10
|
+
|
|
11
|
+
const fields: FormField[] = [
|
|
12
|
+
{
|
|
13
|
+
id: 'age',
|
|
14
|
+
name: 'age',
|
|
15
|
+
label: 'Age',
|
|
16
|
+
type: 'number',
|
|
17
|
+
validations: [{ type: 'min', value: 18, message: 'Age must be at least 18' }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'name',
|
|
21
|
+
name: 'name',
|
|
22
|
+
label: 'Name',
|
|
23
|
+
type: 'text',
|
|
24
|
+
validations: [{ type: 'required', message: 'Name is required' }],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
describe('core formState helpers', () => {
|
|
29
|
+
it('creates initial runtime state', () => {
|
|
30
|
+
const state = createFormRuntimeState(fields);
|
|
31
|
+
expect(state).toEqual({
|
|
32
|
+
values: { age: '', name: '' },
|
|
33
|
+
errors: {},
|
|
34
|
+
isSubmitting: false,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sets submitting state', () => {
|
|
39
|
+
const state = createFormRuntimeState(fields);
|
|
40
|
+
expect(setSubmitting(state, true).isSubmitting).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('applies field change with normalization and inline validation', () => {
|
|
44
|
+
const state = createFormRuntimeState(fields);
|
|
45
|
+
const next = applyFieldChange(fields, state, 'age', '22');
|
|
46
|
+
|
|
47
|
+
expect(next.values.age).toBe(22);
|
|
48
|
+
expect(next.errors.age).toBe('');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('applies blur validation', () => {
|
|
52
|
+
const state = createFormRuntimeState(fields);
|
|
53
|
+
const withInvalidName = applyFieldChange(fields, state, 'name', '');
|
|
54
|
+
const onBlur = applyFieldBlur(fields, withInvalidName, 'name');
|
|
55
|
+
|
|
56
|
+
expect(onBlur.errors.name).toBe('Name is required');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('runs submit validation and returns hasError flag', () => {
|
|
60
|
+
const state = createFormRuntimeState(fields);
|
|
61
|
+
const dirty = applyFieldChange(fields, state, 'age', '16');
|
|
62
|
+
const submitting = setSubmitting(dirty, true);
|
|
63
|
+
|
|
64
|
+
const result = runSubmitValidation(fields, submitting);
|
|
65
|
+
|
|
66
|
+
expect(result.hasError).toBe(true);
|
|
67
|
+
expect(result.state.isSubmitting).toBe(false);
|
|
68
|
+
expect(result.state.errors.age).toBe('Age must be at least 18');
|
|
69
|
+
expect(result.state.errors.name).toBe('Name is required');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateField, getDefaultValues } from '../src';
|
|
2
|
+
import { validateField, validateFieldByName, validateForm, normalizeFieldValue, getDefaultValues } from '../src';
|
|
3
3
|
import { FormField } from '../src/types';
|
|
4
4
|
|
|
5
5
|
describe('core utils - validateField', () => {
|
|
@@ -78,6 +78,85 @@ describe('core utils - validateField', () => {
|
|
|
78
78
|
expect(validateField('magic', field)).toBeNull();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('rule defaults and edge cases', () => {
|
|
83
|
+
it('uses default required message when custom message is absent', () => {
|
|
84
|
+
const field: FormField = {
|
|
85
|
+
name: 'title',
|
|
86
|
+
label: 'Title',
|
|
87
|
+
type: 'text',
|
|
88
|
+
validations: [{ type: 'required' }],
|
|
89
|
+
};
|
|
90
|
+
expect(validateField('', field)).toBe('Title is required');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('uses default min/max messages', () => {
|
|
94
|
+
const field: FormField = {
|
|
95
|
+
name: 'score',
|
|
96
|
+
label: 'Score',
|
|
97
|
+
type: 'number',
|
|
98
|
+
validations: [{ type: 'min', value: 10 }, { type: 'max', value: 20 }],
|
|
99
|
+
};
|
|
100
|
+
expect(validateField(9, field)).toBe('Score must be at least 10');
|
|
101
|
+
expect(validateField(21, field)).toBe('Score must be at most 20');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('supports custom pattern message', () => {
|
|
105
|
+
const field: FormField = {
|
|
106
|
+
name: 'phone',
|
|
107
|
+
label: 'Phone',
|
|
108
|
+
type: 'text',
|
|
109
|
+
validations: [{ type: 'pattern', value: '^\\d{11}$', message: 'Phone must be 11 digits' }],
|
|
110
|
+
};
|
|
111
|
+
expect(validateField('123', field)).toBe('Phone must be 11 digits');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('uses fallback custom message when custom validator returns false', () => {
|
|
115
|
+
const field: FormField = {
|
|
116
|
+
name: 'code',
|
|
117
|
+
label: 'Code',
|
|
118
|
+
type: 'text',
|
|
119
|
+
validations: [{ type: 'custom', message: 'Code invalid', validator: () => false }],
|
|
120
|
+
};
|
|
121
|
+
expect(validateField('anything', field)).toBe('Code invalid');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('core utils - normalizeFieldValue', () => {
|
|
127
|
+
const numberField: FormField = { name: 'age', label: 'Age', type: 'number' };
|
|
128
|
+
const textField: FormField = { name: 'name', label: 'Name', type: 'text' };
|
|
129
|
+
|
|
130
|
+
it('coerces number field values', () => {
|
|
131
|
+
expect(normalizeFieldValue(numberField, '22')).toBe(22);
|
|
132
|
+
expect(normalizeFieldValue(numberField, 22)).toBe(22);
|
|
133
|
+
expect(normalizeFieldValue(numberField, '')).toBe('');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps non-number fields unchanged', () => {
|
|
137
|
+
expect(normalizeFieldValue(textField, 'abc')).toBe('abc');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('core utils - validate helpers', () => {
|
|
142
|
+
const fields: FormField[] = [
|
|
143
|
+
{ name: 'username', label: 'Username', type: 'text', validations: [{ type: 'required', message: 'Required' }] },
|
|
144
|
+
{ name: 'age', label: 'Age', type: 'number', validations: [{ type: 'min', value: 18, message: 'Age >= 18' }] },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
it('validates a field by name', () => {
|
|
148
|
+
expect(validateFieldByName(fields, 'username', '')).toBe('Required');
|
|
149
|
+
expect(validateFieldByName(fields, 'username', 'ok')).toBeNull();
|
|
150
|
+
expect(validateFieldByName(fields, 'missing', 'x')).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('validates entire form and returns keyed errors', () => {
|
|
154
|
+
expect(validateForm(fields, { username: '', age: 17 })).toEqual({
|
|
155
|
+
username: 'Required',
|
|
156
|
+
age: 'Age >= 18',
|
|
157
|
+
});
|
|
158
|
+
expect(validateForm(fields, { username: 'ok', age: 18 })).toEqual({});
|
|
159
|
+
});
|
|
81
160
|
});
|
|
82
161
|
|
|
83
162
|
describe('core utils - getDefaultValues', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'tsup';
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
entry: ['src/index.ts', 'src/types.ts', 'src/utils.ts', 'src/parser.ts'],
|
|
4
|
+
entry: ['src/index.ts', 'src/types.ts', 'src/utils.ts', 'src/formState.ts', 'src/parser.ts'],
|
|
5
5
|
format: ['cjs', 'esm'],
|
|
6
6
|
dts: true,
|
|
7
7
|
clean: true,
|
|
@@ -55,7 +55,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
55
55
|
|
|
56
56
|
// src/DynamicForm.tsx
|
|
57
57
|
var import_react = require("react");
|
|
58
|
-
var
|
|
58
|
+
var import_core2 = require("pdyform/core");
|
|
59
59
|
|
|
60
60
|
// src/components/Input.tsx
|
|
61
61
|
var React = __toESM(require("react"), 1);
|
|
@@ -248,14 +248,11 @@ var Label = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
|
248
248
|
Label.displayName = LabelPrimitive.Root.displayName;
|
|
249
249
|
|
|
250
250
|
// src/components/InputRenderer.tsx
|
|
251
|
+
var import_core = require("pdyform/core");
|
|
251
252
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
252
253
|
var InputRenderer = ({ field, value, onChange, onBlur, fieldId }) => {
|
|
253
254
|
const handleChange = (nextValue) => {
|
|
254
|
-
|
|
255
|
-
onChange(nextValue);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
onChange(nextValue === "" ? "" : Number(nextValue));
|
|
255
|
+
onChange((0, import_core.normalizeFieldValue)(field, nextValue));
|
|
259
256
|
};
|
|
260
257
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
261
258
|
Input,
|
|
@@ -398,47 +395,21 @@ var FormFieldRenderer = ({
|
|
|
398
395
|
// src/DynamicForm.tsx
|
|
399
396
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
400
397
|
var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
401
|
-
const [
|
|
402
|
-
const [errors, setErrors] = (0, import_react.useState)({});
|
|
403
|
-
const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
|
|
398
|
+
const [formState, setFormState] = (0, import_react.useState)(() => (0, import_core2.createFormRuntimeState)(schema.fields));
|
|
404
399
|
const handleFieldChange = (name, value) => {
|
|
405
|
-
|
|
406
|
-
const field = schema.fields.find((f) => f.name === name);
|
|
407
|
-
if (field) {
|
|
408
|
-
const error = (0, import_core.validateField)(value, field);
|
|
409
|
-
setErrors((prev) => ({
|
|
410
|
-
...prev,
|
|
411
|
-
[name]: error || ""
|
|
412
|
-
}));
|
|
413
|
-
}
|
|
400
|
+
setFormState((prev) => (0, import_core2.applyFieldChange)(schema.fields, prev, name, value));
|
|
414
401
|
};
|
|
415
402
|
const handleFieldBlur = (name) => {
|
|
416
|
-
|
|
417
|
-
if (field) {
|
|
418
|
-
const error = (0, import_core.validateField)(values[name], field);
|
|
419
|
-
setErrors((prev) => ({
|
|
420
|
-
...prev,
|
|
421
|
-
[name]: error || ""
|
|
422
|
-
}));
|
|
423
|
-
}
|
|
403
|
+
setFormState((prev) => (0, import_core2.applyFieldBlur)(schema.fields, prev, name));
|
|
424
404
|
};
|
|
425
405
|
const handleSubmit = (e) => {
|
|
426
406
|
e.preventDefault();
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
schema.fields.forEach((field) => {
|
|
431
|
-
const error = (0, import_core.validateField)(values[field.name], field);
|
|
432
|
-
if (error) {
|
|
433
|
-
newErrors[field.name] = error;
|
|
434
|
-
hasError = true;
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
setErrors(newErrors);
|
|
407
|
+
const submittingState = (0, import_core2.setSubmitting)(formState, true);
|
|
408
|
+
const { state: validatedState, hasError } = (0, import_core2.runSubmitValidation)(schema.fields, submittingState);
|
|
409
|
+
setFormState(validatedState);
|
|
438
410
|
if (!hasError) {
|
|
439
|
-
onSubmit(values);
|
|
411
|
+
onSubmit(validatedState.values);
|
|
440
412
|
}
|
|
441
|
-
setIsSubmitting(false);
|
|
442
413
|
};
|
|
443
414
|
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("form", { onSubmit: handleSubmit, className: `space-y-6 ${className || ""}`, children: [
|
|
444
415
|
schema.title && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { className: "text-2xl font-bold tracking-tight", children: schema.title }),
|
|
@@ -447,10 +418,10 @@ var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
|
447
418
|
FormFieldRenderer,
|
|
448
419
|
{
|
|
449
420
|
field,
|
|
450
|
-
value: values[field.name],
|
|
421
|
+
value: formState.values[field.name],
|
|
451
422
|
onChange: (val) => handleFieldChange(field.name, val),
|
|
452
423
|
onBlur: () => handleFieldBlur(field.name),
|
|
453
|
-
error: errors[field.name]
|
|
424
|
+
error: formState.errors[field.name]
|
|
454
425
|
},
|
|
455
426
|
field.name
|
|
456
427
|
)) }),
|
|
@@ -458,9 +429,9 @@ var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
|
458
429
|
"button",
|
|
459
430
|
{
|
|
460
431
|
type: "submit",
|
|
461
|
-
disabled: isSubmitting,
|
|
432
|
+
disabled: formState.isSubmitting,
|
|
462
433
|
className: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full",
|
|
463
|
-
children: isSubmitting ? "Submitting..." : schema.submitButtonText || "Submit"
|
|
434
|
+
children: formState.isSubmitting ? "Submitting..." : schema.submitButtonText || "Submit"
|
|
464
435
|
}
|
|
465
436
|
)
|
|
466
437
|
] });
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// src/DynamicForm.tsx
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createFormRuntimeState,
|
|
5
|
+
applyFieldChange,
|
|
6
|
+
applyFieldBlur,
|
|
7
|
+
runSubmitValidation,
|
|
8
|
+
setSubmitting
|
|
9
|
+
} from "pdyform/core";
|
|
4
10
|
|
|
5
11
|
// src/components/Input.tsx
|
|
6
12
|
import * as React from "react";
|
|
@@ -193,14 +199,11 @@ var Label = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
|
193
199
|
Label.displayName = LabelPrimitive.Root.displayName;
|
|
194
200
|
|
|
195
201
|
// src/components/InputRenderer.tsx
|
|
202
|
+
import { normalizeFieldValue } from "pdyform/core";
|
|
196
203
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
197
204
|
var InputRenderer = ({ field, value, onChange, onBlur, fieldId }) => {
|
|
198
205
|
const handleChange = (nextValue) => {
|
|
199
|
-
|
|
200
|
-
onChange(nextValue);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
onChange(nextValue === "" ? "" : Number(nextValue));
|
|
206
|
+
onChange(normalizeFieldValue(field, nextValue));
|
|
204
207
|
};
|
|
205
208
|
return /* @__PURE__ */ jsx7(
|
|
206
209
|
Input,
|
|
@@ -343,47 +346,21 @@ var FormFieldRenderer = ({
|
|
|
343
346
|
// src/DynamicForm.tsx
|
|
344
347
|
import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
345
348
|
var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
346
|
-
const [
|
|
347
|
-
const [errors, setErrors] = useState({});
|
|
348
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
349
|
+
const [formState, setFormState] = useState(() => createFormRuntimeState(schema.fields));
|
|
349
350
|
const handleFieldChange = (name, value) => {
|
|
350
|
-
|
|
351
|
-
const field = schema.fields.find((f) => f.name === name);
|
|
352
|
-
if (field) {
|
|
353
|
-
const error = validateField(value, field);
|
|
354
|
-
setErrors((prev) => ({
|
|
355
|
-
...prev,
|
|
356
|
-
[name]: error || ""
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
351
|
+
setFormState((prev) => applyFieldChange(schema.fields, prev, name, value));
|
|
359
352
|
};
|
|
360
353
|
const handleFieldBlur = (name) => {
|
|
361
|
-
|
|
362
|
-
if (field) {
|
|
363
|
-
const error = validateField(values[name], field);
|
|
364
|
-
setErrors((prev) => ({
|
|
365
|
-
...prev,
|
|
366
|
-
[name]: error || ""
|
|
367
|
-
}));
|
|
368
|
-
}
|
|
354
|
+
setFormState((prev) => applyFieldBlur(schema.fields, prev, name));
|
|
369
355
|
};
|
|
370
356
|
const handleSubmit = (e) => {
|
|
371
357
|
e.preventDefault();
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
schema.fields.forEach((field) => {
|
|
376
|
-
const error = validateField(values[field.name], field);
|
|
377
|
-
if (error) {
|
|
378
|
-
newErrors[field.name] = error;
|
|
379
|
-
hasError = true;
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
setErrors(newErrors);
|
|
358
|
+
const submittingState = setSubmitting(formState, true);
|
|
359
|
+
const { state: validatedState, hasError } = runSubmitValidation(schema.fields, submittingState);
|
|
360
|
+
setFormState(validatedState);
|
|
383
361
|
if (!hasError) {
|
|
384
|
-
onSubmit(values);
|
|
362
|
+
onSubmit(validatedState.values);
|
|
385
363
|
}
|
|
386
|
-
setIsSubmitting(false);
|
|
387
364
|
};
|
|
388
365
|
return /* @__PURE__ */ jsxs6("form", { onSubmit: handleSubmit, className: `space-y-6 ${className || ""}`, children: [
|
|
389
366
|
schema.title && /* @__PURE__ */ jsx13("h2", { className: "text-2xl font-bold tracking-tight", children: schema.title }),
|
|
@@ -392,10 +369,10 @@ var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
|
392
369
|
FormFieldRenderer,
|
|
393
370
|
{
|
|
394
371
|
field,
|
|
395
|
-
value: values[field.name],
|
|
372
|
+
value: formState.values[field.name],
|
|
396
373
|
onChange: (val) => handleFieldChange(field.name, val),
|
|
397
374
|
onBlur: () => handleFieldBlur(field.name),
|
|
398
|
-
error: errors[field.name]
|
|
375
|
+
error: formState.errors[field.name]
|
|
399
376
|
},
|
|
400
377
|
field.name
|
|
401
378
|
)) }),
|
|
@@ -403,9 +380,9 @@ var DynamicForm = ({ schema, onSubmit, className }) => {
|
|
|
403
380
|
"button",
|
|
404
381
|
{
|
|
405
382
|
type: "submit",
|
|
406
|
-
disabled: isSubmitting,
|
|
383
|
+
disabled: formState.isSubmitting,
|
|
407
384
|
className: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full",
|
|
408
|
-
children: isSubmitting ? "Submitting..." : schema.submitButtonText || "Submit"
|
|
385
|
+
children: formState.isSubmitting ? "Submitting..." : schema.submitButtonText || "Submit"
|
|
409
386
|
}
|
|
410
387
|
)
|
|
411
388
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.6.1","results":[[":test/DynamicForm.test.tsx",{"duration":29,"failed":false}],[":test/FormFieldRenderer.test.tsx",{"duration":
|
|
1
|
+
{"version":"1.6.1","results":[[":test/DynamicForm.test.tsx",{"duration":29,"failed":false}],[":test/FormFieldRenderer.test.tsx",{"duration":129,"failed":false}]]}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
FormSchema,
|
|
4
|
+
FormRuntimeState,
|
|
5
|
+
createFormRuntimeState,
|
|
6
|
+
applyFieldChange,
|
|
7
|
+
applyFieldBlur,
|
|
8
|
+
runSubmitValidation,
|
|
9
|
+
setSubmitting,
|
|
10
|
+
} from 'pdyform/core';
|
|
3
11
|
import { FormFieldRenderer } from './FormFieldRenderer';
|
|
4
12
|
|
|
5
13
|
interface DynamicFormProps {
|
|
@@ -9,56 +17,24 @@ interface DynamicFormProps {
|
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, className }) => {
|
|
12
|
-
const [
|
|
13
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
14
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
20
|
+
const [formState, setFormState] = useState<FormRuntimeState>(() => createFormRuntimeState(schema.fields));
|
|
15
21
|
|
|
16
22
|
const handleFieldChange = (name: string, value: any) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Validate on change
|
|
20
|
-
const field = schema.fields.find(f => f.name === name);
|
|
21
|
-
if (field) {
|
|
22
|
-
const error = validateField(value, field);
|
|
23
|
-
setErrors((prev) => ({
|
|
24
|
-
...prev,
|
|
25
|
-
[name]: error || ''
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
23
|
+
setFormState((prev) => applyFieldChange(schema.fields, prev, name, value));
|
|
28
24
|
};
|
|
29
25
|
|
|
30
26
|
const handleFieldBlur = (name: string) => {
|
|
31
|
-
|
|
32
|
-
if (field) {
|
|
33
|
-
const error = validateField(values[name], field);
|
|
34
|
-
setErrors((prev) => ({
|
|
35
|
-
...prev,
|
|
36
|
-
[name]: error || ''
|
|
37
|
-
}));
|
|
38
|
-
}
|
|
27
|
+
setFormState((prev) => applyFieldBlur(schema.fields, prev, name));
|
|
39
28
|
};
|
|
40
29
|
|
|
41
30
|
const handleSubmit = (e: React.FormEvent) => {
|
|
42
31
|
e.preventDefault();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let hasError = false;
|
|
47
|
-
|
|
48
|
-
schema.fields.forEach((field) => {
|
|
49
|
-
const error = validateField(values[field.name], field);
|
|
50
|
-
if (error) {
|
|
51
|
-
newErrors[field.name] = error;
|
|
52
|
-
hasError = true;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
setErrors(newErrors);
|
|
57
|
-
|
|
32
|
+
const submittingState = setSubmitting(formState, true);
|
|
33
|
+
const { state: validatedState, hasError } = runSubmitValidation(schema.fields, submittingState);
|
|
34
|
+
setFormState(validatedState);
|
|
58
35
|
if (!hasError) {
|
|
59
|
-
onSubmit(values);
|
|
36
|
+
onSubmit(validatedState.values);
|
|
60
37
|
}
|
|
61
|
-
setIsSubmitting(false);
|
|
62
38
|
};
|
|
63
39
|
|
|
64
40
|
return (
|
|
@@ -72,10 +48,10 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, clas
|
|
|
72
48
|
<FormFieldRenderer
|
|
73
49
|
key={field.name}
|
|
74
50
|
field={field}
|
|
75
|
-
value={values[field.name]}
|
|
51
|
+
value={formState.values[field.name]}
|
|
76
52
|
onChange={(val) => handleFieldChange(field.name, val)}
|
|
77
53
|
onBlur={() => handleFieldBlur(field.name)}
|
|
78
|
-
error={errors[field.name]}
|
|
54
|
+
error={formState.errors[field.name]}
|
|
79
55
|
/>
|
|
80
56
|
)
|
|
81
57
|
))}
|
|
@@ -83,10 +59,10 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, clas
|
|
|
83
59
|
|
|
84
60
|
<button
|
|
85
61
|
type="submit"
|
|
86
|
-
disabled={isSubmitting}
|
|
62
|
+
disabled={formState.isSubmitting}
|
|
87
63
|
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
|
|
88
64
|
>
|
|
89
|
-
{isSubmitting ? 'Submitting...' : (schema.submitButtonText || 'Submit')}
|
|
65
|
+
{formState.isSubmitting ? 'Submitting...' : (schema.submitButtonText || 'Submit')}
|
|
90
66
|
</button>
|
|
91
67
|
</form>
|
|
92
68
|
);
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { normalizeFieldValue } from 'pdyform/core';
|
|
2
3
|
import type { FieldRenderContext } from './types';
|
|
3
4
|
import { Input } from './Input';
|
|
4
5
|
|
|
5
6
|
const InputRenderer: React.FC<FieldRenderContext> = ({ field, value, onChange, onBlur, fieldId }) => {
|
|
6
7
|
const handleChange = (nextValue: string) => {
|
|
7
|
-
|
|
8
|
-
onChange(nextValue);
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
onChange(nextValue === '' ? '' : Number(nextValue));
|
|
8
|
+
onChange(normalizeFieldValue(field, nextValue));
|
|
13
9
|
};
|
|
14
10
|
|
|
15
11
|
return (
|