@reformer/core 1.0.0 → 1.1.0-beta.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/llms.txt CHANGED
@@ -1,847 +1,606 @@
1
- # ReFormer
1
+ # ReFormer - LLM Integration Guide
2
2
 
3
- > Signals-based reactive form state management library for React.
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
- ## Installation
5
+ ### Imports (CRITICALLY IMPORTANT)
8
6
 
9
- ```bash
10
- npm install reformer
11
- ```
12
-
13
- Peer dependencies:
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)
7
+ | What | Where |
8
+ | ------------------------------------------------------------------------------------------- | --------------------------- |
9
+ | `createForm`, `useFormControl`, `useFormControlValue` | `@reformer/core` |
10
+ | `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
11
+ | `FormSchema`, `FieldConfig`, `ArrayNode` | `@reformer/core` |
12
+ | `required`, `min`, `max`, `minLength`, `maxLength`, `email` | `@reformer/core/validators` |
13
+ | `pattern`, `url`, `phone`, `number`, `date` | `@reformer/core/validators` |
14
+ | `validate`, `validateAsync`, `validateTree`, `applyWhen` | `@reformer/core/validators` |
15
+ | `notEmpty`, `validateItems` | `@reformer/core/validators` |
16
+ | `computeFrom`, `enableWhen`, `disableWhen`, `watchField`, `copyFrom` | `@reformer/core/behaviors` |
17
+ | `resetWhen`, `revalidateWhen`, `syncFields` | `@reformer/core/behaviors` |
18
+ | `transformValue`, `transformers` | `@reformer/core/behaviors` |
81
19
 
82
- ### Signals-based Reactivity
20
+ ### Type Values
83
21
 
84
- - Uses @preact/signals-core for fine-grained reactivity
85
- - Only affected components re-render when values change
86
- - React integration via useSyncExternalStore
22
+ - Optional numbers: `number | undefined` (NOT `null`)
23
+ - Optional strings: `string` (empty string by default)
24
+ - Do NOT add `[key: string]: unknown` to form interfaces
87
25
 
88
- ## Form Schema
26
+ ## 2. API SIGNATURES
89
27
 
90
- ### FieldConfig<T>
28
+ ### Validators
91
29
 
92
30
  ```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
- ```
31
+ // Basic validators
32
+ required(path, options?: { message?: string })
33
+ min(path, value: number, options?: { message?: string })
34
+ max(path, value: number, options?: { message?: string })
35
+ minLength(path, length: number, options?: { message?: string })
36
+ maxLength(path, length: number, options?: { message?: string })
37
+ email(path, options?: { message?: string })
104
38
 
105
- ### ArrayConfig<T>
39
+ // Additional validators
40
+ pattern(path, regex: RegExp, options?: { message?: string })
41
+ url(path, options?: { message?: string })
42
+ phone(path, options?: { message?: string; format?: PhoneFormat })
43
+ number(path, options?: { message?: string })
44
+ date(path, options?: { message?: string; minAge?: number; maxAge?: number; noFuture?: boolean; noPast?: boolean })
106
45
 
107
- Arrays use single-element tuple syntax in schema:
46
+ // Custom validators
47
+ validate(path, validator: (value, ctx) => ValidationError | null)
48
+ validateAsync(path, validator: async (value, ctx) => ValidationError | null)
49
+ validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
108
50
 
109
- ```typescript
110
- interface FormType {
111
- phones: { type: string; number: string }[];
112
- }
51
+ // Conditional validation
52
+ applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
113
53
 
114
- const schema: FormSchema<FormType> = {
115
- phones: [{
116
- type: { value: 'mobile', component: Select },
117
- number: { value: '', component: Input },
118
- }],
119
- };
54
+ // Array validators
55
+ notEmpty(path, options?: { message?: string })
56
+ validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
120
57
  ```
121
58
 
122
- ### Complete Schema Example
59
+ ### Behaviors
123
60
 
124
61
  ```typescript
125
- import { createForm } from 'reformer';
62
+ // Enable/disable fields conditionally
63
+ enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
64
+ disableWhen(path, condition: (form) => boolean)
126
65
 
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
- ```
66
+ // Computed fields (same nesting level)
67
+ computeFrom(sourcePaths[], targetPath, compute: (values) => result, options?: { debounce?: number; condition?: (form) => boolean })
156
68
 
157
- ## Node Types
69
+ // Watch field changes
70
+ watchField(path, callback: (value, ctx: BehaviorContext) => void, options?: { immediate?: boolean; debounce?: number })
158
71
 
159
- ### FieldNode<T>
72
+ // Copy values between fields
73
+ copyFrom(sourcePath, targetPath, options?: { when?: (form) => boolean; fields?: string[]; transform?: (value) => value })
160
74
 
161
- Represents a single form field value.
75
+ // Reset field when condition met
76
+ resetWhen(path, condition: (form) => boolean, options?: { toValue?: any })
162
77
 
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
78
+ // Re-validate when another field changes
79
+ revalidateWhen(triggerPath, targetPath)
174
80
 
175
- **Methods:**
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
81
+ // Sync multiple fields
82
+ syncFields(paths[], options?: { bidirectional?: boolean })
183
83
 
184
- ### GroupNode<T>
84
+ // Transform values
85
+ transformValue(path, transformer: (value) => value, options?: { on?: 'change' | 'blur' })
86
+ transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transformers.toNumber
185
87
 
186
- Groups multiple fields into an object.
187
-
188
- **Properties:**
189
- - `controls` - Dictionary of child nodes
190
- - All FormNode properties (computed from children)
191
-
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
- ```typescript
200
- // Type-safe field access via proxy
201
- form.name // FieldNode<string>
202
- form.address.city // FieldNode<string>
203
- form.phones // ArrayNode
88
+ // BehaviorContext interface:
89
+ interface BehaviorContext<TForm> {
90
+ form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
91
+ setFieldValue: (path: string, value: any) => void;
92
+ getFieldValue: (path: string) => unknown;
93
+ }
204
94
  ```
205
95
 
206
- ### ArrayNode<T>
96
+ ## 3. COMMON PATTERNS
207
97
 
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
98
+ ### Conditional Fields with Auto-Reset
221
99
 
222
100
  ```typescript
223
- // Usage
224
- form.phones.push({ type: 'work', number: '' });
225
- form.phones.removeAt(0);
226
- form.phones.at(0).controls.number.setValue('123-456');
101
+ enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
102
+ resetOnDisable: true,
103
+ });
227
104
  ```
228
105
 
229
- ## Validation
230
-
231
- ### ValidationSchemaFn
106
+ ### Computed Field from Nested to Root Level
232
107
 
233
108
  ```typescript
234
- import { required, email, minLength, validate, validateAsync } from 'reformer/validators';
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
- },
109
+ // DO NOT use computeFrom for cross-level computations
110
+ // Use watchField instead:
111
+ watchField(path.nested.field, (value, ctx) => {
112
+ ctx.setFieldValue('rootField', computedValue);
257
113
  });
258
114
  ```
259
115
 
260
- ### Built-in Validators
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
116
+ ### Type-Safe useFormControl
279
117
 
280
118
  ```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
- }
119
+ const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
304
120
  ```
305
121
 
306
- ### Async Validation Example
307
-
308
- ```typescript
309
- // Check username availability on server
310
- validation: (path) => {
311
- required(path.username);
122
+ ## 4. ⚠️ COMMON MISTAKES
312
123
 
313
- validateAsync(path.username, async (value, ctx) => {
314
- if (!value || value.length < 3) return null;
124
+ ### Validators
315
125
 
316
- const response = await fetch(`/api/check-username?u=${value}`);
317
- const { available } = await response.json();
126
+ ```typescript
127
+ // WRONG
128
+ required(path.email, 'Email is required');
318
129
 
319
- if (!available) {
320
- return { code: 'usernameTaken', message: 'Username is already taken' };
321
- }
322
- return null;
323
- }, { debounce: 500 });
324
- }
130
+ // CORRECT
131
+ required(path.email, { message: 'Email is required' });
325
132
  ```
326
133
 
327
- ### Cross-field Validation
134
+ ### Types
328
135
 
329
136
  ```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
- ```
137
+ // WRONG
138
+ amount: number | null;
139
+ [key: string]: unknown;
352
140
 
353
- ## Behaviors
354
-
355
- Behaviors add reactive logic to forms. All imported from `reformer/behaviors`.
141
+ // ✅ CORRECT
142
+ amount: number | undefined;
143
+ // No index signature
144
+ ```
356
145
 
357
146
  ### computeFrom
358
147
 
359
- Calculate field value from other fields:
360
-
361
148
  ```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
- ```
149
+ // WRONG - different nesting levels
150
+ computeFrom([path.nested.a, path.nested.b], path.root, ...)
373
151
 
374
- ### enableWhen / disableWhen
152
+ // CORRECT - use watchField
153
+ watchField(path.nested.a, (_, ctx) => {
154
+ ctx.setFieldValue('root', computed);
155
+ });
156
+ ```
375
157
 
376
- Conditional field enable/disable:
158
+ ### Imports
377
159
 
378
160
  ```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);
161
+ // WRONG - types are not in submodules
162
+ import { ValidationSchemaFn } from '@reformer/core/validators';
384
163
 
385
- // Disable shipping when pickup is selected
386
- disableWhen(path.shippingAddress, (form) => form.deliveryMethod === 'pickup');
387
- }
164
+ // CORRECT - types from main module
165
+ import type { ValidationSchemaFn } from '@reformer/core';
166
+ import { required, email } from '@reformer/core/validators';
388
167
  ```
389
168
 
390
- ### watchField
169
+ ## 5. TROUBLESHOOTING
391
170
 
392
- React to field changes with custom logic:
171
+ | Error | Cause | Solution |
172
+ | ------------------------------------------------------ | ------------------------------ | --------------------------------- |
173
+ | `'string' is not assignable to '{ message?: string }'` | Wrong validator format | Use `{ message: 'text' }` |
174
+ | `'null' is not assignable to 'undefined'` | Wrong optional type | Replace `null` with `undefined` |
175
+ | `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
176
+ | `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
177
+ | `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
178
+
179
+ ## 6. COMPLETE IMPORT EXAMPLE
393
180
 
394
181
  ```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
- ```
182
+ // Types - always from @reformer/core
183
+ import type {
184
+ ValidationSchemaFn,
185
+ BehaviorSchemaFn,
186
+ FieldPath,
187
+ GroupNodeWithControls,
188
+ FieldNode,
189
+ } from '@reformer/core';
406
190
 
407
- ### copyFrom
191
+ // Core functions
192
+ import { createForm, useFormControl } from '@reformer/core';
408
193
 
409
- Copy values from one field/group to another:
194
+ // Validators - from /validators submodule
195
+ import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
410
196
 
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
- }
197
+ // Behaviors - from /behaviors submodule
198
+ import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
421
199
  ```
422
200
 
423
- ### syncFields
424
-
425
- Two-way field synchronization:
201
+ ## 7. FORM TYPE DEFINITION
426
202
 
427
203
  ```typescript
428
- import { syncFields } from 'reformer/behaviors';
429
-
430
- behavior: (path) => {
431
- syncFields(path.field1, path.field2);
432
- }
433
- ```
204
+ // CORRECT form type definition
205
+ interface MyForm {
206
+ // Required fields
207
+ name: string;
208
+ email: string;
434
209
 
435
- ### resetWhen
210
+ // Optional fields - use undefined, not null
211
+ phone?: string;
212
+ age?: number;
436
213
 
437
- Reset field when condition is met:
214
+ // Enum/union types
215
+ status: 'active' | 'inactive';
438
216
 
439
- ```typescript
440
- import { resetWhen } from 'reformer/behaviors';
217
+ // Nested objects
218
+ address: {
219
+ street: string;
220
+ city: string;
221
+ };
441
222
 
442
- behavior: (path) => {
443
- // Reset city when country changes
444
- resetWhen(path.city, [path.country]);
223
+ // Arrays - use tuple format for schema
224
+ items: Array<{
225
+ id: string;
226
+ name: string;
227
+ }>;
445
228
  }
446
229
  ```
447
230
 
448
- ### revalidateWhen
231
+ ## 8. FORMSCHEMA FORMAT (CRITICALLY IMPORTANT)
449
232
 
450
- Trigger revalidation when another field changes:
233
+ ⚠️ **Every field MUST have `value` and `component` properties!**
451
234
 
452
- ```typescript
453
- import { revalidateWhen } from 'reformer/behaviors';
235
+ ### FieldConfig Interface
454
236
 
455
- behavior: (path) => {
456
- // Revalidate confirmPassword when password changes
457
- revalidateWhen(path.confirmPassword, [path.password]);
237
+ ```typescript
238
+ interface FieldConfig<T> {
239
+ value: T | null; // Initial value (REQUIRED)
240
+ component: ComponentType; // React component (REQUIRED)
241
+ componentProps?: object; // Props passed to component
242
+ disabled?: boolean; // Disable field initially
243
+ validators?: ValidatorFn[]; // Sync validators
244
+ asyncValidators?: AsyncValidatorFn[]; // Async validators
245
+ updateOn?: 'change' | 'blur' | 'submit';
246
+ debounce?: number;
458
247
  }
459
248
  ```
460
249
 
461
- ### Custom Behavior
462
-
463
- Create reusable custom behaviors:
250
+ ### Primitive Fields
464
251
 
465
252
  ```typescript
466
- // behaviors/auto-save.ts
467
- import { Behavior } from 'reformer';
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
- }
253
+ import { Input, Select, Checkbox } from '@/components/ui';
490
254
 
491
- // Usage
492
- behaviors: (path, { use }) => [
493
- use(autoSave({
494
- debounce: 2000,
495
- onSave: async (data) => {
496
- await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
255
+ const schema: FormSchema<MyForm> = {
256
+ // String field
257
+ name: {
258
+ value: '', // Initial value (REQUIRED)
259
+ component: Input, // React component (REQUIRED)
260
+ componentProps: {
261
+ label: 'Name',
262
+ placeholder: 'Enter name',
497
263
  },
498
- })),
499
- ];
500
- ```
264
+ },
501
265
 
502
- ## Recommended Project Structure
266
+ // Number field (optional)
267
+ age: {
268
+ value: undefined, // Use undefined, NOT null
269
+ component: Input,
270
+ componentProps: { type: 'number', label: 'Age' },
271
+ },
503
272
 
504
- ### Form Organization (Colocation)
273
+ // Boolean field
274
+ agree: {
275
+ value: false,
276
+ component: Checkbox,
277
+ componentProps: { label: 'I agree to terms' },
278
+ },
505
279
 
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
280
+ // Enum/Select field
281
+ status: {
282
+ value: 'active',
283
+ component: Select,
284
+ componentProps: {
285
+ label: 'Status',
286
+ options: [
287
+ { value: 'active', label: 'Active' },
288
+ { value: 'inactive', label: 'Inactive' },
289
+ ],
290
+ },
291
+ },
292
+ };
520
293
  ```
521
294
 
522
- ### Schema File Pattern
295
+ ### Nested Objects
523
296
 
524
297
  ```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
- });
298
+ const schema: FormSchema<MyForm> = {
299
+ address: {
300
+ street: { value: '', component: Input, componentProps: { label: 'Street' } },
301
+ city: { value: '', component: Input, componentProps: { label: 'City' } },
302
+ zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
303
+ },
304
+ };
541
305
  ```
542
306
 
543
- ### Multi-step Form Structure
307
+ ### Arrays (Tuple Format)
544
308
 
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
309
+ ```typescript
310
+ const itemSchema = {
311
+ id: { value: '', component: Input, componentProps: { label: 'ID' } },
312
+ name: { value: '', component: Input, componentProps: { label: 'Name' } },
313
+ };
565
314
 
566
- ### useFormControl<T>
315
+ const schema: FormSchema<MyForm> = {
316
+ items: [itemSchema], // Array with ONE template item
317
+ };
318
+ ```
567
319
 
568
- Subscribe to all field state changes:
320
+ ### WRONG - This will NOT compile
569
321
 
570
322
  ```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
- }
323
+ // Missing value and component - TypeScript will error!
324
+ const schema = {
325
+ name: '', // Wrong
326
+ email: '', // ❌ Wrong
327
+ };
600
328
  ```
601
329
 
602
- ### useFormControlValue<T>
603
-
604
- Lightweight hook - returns only value (better performance):
330
+ ### createForm API
605
331
 
606
332
  ```typescript
607
- import { useFormControlValue } from 'reformer';
608
-
609
- function ConditionalField({ trigger, field }) {
610
- // Re-renders only when trigger value changes
611
- const showField = useFormControlValue(trigger);
333
+ // Full config with behavior and validation
334
+ const form = createForm<MyForm>({
335
+ form: formSchema, // Required: form schema with FieldConfig
336
+ behavior: behaviorSchema, // Optional: behavior rules
337
+ validation: validationSchema, // Optional: validation rules
338
+ });
612
339
 
613
- if (showField !== 'yes') return null;
614
- return <TextField field={field} />;
615
- }
340
+ // Access form controls
341
+ form.name.setValue('John');
342
+ form.address.city.value.value; // Get current value
343
+ form.items.push({ id: '1', name: 'Item' }); // Array operations
616
344
  ```
617
345
 
618
- ### Performance Notes
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.
346
+ ## 9. ARRAY SCHEMA FORMAT
630
347
 
631
348
  ```typescript
632
- function createForm<T>(config: GroupNodeConfig<T>): GroupNodeWithControls<T>
349
+ // CORRECT - use tuple format for arrays
350
+ const schema = {
351
+ items: [itemSchema] as [typeof itemSchema],
352
+ properties: [propertySchema] as [typeof propertySchema],
353
+ };
633
354
 
634
- interface GroupNodeConfig<T> {
635
- form: FormSchema<T>;
636
- validation?: ValidationSchemaFn<T>;
637
- behavior?: BehaviorSchemaFn<T>;
638
- }
355
+ // WRONG - object format is NOT supported
356
+ const schema = {
357
+ items: { schema: itemSchema, initialItems: [] }, // This will NOT work
358
+ };
639
359
  ```
640
360
 
641
- ### Node Common Properties
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
651
-
652
- ### Node Common Methods
361
+ ## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
653
362
 
654
363
  ```typescript
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
- ```
364
+ // CORRECT - async watchField with ALL safeguards
365
+ watchField(
366
+ path.parentField,
367
+ async (value, ctx) => {
368
+ if (!value) return; // Guard clause
664
369
 
665
- ### SetValueOptions
370
+ try {
371
+ const { data } = await fetchData(value);
372
+ ctx.form.dependentField.updateComponentProps({ options: data });
373
+ } catch (error) {
374
+ console.error('Failed:', error);
375
+ ctx.form.dependentField.updateComponentProps({ options: [] });
376
+ }
377
+ },
378
+ { immediate: false, debounce: 300 } // REQUIRED options
379
+ );
666
380
 
667
- ```typescript
668
- interface SetValueOptions {
669
- emitEvent?: boolean; // Trigger change events (default: true)
670
- onlySelf?: boolean; // Don't propagate to parent (default: false)
671
- }
381
+ // ❌ WRONG - missing safeguards
382
+ watchField(path.field, async (value, ctx) => {
383
+ const { data } = await fetchData(value); // Will fail silently!
384
+ });
672
385
  ```
673
386
 
674
- ### ValidationError
387
+ ### Required Options for async watchField:
388
+ - `immediate: false` - prevents execution during initialization
389
+ - `debounce: 300` - prevents excessive API calls (300-500ms recommended)
390
+ - Guard clause - skip if value is empty
391
+ - try-catch - handle errors explicitly
392
+
393
+ ## 11. ARRAY CLEANUP PATTERN
675
394
 
676
395
  ```typescript
677
- interface ValidationError {
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
- }
396
+ // CORRECT - cleanup array when checkbox unchecked
397
+ watchField(
398
+ path.hasItems,
399
+ (hasItems, ctx) => {
400
+ if (!hasItems && ctx.form.items) {
401
+ ctx.form.items.clear();
402
+ }
403
+ },
404
+ { immediate: false }
405
+ );
406
+
407
+ // ❌ WRONG - no immediate: false, no null check
408
+ watchField(path.hasItems, (hasItems, ctx) => {
409
+ if (!hasItems) ctx.form.items.clear(); // May crash on init!
410
+ });
684
411
  ```
685
412
 
686
- ### FieldStatus
413
+ ## 12. MULTI-STEP FORM VALIDATION
687
414
 
688
415
  ```typescript
689
- type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
690
- ```
416
+ // Step-specific validation schemas
417
+ const step1Validation: ValidationSchemaFn<Form> = (path) => {
418
+ required(path.loanType);
419
+ required(path.loanAmount);
420
+ };
691
421
 
692
- ### Type Guards
422
+ const step2Validation: ValidationSchemaFn<Form> = (path) => {
423
+ required(path.personalData.firstName);
424
+ required(path.personalData.lastName);
425
+ };
693
426
 
694
- ```typescript
695
- import { isFieldNode, isGroupNode, isArrayNode, getNodeType } from 'reformer';
427
+ // STEP_VALIDATIONS map for useStepForm hook
428
+ export const STEP_VALIDATIONS = {
429
+ 1: step1Validation,
430
+ 2: step2Validation,
431
+ };
696
432
 
697
- if (isFieldNode(node)) { /* node is FieldNode */ }
698
- if (isGroupNode(node)) { /* node is GroupNode */ }
699
- if (isArrayNode(node)) { /* node is ArrayNode */ }
700
- const type = getNodeType(node); // 'field' | 'group' | 'array'
433
+ // Full validation (combines all steps)
434
+ export const fullValidation: ValidationSchemaFn<Form> = (path) => {
435
+ step1Validation(path);
436
+ step2Validation(path);
437
+ };
701
438
  ```
702
439
 
703
- ## Common Patterns
440
+ ## 13. ⚠️ EXTENDED COMMON MISTAKES
704
441
 
705
- ### Multi-step Form
442
+ ### Behavior Composition (Cycle Error)
706
443
 
707
444
  ```typescript
708
- function MultiStepForm() {
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
- };
445
+ // ❌ WRONG - apply() in behavior causes "Cycle detected"
446
+ const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
447
+ apply(addressBehavior, path.address); // WILL FAIL!
448
+ };
718
449
 
719
- const handleNext = async () => {
720
- if (await validateStep()) {
721
- setStep(s => s + 1);
722
- }
723
- };
450
+ // CORRECT - inline or use setup function
451
+ const setupAddressBehavior = (path: FieldPath<Address>) => {
452
+ watchField(path.region, async (region, ctx) => {
453
+ // ...
454
+ }, { immediate: false });
455
+ };
724
456
 
725
- return (
726
- <div>
727
- {step === 0 && <ShippingStep form={form} />}
728
- {step === 1 && <PaymentStep form={form} />}
729
- {step === 2 && <ConfirmationStep form={form} />}
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
- );
739
- }
457
+ const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
458
+ setupAddressBehavior(path.address); // Works!
459
+ };
740
460
  ```
741
461
 
742
- ### Nested Form with Reusable Schema
462
+ ### Infinite Loop in watchField
743
463
 
744
464
  ```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
- },
465
+ // ❌ WRONG - causes infinite loop
466
+ watchField(path.field, (value, ctx) => {
467
+ ctx.form.field.setValue(value.toUpperCase()); // Loop!
758
468
  });
469
+
470
+ // ✅ CORRECT - write to different field OR add guard
471
+ watchField(path.input, (value, ctx) => {
472
+ const upper = value?.toUpperCase() || '';
473
+ if (ctx.form.display.value.value !== upper) {
474
+ ctx.form.display.setValue(upper);
475
+ }
476
+ }, { immediate: false });
759
477
  ```
760
478
 
761
- ### Dynamic Array (Add/Remove Items)
479
+ ### validateTree Typing
762
480
 
763
481
  ```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
- ```
482
+ // WRONG - implicit any
483
+ validateTree((ctx) => { ... });
784
484
 
785
- ### Conditional Fields
485
+ // CORRECT - explicit typing
486
+ validateTree((ctx: { form: MyForm }) => {
487
+ if (ctx.form.field1 > ctx.form.field2) {
488
+ return { code: 'error', message: 'Invalid' };
489
+ }
490
+ return null;
491
+ });
492
+ ```
786
493
 
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]);
494
+ ## 14. PROJECT STRUCTURE (COLOCATION)
495
+
496
+ ```
497
+ src/
498
+ ├── components/ui/ # Reusable UI components
499
+ │ ├── FormField.tsx
500
+ │ └── FormArrayManager.tsx
501
+
502
+ ├── forms/
503
+ │ └── [form-name]/ # Form module
504
+ │ ├── type.ts # Main form type
505
+ │ ├── schema.ts # Main schema
506
+ │ ├── validators.ts # Validators
507
+ │ ├── behaviors.ts # Behaviors
508
+ │ ├── [FormName]Form.tsx # Main component
509
+ │ │
510
+ │ ├── steps/ # Multi-step wizard
511
+ │ │ ├── loan-info/
512
+ │ │ │ ├── type.ts
513
+ │ │ │ ├── schema.ts
514
+ │ │ │ ├── validators.ts
515
+ │ │ │ ├── behaviors.ts
516
+ │ │ │ └── LoanInfoForm.tsx
517
+ │ │ └── ...
518
+ │ │
519
+ │ └── sub-forms/ # Reusable sub-forms
520
+ │ ├── address/
521
+ │ └── personal-data/
522
+ ```
523
+
524
+ ### Key Files
525
+
526
+ ```typescript
527
+ // forms/credit-application/type.ts
528
+ export type { LoanInfoStep } from './steps/loan-info/type';
529
+ export interface CreditApplicationForm {
530
+ loanType: LoanType;
531
+ loanAmount: number;
532
+ // ...
796
533
  }
797
- ```
798
534
 
799
- ## Troubleshooting / FAQ
535
+ // forms/credit-application/schema.ts
536
+ import { loanInfoSchema } from './steps/loan-info/schema';
537
+ export const creditApplicationSchema = {
538
+ ...loanInfoSchema,
539
+ monthlyPayment: { value: 0, disabled: true },
540
+ };
800
541
 
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.
542
+ // forms/credit-application/validators.ts
543
+ import { loanValidation } from './steps/loan-info/validators';
544
+ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
545
+ loanValidation(path);
546
+ // Cross-step validation...
547
+ };
548
+ ```
803
549
 
804
- ### Q: Validation not triggering?
805
- A: Check `updateOn` option in field config. Default is 'change'. For blur-triggered validation use `updateOn: 'blur'`.
550
+ ### Scaling
806
551
 
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`.
552
+ | Complexity | Structure |
553
+ |------------|-----------|
554
+ | Simple | Single file: `ContactForm.tsx` |
555
+ | Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
556
+ | Complex | Full colocation with `steps/` and `sub-forms/` |
809
557
 
810
- ### Q: TypeScript errors with schema?
811
- A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
558
+ ## 15. NON-EXISTENT API (DO NOT USE)
812
559
 
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.
560
+ ⚠️ **The following APIs do NOT exist in @reformer/core:**
815
561
 
816
- ### Q: How to get all form values?
817
- A: Access `form.value.value` (it's a Signal) or use `form.getValue()` method.
562
+ | Wrong | Correct | Notes |
563
+ |----------|-----------|-------|
564
+ | `useForm` | `createForm` | There is no useForm hook |
565
+ | `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
566
+ | `when()` | `applyWhen()` | Conditional validation function |
567
+ | `FormFields` | `FieldNode<T>` | Type for field nodes |
818
568
 
819
- ### Q: How to programmatically set multiple values?
820
- A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
569
+ ### Common Import Errors
821
570
 
822
- ### Q: Form instance recreated on every render?
823
- A: Wrap `createForm()` in `useMemo()`:
824
571
  ```typescript
825
- const form = useMemo(() => createForm<MyForm>({ form: schema }), []);
572
+ // WRONG - These do NOT exist
573
+ import { useForm } from '@reformer/core'; // NO!
574
+ import { when } from '@reformer/core/validators'; // NO!
575
+ import type { FieldSchema } from '@reformer/core'; // NO!
576
+ import type { FormFields } from '@reformer/core'; // NO!
577
+
578
+ // ✅ CORRECT
579
+ import { createForm, useFormControl } from '@reformer/core';
580
+ import { applyWhen } from '@reformer/core/validators';
581
+ import type { FieldConfig, FieldNode } from '@reformer/core';
826
582
  ```
827
583
 
828
- ### Q: How to handle form submission?
829
- A:
584
+ ### FormSchema Common Mistakes
585
+
830
586
  ```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
- }
587
+ // WRONG - Simple values don't work
588
+ const schema = {
589
+ name: '', // Missing { value, component }
590
+ email: '', // Missing { value, component }
840
591
  };
841
- ```
842
592
 
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
593
+ // ✅ CORRECT - Every field needs value and component
594
+ const schema: FormSchema<MyForm> = {
595
+ name: {
596
+ value: '',
597
+ component: Input,
598
+ componentProps: { label: 'Name' },
599
+ },
600
+ email: {
601
+ value: '',
602
+ component: Input,
603
+ componentProps: { label: 'Email', type: 'email' },
604
+ },
605
+ };
606
+ ```