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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/chunk-TP3IHKWV.js +69 -0
  3. package/packages/core/dist/{chunk-GQASS6PM.js → chunk-WEDHXOHH.js} +22 -0
  4. package/packages/core/dist/formState.cjs +187 -0
  5. package/packages/core/dist/formState.d.cts +17 -0
  6. package/packages/core/dist/formState.d.ts +17 -0
  7. package/packages/core/dist/formState.js +15 -0
  8. package/packages/core/dist/index.cjs +92 -2
  9. package/packages/core/dist/index.d.cts +2 -1
  10. package/packages/core/dist/index.d.ts +2 -1
  11. package/packages/core/dist/index.js +21 -3
  12. package/packages/core/dist/utils.cjs +27 -2
  13. package/packages/core/dist/utils.d.cts +4 -1
  14. package/packages/core/dist/utils.d.ts +4 -1
  15. package/packages/core/dist/utils.js +9 -3
  16. package/packages/core/node_modules/.vite/vitest/results.json +1 -1
  17. package/packages/core/src/formState.ts +79 -0
  18. package/packages/core/src/index.ts +1 -0
  19. package/packages/core/src/utils.ts +24 -0
  20. package/packages/core/test/formState.test.ts +71 -0
  21. package/packages/core/test/utils.test.ts +80 -1
  22. package/packages/core/tsup.config.ts +1 -1
  23. package/packages/react/dist/index.cjs +14 -43
  24. package/packages/react/dist/index.js +20 -43
  25. package/packages/react/node_modules/.vite/vitest/results.json +1 -1
  26. package/packages/react/src/DynamicForm.tsx +20 -44
  27. package/packages/react/src/components/InputRenderer.tsx +2 -6
  28. package/packages/vue/dist/index.js +5 -5
  29. package/packages/vue/dist/index.mjs +769 -787
  30. package/packages/vue/node_modules/.vite/vitest/results.json +1 -1
  31. package/packages/vue/src/DynamicForm.vue +18 -36
  32. package/packages/vue/src/components/InputRenderer.vue +2 -6
  33. package/packages/core/dist/parser.cjs +0 -1
  34. package/packages/core/dist/parser.d.cts +0 -2
  35. package/packages/core/dist/parser.d.ts +0 -2
  36. package/packages/core/dist/parser.js +0 -0
  37. 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
+ }
@@ -1,2 +1,3 @@
1
1
  export type * from './types';
2
2
  export * from './utils';
3
+ export * from './formState';
@@ -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 import_core = require("pdyform/core");
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
- if (field.type !== "number") {
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 [values, setValues] = (0, import_react.useState)((0, import_core.getDefaultValues)(schema.fields));
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
- setValues((prev) => ({ ...prev, [name]: value }));
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
- const field = schema.fields.find((f) => f.name === name);
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
- setIsSubmitting(true);
428
- const newErrors = {};
429
- let hasError = false;
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 { validateField, getDefaultValues } from "pdyform/core";
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
- if (field.type !== "number") {
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 [values, setValues] = useState(getDefaultValues(schema.fields));
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
- setValues((prev) => ({ ...prev, [name]: value }));
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
- const field = schema.fields.find((f) => f.name === name);
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
- setIsSubmitting(true);
373
- const newErrors = {};
374
- let hasError = false;
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":128,"failed":false}]]}
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 { FormSchema, validateField, getDefaultValues } from 'pdyform/core';
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 [values, setValues] = useState<Record<string, any>>(getDefaultValues(schema.fields));
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
- setValues((prev) => ({ ...prev, [name]: value }));
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
- const field = schema.fields.find(f => f.name === name);
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
- setIsSubmitting(true);
44
-
45
- const newErrors: Record<string, string> = {};
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
- if (field.type !== 'number') {
8
- onChange(nextValue);
9
- return;
10
- }
11
-
12
- onChange(nextValue === '' ? '' : Number(nextValue));
8
+ onChange(normalizeFieldValue(field, nextValue));
13
9
  };
14
10
 
15
11
  return (