@reformer/core 1.0.0 → 1.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 +57 -48
- package/llms.txt +110 -779
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@reformer/core)
|
|
4
4
|
[](https://www.npmjs.com/package/@reformer/core)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://stackblitz.com/~/github.com/AlexandrBukhtatyy/ReFormer/tree/main/projects/react-playground?file=projects/react-playground/src/App.tsx)
|
|
7
6
|
|
|
8
7
|
Reactive form state management library for React with signals-based architecture.
|
|
9
8
|
|
|
9
|
+
## Playground
|
|
10
|
+
|
|
11
|
+
[](https://stackblitz.com/~/github.com/AlexandrBukhtatyy/ReFormer/tree/main/projects/react-playground?file=projects/react-playground/src/App.tsx)
|
|
12
|
+
|
|
10
13
|
## Documentation
|
|
11
14
|
|
|
12
15
|
Full documentation is available at [https://alexandrbukhtatyy.github.io/ReFormer/](https://alexandrbukhtatyy.github.io/ReFormer/)
|
|
@@ -22,7 +25,7 @@ Full documentation is available at [https://alexandrbukhtatyy.github.io/ReFormer
|
|
|
22
25
|
## Installation
|
|
23
26
|
|
|
24
27
|
```bash
|
|
25
|
-
npm install @reformer/core
|
|
28
|
+
npm install @reformer/core@beta # Active development is underway, so you can try beta
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
## Quick Start
|
|
@@ -39,15 +42,7 @@ import {
|
|
|
39
42
|
FieldNode,
|
|
40
43
|
} from '@reformer/core';
|
|
41
44
|
|
|
42
|
-
//
|
|
43
|
-
interface RegistrationForm {
|
|
44
|
-
username: string;
|
|
45
|
-
email: string;
|
|
46
|
-
password: string;
|
|
47
|
-
confirmPassword: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. Simple FormField component
|
|
45
|
+
// 0. Simple FormField component
|
|
51
46
|
function FormField({ label, control }: { label: string; control: FieldNode<string> }) {
|
|
52
47
|
const { value, errors } = useFormControl(control);
|
|
53
48
|
|
|
@@ -64,47 +59,61 @@ function FormField({ label, control }: { label: string; control: FieldNode<strin
|
|
|
64
59
|
);
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
//
|
|
62
|
+
// 1. Define form interface
|
|
63
|
+
interface RegistrationForm {
|
|
64
|
+
username: string;
|
|
65
|
+
email: string;
|
|
66
|
+
password: string;
|
|
67
|
+
confirmPassword: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Form schema
|
|
71
|
+
const formSchema = {
|
|
72
|
+
username: { value: '' },
|
|
73
|
+
email: { value: '' },
|
|
74
|
+
password: { value: '' },
|
|
75
|
+
confirmPassword: { value: '' },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 3. Validation schema
|
|
79
|
+
validationSchema = (path) => {
|
|
80
|
+
required(path.username);
|
|
81
|
+
|
|
82
|
+
required(path.email);
|
|
83
|
+
email(path.email);
|
|
84
|
+
|
|
85
|
+
required(path.password);
|
|
86
|
+
required(path.confirmPassword);
|
|
87
|
+
|
|
88
|
+
// Cross-field validation: passwords must match
|
|
89
|
+
validate(path.confirmPassword, (value, ctx) => {
|
|
90
|
+
const password = ctx.form.password.value.value;
|
|
91
|
+
if (value && password && value !== password) {
|
|
92
|
+
return { code: 'mismatch', message: 'Passwords do not match' };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// 4. Behavior schema
|
|
99
|
+
behavior = (path) => {
|
|
100
|
+
// Clear confirmPassword when password changes (if not empty)
|
|
101
|
+
watchField(path.password, (_, ctx) => {
|
|
102
|
+
const confirmValue = ctx.form.confirmPassword.value.value;
|
|
103
|
+
if (confirmValue) {
|
|
104
|
+
ctx.form.confirmPassword.setValue('', { emitEvent: false });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// 5. Registration form component
|
|
68
110
|
function RegistrationFormExample() {
|
|
69
111
|
const form = useMemo(
|
|
70
112
|
() =>
|
|
71
113
|
createForm<RegistrationForm>({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
email: { value: '' },
|
|
76
|
-
password: { value: '' },
|
|
77
|
-
confirmPassword: { value: '' },
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
// Validation schema
|
|
81
|
-
validation: (path) => {
|
|
82
|
-
required(path.username);
|
|
83
|
-
required(path.email);
|
|
84
|
-
email(path.email);
|
|
85
|
-
required(path.password);
|
|
86
|
-
required(path.confirmPassword);
|
|
87
|
-
|
|
88
|
-
// Cross-field validation: passwords must match
|
|
89
|
-
validate(path.confirmPassword, (value, ctx) => {
|
|
90
|
-
const password = ctx.form.password.value.value;
|
|
91
|
-
if (value && password && value !== password) {
|
|
92
|
-
return { code: 'mismatch', message: 'Passwords do not match' };
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
});
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
// Behavior schema
|
|
99
|
-
behavior: (path) => {
|
|
100
|
-
// Clear confirmPassword when password changes (if not empty)
|
|
101
|
-
watchField(path.password, (_, ctx) => {
|
|
102
|
-
const confirmValue = ctx.form.confirmPassword.value.value;
|
|
103
|
-
if (confirmValue) {
|
|
104
|
-
ctx.form.confirmPassword.setValue('', { emitEvent: false });
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
},
|
|
114
|
+
form: formSchema,
|
|
115
|
+
validation: validationSchema,
|
|
116
|
+
behavior: behaviorSchema,
|
|
108
117
|
}),
|
|
109
118
|
[]
|
|
110
119
|
);
|
package/llms.txt
CHANGED
|
@@ -1,847 +1,178 @@
|
|
|
1
|
-
# ReFormer
|
|
1
|
+
# ReFormer - LLM Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Built on Preact Signals Core for fine-grained reactivity with full TypeScript support.
|
|
5
|
-
> Key features: type-safe schemas, declarative validation, reactive behaviors, nested forms, dynamic arrays.
|
|
3
|
+
## 1. QUICK REFERENCE
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
### Imports (CRITICALLY IMPORTANT)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- React 18+ or React 19+
|
|
15
|
-
- @preact/signals-core ^1.8.0
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
import { createForm } from 'reformer';
|
|
21
|
-
import { required, email, minLength } from 'reformer/validators';
|
|
22
|
-
import { useFormControl } from 'reformer';
|
|
23
|
-
|
|
24
|
-
// 1. Define your form type
|
|
25
|
-
type ContactForm = {
|
|
26
|
-
name: string;
|
|
27
|
-
email: string;
|
|
28
|
-
message: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// 2. Create form with schema and validation
|
|
32
|
-
const form = createForm<ContactForm>({
|
|
33
|
-
form: {
|
|
34
|
-
name: { value: '', component: Input, componentProps: { label: 'Name' } },
|
|
35
|
-
email: { value: '', component: Input, componentProps: { label: 'Email' } },
|
|
36
|
-
message: { value: '', component: Textarea, componentProps: { label: 'Message' } },
|
|
37
|
-
},
|
|
38
|
-
validation: (path) => {
|
|
39
|
-
required(path.name);
|
|
40
|
-
minLength(path.name, 2);
|
|
41
|
-
required(path.email);
|
|
42
|
-
email(path.email);
|
|
43
|
-
required(path.message);
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// 3. Use in React component
|
|
48
|
-
function ContactForm() {
|
|
49
|
-
const { value, errors, shouldShowError } = useFormControl(form.name);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<input
|
|
53
|
-
value={value}
|
|
54
|
-
onChange={(e) => form.name.setValue(e.target.value)}
|
|
55
|
-
onBlur={() => form.name.markAsTouched()}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Architecture
|
|
62
|
-
|
|
63
|
-
### Node Hierarchy
|
|
64
|
-
|
|
65
|
-
ReFormer uses a tree-based node architecture:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
GroupNode (Form)
|
|
69
|
-
├── FieldNode (single values: string, number, boolean)
|
|
70
|
-
├── GroupNode (nested objects)
|
|
71
|
-
└── ArrayNode (dynamic arrays)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
All nodes inherit from abstract `FormNode` base class.
|
|
75
|
-
|
|
76
|
-
### Key Concepts
|
|
77
|
-
|
|
78
|
-
1. **Form Schema** - Defines structure, components, and initial values
|
|
79
|
-
2. **Validation Schema** - Declares validation rules (separate from structure)
|
|
80
|
-
3. **Behavior Schema** - Reactive logic (computed fields, conditional visibility, sync)
|
|
81
|
-
|
|
82
|
-
### Signals-based Reactivity
|
|
83
|
-
|
|
84
|
-
- Uses @preact/signals-core for fine-grained reactivity
|
|
85
|
-
- Only affected components re-render when values change
|
|
86
|
-
- React integration via useSyncExternalStore
|
|
87
|
-
|
|
88
|
-
## Form Schema
|
|
89
|
-
|
|
90
|
-
### FieldConfig<T>
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
interface FieldConfig<T> {
|
|
94
|
-
value: T | null; // Initial value
|
|
95
|
-
component: ComponentType<any>; // React component to render
|
|
96
|
-
componentProps?: Record<string, any>; // Props passed to component
|
|
97
|
-
validators?: ValidatorFn<T>[]; // Sync validators
|
|
98
|
-
asyncValidators?: AsyncValidatorFn<T>[]; // Async validators
|
|
99
|
-
disabled?: boolean; // Initially disabled
|
|
100
|
-
updateOn?: 'change' | 'blur' | 'submit'; // When to validate (default: 'change')
|
|
101
|
-
debounce?: number; // Debounce validation in ms
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### ArrayConfig<T>
|
|
106
|
-
|
|
107
|
-
Arrays use single-element tuple syntax in schema:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
interface FormType {
|
|
111
|
-
phones: { type: string; number: string }[];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const schema: FormSchema<FormType> = {
|
|
115
|
-
phones: [{
|
|
116
|
-
type: { value: 'mobile', component: Select },
|
|
117
|
-
number: { value: '', component: Input },
|
|
118
|
-
}],
|
|
119
|
-
};
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Complete Schema Example
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
import { createForm } from 'reformer';
|
|
126
|
-
|
|
127
|
-
type UserForm = {
|
|
128
|
-
name: string;
|
|
129
|
-
email: string;
|
|
130
|
-
age: number;
|
|
131
|
-
address: {
|
|
132
|
-
street: string;
|
|
133
|
-
city: string;
|
|
134
|
-
};
|
|
135
|
-
phones: { type: string; number: string }[];
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const form = createForm<UserForm>({
|
|
139
|
-
form: {
|
|
140
|
-
name: { value: '', component: Input },
|
|
141
|
-
email: { value: '', component: Input },
|
|
142
|
-
age: { value: 0, component: Input, componentProps: { type: 'number' } },
|
|
143
|
-
address: {
|
|
144
|
-
street: { value: '', component: Input },
|
|
145
|
-
city: { value: '', component: Input },
|
|
146
|
-
},
|
|
147
|
-
phones: [{
|
|
148
|
-
type: { value: 'mobile', component: Select },
|
|
149
|
-
number: { value: '', component: Input },
|
|
150
|
-
}],
|
|
151
|
-
},
|
|
152
|
-
validation: (path) => { /* validators */ },
|
|
153
|
-
behavior: (path) => { /* behaviors */ },
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Node Types
|
|
158
|
-
|
|
159
|
-
### FieldNode<T>
|
|
160
|
-
|
|
161
|
-
Represents a single form field value.
|
|
162
|
-
|
|
163
|
-
**Properties (all are Signals):**
|
|
164
|
-
- `value` - Current value
|
|
165
|
-
- `valid` / `invalid` - Validation state
|
|
166
|
-
- `touched` / `untouched` - User interaction state
|
|
167
|
-
- `dirty` / `pristine` - Value changed from initial
|
|
168
|
-
- `errors` - Array of ValidationError objects
|
|
169
|
-
- `shouldShowError` - true when invalid AND (touched OR dirty)
|
|
170
|
-
- `disabled` - Is field disabled
|
|
171
|
-
- `pending` - Async validation in progress
|
|
172
|
-
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
173
|
-
- `componentProps` - Props for component
|
|
7
|
+
| What | Where |
|
|
8
|
+
| ------------------------------------------------------------------------------------------- | --------------------------- |
|
|
9
|
+
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
|
|
10
|
+
| `required`, `min`, `max`, `minLength`, `email`, `validate`, `validateTree` | `@reformer/core/validators` |
|
|
11
|
+
| `computeFrom`, `enableWhen`, `disableWhen`, `copyFrom`, `watchField` | `@reformer/core/behaviors` |
|
|
174
12
|
|
|
175
|
-
|
|
176
|
-
- `setValue(value, options?)` - Set new value
|
|
177
|
-
- `reset()` - Reset to initial value
|
|
178
|
-
- `markAsTouched()` - Mark as touched
|
|
179
|
-
- `markAsDirty()` - Mark as dirty
|
|
180
|
-
- `disable()` / `enable()` - Toggle disabled state
|
|
181
|
-
- `validate()` - Run validation
|
|
182
|
-
- `getErrors(filter?)` - Get filtered errors
|
|
13
|
+
### Type Values
|
|
183
14
|
|
|
184
|
-
|
|
15
|
+
- Optional numbers: `number | undefined` (NOT `null`)
|
|
16
|
+
- Optional strings: `string` (empty string by default)
|
|
17
|
+
- Do NOT add `[key: string]: unknown` to form interfaces
|
|
185
18
|
|
|
186
|
-
|
|
19
|
+
## 2. API SIGNATURES
|
|
187
20
|
|
|
188
|
-
|
|
189
|
-
- `controls` - Dictionary of child nodes
|
|
190
|
-
- All FormNode properties (computed from children)
|
|
21
|
+
### Validators
|
|
191
22
|
|
|
192
|
-
**Methods:**
|
|
193
|
-
- `getFieldByPath(path: string)` - Get field by dot-notation path
|
|
194
|
-
- `patchValue(partial)` - Update subset of fields
|
|
195
|
-
- `resetAll()` - Reset all children
|
|
196
|
-
- All FormNode methods
|
|
197
|
-
|
|
198
|
-
**Proxy Access:**
|
|
199
23
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
24
|
+
required(path, options?: { message?: string })
|
|
25
|
+
min(path, value: number, options?: { message?: string })
|
|
26
|
+
max(path, value: number, options?: { message?: string })
|
|
27
|
+
minLength(path, length: number, options?: { message?: string })
|
|
28
|
+
maxLength(path, length: number, options?: { message?: string })
|
|
29
|
+
email(path, options?: { message?: string })
|
|
30
|
+
validate(path, validator: (value) => ValidationError | null)
|
|
31
|
+
validateTree(validator: (ctx) => ValidationError | null)
|
|
32
|
+
when(condition: (form) => boolean, validatorsFn: () => void)
|
|
204
33
|
```
|
|
205
34
|
|
|
206
|
-
###
|
|
207
|
-
|
|
208
|
-
Manages dynamic arrays.
|
|
209
|
-
|
|
210
|
-
**Properties:**
|
|
211
|
-
- `controls` - Array of GroupNode items
|
|
212
|
-
- `length` - Number of items
|
|
213
|
-
|
|
214
|
-
**Methods:**
|
|
215
|
-
- `push(value)` - Add item to end
|
|
216
|
-
- `insert(index, value)` - Insert at position
|
|
217
|
-
- `removeAt(index)` - Remove at position
|
|
218
|
-
- `move(from, to)` - Move item
|
|
219
|
-
- `clear()` - Remove all items
|
|
220
|
-
- `at(index)` - Get item at index
|
|
35
|
+
### Behaviors
|
|
221
36
|
|
|
222
37
|
```typescript
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
38
|
+
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
|
|
39
|
+
disableWhen(path, condition: (form) => boolean)
|
|
40
|
+
computeFrom(sourcePaths[], targetPath, compute: (values) => result)
|
|
41
|
+
watchField(path, callback: (value, ctx) => void)
|
|
42
|
+
copyFrom(sourcePath, targetPath, options?: { when?, fields?, transform? })
|
|
227
43
|
```
|
|
228
44
|
|
|
229
|
-
##
|
|
45
|
+
## 3. COMMON PATTERNS
|
|
230
46
|
|
|
231
|
-
###
|
|
47
|
+
### Conditional Fields with Auto-Reset
|
|
232
48
|
|
|
233
49
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const form = createForm<FormType>({
|
|
237
|
-
form: { /* schema */ },
|
|
238
|
-
validation: (path) => {
|
|
239
|
-
// Built-in validators
|
|
240
|
-
required(path.name);
|
|
241
|
-
email(path.email);
|
|
242
|
-
minLength(path.password, 8);
|
|
243
|
-
|
|
244
|
-
// Custom validator
|
|
245
|
-
validate(path.age, (value) => {
|
|
246
|
-
if (value < 18) return { code: 'tooYoung', message: 'Must be 18+' };
|
|
247
|
-
return null;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Async validator
|
|
251
|
-
validateAsync(path.username, async (value) => {
|
|
252
|
-
const available = await checkUsername(value);
|
|
253
|
-
if (!available) return { code: 'taken', message: 'Username taken' };
|
|
254
|
-
return null;
|
|
255
|
-
}, { debounce: 500 });
|
|
256
|
-
},
|
|
50
|
+
enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
|
|
51
|
+
resetOnDisable: true,
|
|
257
52
|
});
|
|
258
53
|
```
|
|
259
54
|
|
|
260
|
-
###
|
|
261
|
-
|
|
262
|
-
All imported from `reformer/validators`:
|
|
263
|
-
|
|
264
|
-
| Validator | Usage | Description |
|
|
265
|
-
|-----------|-------|-------------|
|
|
266
|
-
| `required(path)` | `required(path.name)` | Non-empty value |
|
|
267
|
-
| `email(path)` | `email(path.email)` | Valid email format |
|
|
268
|
-
| `minLength(path, n)` | `minLength(path.name, 2)` | Minimum string length |
|
|
269
|
-
| `maxLength(path, n)` | `maxLength(path.bio, 500)` | Maximum string length |
|
|
270
|
-
| `min(path, n)` | `min(path.age, 18)` | Minimum number value |
|
|
271
|
-
| `max(path, n)` | `max(path.qty, 100)` | Maximum number value |
|
|
272
|
-
| `pattern(path, regex)` | `pattern(path.code, /^[A-Z]+$/)` | Match regex |
|
|
273
|
-
| `url(path)` | `url(path.website)` | Valid URL |
|
|
274
|
-
| `phone(path)` | `phone(path.phone)` | Valid phone |
|
|
275
|
-
| `number(path)` | `number(path.amount)` | Must be number |
|
|
276
|
-
| `date(path)` | `date(path.birthDate)` | Valid date |
|
|
277
|
-
|
|
278
|
-
### Custom Validator Example
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
// validators/password.ts
|
|
282
|
-
export function strongPassword() {
|
|
283
|
-
return (value: string) => {
|
|
284
|
-
if (!value) return null; // Skip empty (use required() separately)
|
|
285
|
-
|
|
286
|
-
const errors: string[] = [];
|
|
287
|
-
if (!/[A-Z]/.test(value)) errors.push('uppercase');
|
|
288
|
-
if (!/[a-z]/.test(value)) errors.push('lowercase');
|
|
289
|
-
if (!/[0-9]/.test(value)) errors.push('number');
|
|
290
|
-
if (value.length < 8) errors.push('length');
|
|
291
|
-
|
|
292
|
-
if (errors.length) {
|
|
293
|
-
return { code: 'weakPassword', message: 'Password too weak', params: { missing: errors } };
|
|
294
|
-
}
|
|
295
|
-
return null;
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Usage
|
|
300
|
-
validation: (path) => {
|
|
301
|
-
required(path.password);
|
|
302
|
-
validate(path.password, strongPassword());
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Async Validation Example
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// Check username availability on server
|
|
310
|
-
validation: (path) => {
|
|
311
|
-
required(path.username);
|
|
312
|
-
|
|
313
|
-
validateAsync(path.username, async (value, ctx) => {
|
|
314
|
-
if (!value || value.length < 3) return null;
|
|
315
|
-
|
|
316
|
-
const response = await fetch(`/api/check-username?u=${value}`);
|
|
317
|
-
const { available } = await response.json();
|
|
318
|
-
|
|
319
|
-
if (!available) {
|
|
320
|
-
return { code: 'usernameTaken', message: 'Username is already taken' };
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
}, { debounce: 500 });
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Cross-field Validation
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { validateTree } from 'reformer/validators';
|
|
331
|
-
|
|
332
|
-
validation: (path) => {
|
|
333
|
-
required(path.password);
|
|
334
|
-
required(path.confirmPassword);
|
|
335
|
-
|
|
336
|
-
// Cross-field validation
|
|
337
|
-
validateTree((ctx) => {
|
|
338
|
-
const password = ctx.form.password.value.value;
|
|
339
|
-
const confirm = ctx.form.confirmPassword.value.value;
|
|
340
|
-
|
|
341
|
-
if (password && confirm && password !== confirm) {
|
|
342
|
-
return {
|
|
343
|
-
code: 'passwordMismatch',
|
|
344
|
-
message: 'Passwords do not match',
|
|
345
|
-
path: 'confirmPassword',
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return null;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
## Behaviors
|
|
354
|
-
|
|
355
|
-
Behaviors add reactive logic to forms. All imported from `reformer/behaviors`.
|
|
356
|
-
|
|
357
|
-
### computeFrom
|
|
358
|
-
|
|
359
|
-
Calculate field value from other fields:
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
import { computeFrom } from 'reformer/behaviors';
|
|
363
|
-
|
|
364
|
-
behavior: (path) => {
|
|
365
|
-
// total = price * quantity
|
|
366
|
-
computeFrom(
|
|
367
|
-
[path.price, path.quantity], // Watch these fields
|
|
368
|
-
path.total, // Update this field
|
|
369
|
-
({ price, quantity }) => price * quantity // Compute function
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### enableWhen / disableWhen
|
|
375
|
-
|
|
376
|
-
Conditional field enable/disable:
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
import { enableWhen, disableWhen } from 'reformer/behaviors';
|
|
380
|
-
|
|
381
|
-
behavior: (path) => {
|
|
382
|
-
// Enable discount field only when total > 500
|
|
383
|
-
enableWhen(path.discount, (form) => form.total > 500);
|
|
384
|
-
|
|
385
|
-
// Disable shipping when pickup is selected
|
|
386
|
-
disableWhen(path.shippingAddress, (form) => form.deliveryMethod === 'pickup');
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### watchField
|
|
391
|
-
|
|
392
|
-
React to field changes with custom logic:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
import { watchField } from 'reformer/behaviors';
|
|
396
|
-
|
|
397
|
-
behavior: (path) => {
|
|
398
|
-
// Load cities when country changes
|
|
399
|
-
watchField(path.country, async (value, ctx) => {
|
|
400
|
-
const cities = await fetchCities(value);
|
|
401
|
-
ctx.form.city.updateComponentProps({ options: cities });
|
|
402
|
-
ctx.form.city.setValue(''); // Reset city selection
|
|
403
|
-
}, { debounce: 300 });
|
|
404
|
-
}
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### copyFrom
|
|
408
|
-
|
|
409
|
-
Copy values from one field/group to another:
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
import { copyFrom } from 'reformer/behaviors';
|
|
413
|
-
|
|
414
|
-
behavior: (path) => {
|
|
415
|
-
// Copy billing address to shipping when checkbox is checked
|
|
416
|
-
copyFrom(path.billingAddress, path.shippingAddress, {
|
|
417
|
-
when: (form) => form.sameAsShipping === true,
|
|
418
|
-
fields: 'all', // or ['street', 'city', 'zip']
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### syncFields
|
|
424
|
-
|
|
425
|
-
Two-way field synchronization:
|
|
55
|
+
### Computed Field from Nested to Root Level
|
|
426
56
|
|
|
427
57
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
58
|
+
// DO NOT use computeFrom for cross-level computations
|
|
59
|
+
// Use watchField instead:
|
|
60
|
+
watchField(path.nested.field, (value, ctx) => {
|
|
61
|
+
ctx.setFieldValue('rootField', computedValue);
|
|
62
|
+
});
|
|
433
63
|
```
|
|
434
64
|
|
|
435
|
-
###
|
|
436
|
-
|
|
437
|
-
Reset field when condition is met:
|
|
65
|
+
### Type-Safe useFormControl
|
|
438
66
|
|
|
439
67
|
```typescript
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
behavior: (path) => {
|
|
443
|
-
// Reset city when country changes
|
|
444
|
-
resetWhen(path.city, [path.country]);
|
|
445
|
-
}
|
|
68
|
+
const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
446
69
|
```
|
|
447
70
|
|
|
448
|
-
|
|
71
|
+
## 4. ⚠️ COMMON MISTAKES
|
|
449
72
|
|
|
450
|
-
|
|
73
|
+
### Validators
|
|
451
74
|
|
|
452
75
|
```typescript
|
|
453
|
-
|
|
76
|
+
// ❌ WRONG
|
|
77
|
+
required(path.email, 'Email is required');
|
|
454
78
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
revalidateWhen(path.confirmPassword, [path.password]);
|
|
458
|
-
}
|
|
79
|
+
// ✅ CORRECT
|
|
80
|
+
required(path.email, { message: 'Email is required' });
|
|
459
81
|
```
|
|
460
82
|
|
|
461
|
-
###
|
|
462
|
-
|
|
463
|
-
Create reusable custom behaviors:
|
|
83
|
+
### Types
|
|
464
84
|
|
|
465
85
|
```typescript
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
interface AutoSaveOptions {
|
|
470
|
-
debounce?: number;
|
|
471
|
-
onSave: (data: any) => Promise<void>;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function autoSave<T>(options: AutoSaveOptions): Behavior<T> {
|
|
475
|
-
const { debounce = 1000, onSave } = options;
|
|
476
|
-
let timeoutId: NodeJS.Timeout;
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
key: 'autoSave',
|
|
480
|
-
paths: [], // Empty = listen to all fields
|
|
481
|
-
run: (values, ctx) => {
|
|
482
|
-
clearTimeout(timeoutId);
|
|
483
|
-
timeoutId = setTimeout(async () => {
|
|
484
|
-
await onSave(ctx.form.getValue());
|
|
485
|
-
}, debounce);
|
|
486
|
-
},
|
|
487
|
-
cleanup: () => clearTimeout(timeoutId),
|
|
488
|
-
};
|
|
489
|
-
}
|
|
86
|
+
// ❌ WRONG
|
|
87
|
+
amount: number | null;
|
|
88
|
+
[key: string]: unknown;
|
|
490
89
|
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
debounce: 2000,
|
|
495
|
-
onSave: async (data) => {
|
|
496
|
-
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
|
|
497
|
-
},
|
|
498
|
-
})),
|
|
499
|
-
];
|
|
90
|
+
// ✅ CORRECT
|
|
91
|
+
amount: number | undefined;
|
|
92
|
+
// No index signature
|
|
500
93
|
```
|
|
501
94
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
### Form Organization (Colocation)
|
|
505
|
-
|
|
506
|
-
```
|
|
507
|
-
src/forms/
|
|
508
|
-
├── user-profile/
|
|
509
|
-
│ ├── UserProfileForm.tsx # React component
|
|
510
|
-
│ ├── type.ts # TypeScript interfaces
|
|
511
|
-
│ ├── schema.ts # Form schema (createForm)
|
|
512
|
-
│ ├── validators.ts # Validation rules
|
|
513
|
-
│ ├── behaviors.ts # Reactive behaviors
|
|
514
|
-
│ └── sub-forms/ # Reusable nested schemas
|
|
515
|
-
│ ├── address/
|
|
516
|
-
│ │ ├── type.ts
|
|
517
|
-
│ │ ├── schema.ts
|
|
518
|
-
│ │ ├── validators.ts
|
|
519
|
-
│ │ └── AddressForm.tsx
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
### Schema File Pattern
|
|
523
|
-
|
|
524
|
-
```typescript
|
|
525
|
-
// forms/user-profile/schema.ts
|
|
526
|
-
import { createForm } from 'reformer';
|
|
527
|
-
import { validation } from './validators';
|
|
528
|
-
import { behavior } from './behaviors';
|
|
529
|
-
import type { UserProfile } from './type';
|
|
530
|
-
|
|
531
|
-
export const createUserProfileForm = (initial?: Partial<UserProfile>) =>
|
|
532
|
-
createForm<UserProfile>({
|
|
533
|
-
form: {
|
|
534
|
-
name: { value: initial?.name ?? '', component: Input },
|
|
535
|
-
email: { value: initial?.email ?? '', component: Input },
|
|
536
|
-
// ...
|
|
537
|
-
},
|
|
538
|
-
validation,
|
|
539
|
-
behavior,
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Multi-step Form Structure
|
|
544
|
-
|
|
545
|
-
```
|
|
546
|
-
src/forms/checkout/
|
|
547
|
-
├── CheckoutForm.tsx # Main form component
|
|
548
|
-
├── type.ts # Combined type
|
|
549
|
-
├── schema.ts # Combined schema
|
|
550
|
-
├── validators.ts # Combined + cross-step validators
|
|
551
|
-
├── behaviors.ts # Combined + cross-step behaviors
|
|
552
|
-
├── steps/
|
|
553
|
-
│ ├── shipping/
|
|
554
|
-
│ │ ├── type.ts
|
|
555
|
-
│ │ ├── schema.ts
|
|
556
|
-
│ │ ├── validators.ts
|
|
557
|
-
│ │ └── ShippingStep.tsx
|
|
558
|
-
│ ├── payment/
|
|
559
|
-
│ └── confirmation/
|
|
560
|
-
└── hooks/
|
|
561
|
-
└── useCheckoutNavigation.ts
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
## React Integration
|
|
565
|
-
|
|
566
|
-
### useFormControl<T>
|
|
567
|
-
|
|
568
|
-
Subscribe to all field state changes:
|
|
569
|
-
|
|
570
|
-
```typescript
|
|
571
|
-
import { useFormControl } from 'reformer';
|
|
572
|
-
|
|
573
|
-
function TextField({ field }: { field: FieldNode<string> }) {
|
|
574
|
-
const {
|
|
575
|
-
value, // Current value
|
|
576
|
-
valid, // Is valid
|
|
577
|
-
invalid, // Has errors
|
|
578
|
-
errors, // ValidationError[]
|
|
579
|
-
touched, // User interacted
|
|
580
|
-
disabled, // Is disabled
|
|
581
|
-
pending, // Async validation running
|
|
582
|
-
shouldShowError, // Show error (touched && invalid)
|
|
583
|
-
componentProps, // Custom props from schema
|
|
584
|
-
} = useFormControl(field);
|
|
585
|
-
|
|
586
|
-
return (
|
|
587
|
-
<div>
|
|
588
|
-
<input
|
|
589
|
-
value={value}
|
|
590
|
-
onChange={(e) => field.setValue(e.target.value)}
|
|
591
|
-
onBlur={() => field.markAsTouched()}
|
|
592
|
-
disabled={disabled}
|
|
593
|
-
/>
|
|
594
|
-
{shouldShowError && errors[0] && (
|
|
595
|
-
<span className="error">{errors[0].message}</span>
|
|
596
|
-
)}
|
|
597
|
-
</div>
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
### useFormControlValue<T>
|
|
603
|
-
|
|
604
|
-
Lightweight hook - returns only value (better performance):
|
|
95
|
+
### computeFrom
|
|
605
96
|
|
|
606
97
|
```typescript
|
|
607
|
-
|
|
98
|
+
// ❌ WRONG - different nesting levels
|
|
99
|
+
computeFrom([path.nested.a, path.nested.b], path.root, ...)
|
|
608
100
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (showField !== 'yes') return null;
|
|
614
|
-
return <TextField field={field} />;
|
|
615
|
-
}
|
|
101
|
+
// ✅ CORRECT - use watchField
|
|
102
|
+
watchField(path.nested.a, (_, ctx) => {
|
|
103
|
+
ctx.setFieldValue('root', computed);
|
|
104
|
+
});
|
|
616
105
|
```
|
|
617
106
|
|
|
618
|
-
###
|
|
619
|
-
|
|
620
|
-
- Uses `useSyncExternalStore` for React 18+ integration
|
|
621
|
-
- Fine-grained updates - only affected components re-render
|
|
622
|
-
- Memoized state objects prevent unnecessary re-renders
|
|
623
|
-
- Use `useFormControlValue` when you only need the value
|
|
624
|
-
|
|
625
|
-
## API Reference
|
|
626
|
-
|
|
627
|
-
### createForm<T>(config)
|
|
628
|
-
|
|
629
|
-
Creates a new form instance with type-safe proxy access.
|
|
107
|
+
### Imports
|
|
630
108
|
|
|
631
109
|
```typescript
|
|
632
|
-
|
|
110
|
+
// ❌ WRONG - types are not in submodules
|
|
111
|
+
import { ValidationSchemaFn } from '@reformer/core/validators';
|
|
633
112
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
behavior?: BehaviorSchemaFn<T>;
|
|
638
|
-
}
|
|
113
|
+
// ✅ CORRECT - types from main module
|
|
114
|
+
import type { ValidationSchemaFn } from '@reformer/core';
|
|
115
|
+
import { required, email } from '@reformer/core/validators';
|
|
639
116
|
```
|
|
640
117
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
All nodes have these Signal properties:
|
|
644
|
-
- `value` - Current value
|
|
645
|
-
- `valid` / `invalid` - Validation state
|
|
646
|
-
- `touched` / `untouched` - Interaction state
|
|
647
|
-
- `dirty` / `pristine` - Changed state
|
|
648
|
-
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
649
|
-
- `disabled` - Is disabled
|
|
650
|
-
- `pending` - Async validation in progress
|
|
118
|
+
## 5. TROUBLESHOOTING
|
|
651
119
|
|
|
652
|
-
|
|
120
|
+
| Error | Cause | Solution |
|
|
121
|
+
| ------------------------------------------------------ | ------------------------------ | --------------------------------- |
|
|
122
|
+
| `'string' is not assignable to '{ message?: string }'` | Wrong validator format | Use `{ message: 'text' }` |
|
|
123
|
+
| `'null' is not assignable to 'undefined'` | Wrong optional type | Replace `null` with `undefined` |
|
|
124
|
+
| `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
|
|
125
|
+
| `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
|
|
126
|
+
| `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
|
|
653
127
|
|
|
654
|
-
|
|
655
|
-
setValue(value: T, options?: SetValueOptions): void
|
|
656
|
-
reset(): void
|
|
657
|
-
markAsTouched(): void
|
|
658
|
-
markAsDirty(): void
|
|
659
|
-
disable(): void
|
|
660
|
-
enable(): void
|
|
661
|
-
validate(): Promise<void>
|
|
662
|
-
getErrors(filter?: (error: ValidationError) => boolean): ValidationError[]
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### SetValueOptions
|
|
128
|
+
## 6. COMPLETE IMPORT EXAMPLE
|
|
666
129
|
|
|
667
130
|
```typescript
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
131
|
+
// Types - always from @reformer/core
|
|
132
|
+
import type {
|
|
133
|
+
ValidationSchemaFn,
|
|
134
|
+
BehaviorSchemaFn,
|
|
135
|
+
FieldPath,
|
|
136
|
+
GroupNodeWithControls,
|
|
137
|
+
FieldNode,
|
|
138
|
+
} from '@reformer/core';
|
|
673
139
|
|
|
674
|
-
|
|
140
|
+
// Core functions
|
|
141
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
675
142
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
code: string; // Error identifier
|
|
679
|
-
message: string; // Human-readable message
|
|
680
|
-
params?: Record<string, any>; // Additional error data
|
|
681
|
-
severity?: 'error' | 'warning'; // Severity level
|
|
682
|
-
path?: string; // Field path (for cross-field)
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
### FieldStatus
|
|
143
|
+
// Validators - from /validators submodule
|
|
144
|
+
import { required, min, max, email, validate, when } from '@reformer/core/validators';
|
|
687
145
|
|
|
688
|
-
|
|
689
|
-
|
|
146
|
+
// Behaviors - from /behaviors submodule
|
|
147
|
+
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
|
|
690
148
|
```
|
|
691
149
|
|
|
692
|
-
|
|
150
|
+
## 7. FORM TYPE DEFINITION
|
|
693
151
|
|
|
694
152
|
```typescript
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const type = getNodeType(node); // 'field' | 'group' | 'array'
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
## Common Patterns
|
|
153
|
+
// ✅ CORRECT form type definition
|
|
154
|
+
interface MyForm {
|
|
155
|
+
// Required fields
|
|
156
|
+
name: string;
|
|
157
|
+
email: string;
|
|
704
158
|
|
|
705
|
-
|
|
159
|
+
// Optional fields - use undefined, not null
|
|
160
|
+
phone?: string;
|
|
161
|
+
age?: number;
|
|
706
162
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const form = useMemo(() => createCheckoutForm(), []);
|
|
710
|
-
const [step, setStep] = useState(0);
|
|
711
|
-
|
|
712
|
-
const validateStep = async () => {
|
|
713
|
-
const stepFields = getStepFields(step);
|
|
714
|
-
stepFields.forEach(f => f.markAsTouched());
|
|
715
|
-
await form.validate();
|
|
716
|
-
return stepFields.every(f => f.valid.value);
|
|
717
|
-
};
|
|
163
|
+
// Enum/union types
|
|
164
|
+
status: 'active' | 'inactive';
|
|
718
165
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
166
|
+
// Nested objects
|
|
167
|
+
address: {
|
|
168
|
+
street: string;
|
|
169
|
+
city: string;
|
|
723
170
|
};
|
|
724
171
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
<button onClick={() => setStep(s => s - 1)} disabled={step === 0}>
|
|
732
|
-
Back
|
|
733
|
-
</button>
|
|
734
|
-
<button onClick={handleNext}>
|
|
735
|
-
{step === 2 ? 'Submit' : 'Next'}
|
|
736
|
-
</button>
|
|
737
|
-
</div>
|
|
738
|
-
);
|
|
172
|
+
// Arrays
|
|
173
|
+
items: Array<{
|
|
174
|
+
id: string;
|
|
175
|
+
name: string;
|
|
176
|
+
}>;
|
|
739
177
|
}
|
|
740
178
|
```
|
|
741
|
-
|
|
742
|
-
### Nested Form with Reusable Schema
|
|
743
|
-
|
|
744
|
-
```typescript
|
|
745
|
-
// sub-forms/address/schema.ts
|
|
746
|
-
export const addressSchema = {
|
|
747
|
-
street: { value: '', component: Input, componentProps: { label: 'Street' } },
|
|
748
|
-
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
749
|
-
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
// main form
|
|
753
|
-
const form = createForm<OrderForm>({
|
|
754
|
-
form: {
|
|
755
|
-
billingAddress: addressSchema,
|
|
756
|
-
shippingAddress: addressSchema,
|
|
757
|
-
},
|
|
758
|
-
});
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
### Dynamic Array (Add/Remove Items)
|
|
762
|
-
|
|
763
|
-
```typescript
|
|
764
|
-
function PhoneList({ array }: { array: ArrayNode<Phone> }) {
|
|
765
|
-
const { length } = useFormControl(array);
|
|
766
|
-
|
|
767
|
-
return (
|
|
768
|
-
<div>
|
|
769
|
-
{array.controls.map((phone, index) => (
|
|
770
|
-
<div key={phone.id}>
|
|
771
|
-
<FormField field={phone.controls.type} />
|
|
772
|
-
<FormField field={phone.controls.number} />
|
|
773
|
-
<button onClick={() => array.removeAt(index)}>Remove</button>
|
|
774
|
-
</div>
|
|
775
|
-
))}
|
|
776
|
-
|
|
777
|
-
<button onClick={() => array.push({ type: 'mobile', number: '' })}>
|
|
778
|
-
Add Phone
|
|
779
|
-
</button>
|
|
780
|
-
</div>
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Conditional Fields
|
|
786
|
-
|
|
787
|
-
```typescript
|
|
788
|
-
behavior: (path) => {
|
|
789
|
-
// Show company fields only for business accounts
|
|
790
|
-
enableWhen(path.companyName, (form) => form.accountType === 'business');
|
|
791
|
-
enableWhen(path.taxId, (form) => form.accountType === 'business');
|
|
792
|
-
|
|
793
|
-
// Reset company fields when switching to personal
|
|
794
|
-
resetWhen(path.companyName, [path.accountType]);
|
|
795
|
-
resetWhen(path.taxId, [path.accountType]);
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
## Troubleshooting / FAQ
|
|
800
|
-
|
|
801
|
-
### Q: Field not updating in React?
|
|
802
|
-
A: Ensure you're using `useFormControl()` hook to subscribe to changes. Direct signal access (`.value.value`) won't trigger re-renders.
|
|
803
|
-
|
|
804
|
-
### Q: Validation not triggering?
|
|
805
|
-
A: Check `updateOn` option in field config. Default is 'change'. For blur-triggered validation use `updateOn: 'blur'`.
|
|
806
|
-
|
|
807
|
-
### Q: How to access nested field by path string?
|
|
808
|
-
A: Use `form.getFieldByPath('address.city')` for dynamic string-based access. For type-safe access use proxy: `form.address.city`.
|
|
809
|
-
|
|
810
|
-
### Q: TypeScript errors with schema?
|
|
811
|
-
A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
|
|
812
|
-
|
|
813
|
-
### Q: How to reset form to initial values?
|
|
814
|
-
A: Call `form.reset()` for single field or `form.resetAll()` for GroupNode to reset all children.
|
|
815
|
-
|
|
816
|
-
### Q: How to get all form values?
|
|
817
|
-
A: Access `form.value.value` (it's a Signal) or use `form.getValue()` method.
|
|
818
|
-
|
|
819
|
-
### Q: How to programmatically set multiple values?
|
|
820
|
-
A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
|
|
821
|
-
|
|
822
|
-
### Q: Form instance recreated on every render?
|
|
823
|
-
A: Wrap `createForm()` in `useMemo()`:
|
|
824
|
-
```typescript
|
|
825
|
-
const form = useMemo(() => createForm<MyForm>({ form: schema }), []);
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
### Q: How to handle form submission?
|
|
829
|
-
A:
|
|
830
|
-
```typescript
|
|
831
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
832
|
-
e.preventDefault();
|
|
833
|
-
form.markAsTouched(); // Show all errors
|
|
834
|
-
await form.validate(); // Run all validators
|
|
835
|
-
|
|
836
|
-
if (form.valid.value) {
|
|
837
|
-
const data = form.value.value;
|
|
838
|
-
await submitToServer(data);
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
## Links
|
|
844
|
-
|
|
845
|
-
- Repository: https://github.com/AlexandrBukhtatyy/ReFormer
|
|
846
|
-
- Documentation: https://alexandrbukhtatyy.github.io/ReFormer/
|
|
847
|
-
- Issues: https://github.com/AlexandrBukhtatyy/ReFormer/issues
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reformer/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Reactive form state management library for React with signals-based architecture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,14 +60,15 @@
|
|
|
60
60
|
"dist",
|
|
61
61
|
"README.md",
|
|
62
62
|
"LICENSE",
|
|
63
|
-
"
|
|
63
|
+
"LLMs.txt"
|
|
64
64
|
],
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"react": "^18.0.0 || ^19.0.0",
|
|
67
67
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@preact/signals-core": "^1.8.0"
|
|
70
|
+
"@preact/signals-core": "^1.8.0",
|
|
71
|
+
"uuid": "^13.0.0"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@types/node": "^24.10.1",
|
|
@@ -76,8 +77,8 @@
|
|
|
76
77
|
"@types/uuid": "^10.0.0",
|
|
77
78
|
"@vitejs/plugin-react": "^5.1.0",
|
|
78
79
|
"@vitest/utils": "^4.0.8",
|
|
79
|
-
"react": "^19.2.
|
|
80
|
-
"react-dom": "^19.2.
|
|
80
|
+
"react": "^19.2.1",
|
|
81
|
+
"react-dom": "^19.2.1",
|
|
81
82
|
"typescript": "^5.9.3",
|
|
82
83
|
"vite": "^7.2.2",
|
|
83
84
|
"vite-plugin-dts": "^4.5.4",
|