@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.
Files changed (3) hide show
  1. package/README.md +57 -48
  2. package/llms.txt +110 -779
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -3,10 +3,13 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@reformer/core.svg)](https://www.npmjs.com/package/@reformer/core)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@reformer/core.svg)](https://www.npmjs.com/package/@reformer/core)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
- [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz_small.svg)](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
- // 1. Define form interface
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
- // 3. Registration form component
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
- // Form schema
73
- form: {
74
- username: { value: '' },
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
- > 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)
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
- **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
13
+ ### Type Values
183
14
 
184
- ### GroupNode<T>
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
- Groups multiple fields into an object.
19
+ ## 2. API SIGNATURES
187
20
 
188
- **Properties:**
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
- // Type-safe field access via proxy
201
- form.name // FieldNode<string>
202
- form.address.city // FieldNode<string>
203
- form.phones // ArrayNode
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
- ### ArrayNode<T>
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
- // Usage
224
- form.phones.push({ type: 'work', number: '' });
225
- form.phones.removeAt(0);
226
- form.phones.at(0).controls.number.setValue('123-456');
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
- ## Validation
45
+ ## 3. COMMON PATTERNS
230
46
 
231
- ### ValidationSchemaFn
47
+ ### Conditional Fields with Auto-Reset
232
48
 
233
49
  ```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
- },
50
+ enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
51
+ resetOnDisable: true,
257
52
  });
258
53
  ```
259
54
 
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
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
- import { syncFields } from 'reformer/behaviors';
429
-
430
- behavior: (path) => {
431
- syncFields(path.field1, path.field2);
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
- ### resetWhen
436
-
437
- Reset field when condition is met:
65
+ ### Type-Safe useFormControl
438
66
 
439
67
  ```typescript
440
- import { resetWhen } from 'reformer/behaviors';
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
- ### revalidateWhen
71
+ ## 4. ⚠️ COMMON MISTAKES
449
72
 
450
- Trigger revalidation when another field changes:
73
+ ### Validators
451
74
 
452
75
  ```typescript
453
- import { revalidateWhen } from 'reformer/behaviors';
76
+ // WRONG
77
+ required(path.email, 'Email is required');
454
78
 
455
- behavior: (path) => {
456
- // Revalidate confirmPassword when password changes
457
- revalidateWhen(path.confirmPassword, [path.password]);
458
- }
79
+ // CORRECT
80
+ required(path.email, { message: 'Email is required' });
459
81
  ```
460
82
 
461
- ### Custom Behavior
462
-
463
- Create reusable custom behaviors:
83
+ ### Types
464
84
 
465
85
  ```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
- }
86
+ // ❌ WRONG
87
+ amount: number | null;
88
+ [key: string]: unknown;
490
89
 
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) });
497
- },
498
- })),
499
- ];
90
+ // ✅ CORRECT
91
+ amount: number | undefined;
92
+ // No index signature
500
93
  ```
501
94
 
502
- ## Recommended Project Structure
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
- import { useFormControlValue } from 'reformer';
98
+ // WRONG - different nesting levels
99
+ computeFrom([path.nested.a, path.nested.b], path.root, ...)
608
100
 
609
- function ConditionalField({ trigger, field }) {
610
- // Re-renders only when trigger value changes
611
- const showField = useFormControlValue(trigger);
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
- ### 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.
107
+ ### Imports
630
108
 
631
109
  ```typescript
632
- function createForm<T>(config: GroupNodeConfig<T>): GroupNodeWithControls<T>
110
+ // WRONG - types are not in submodules
111
+ import { ValidationSchemaFn } from '@reformer/core/validators';
633
112
 
634
- interface GroupNodeConfig<T> {
635
- form: FormSchema<T>;
636
- validation?: ValidationSchemaFn<T>;
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
- ### 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
118
+ ## 5. TROUBLESHOOTING
651
119
 
652
- ### Node Common Methods
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
- ```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
- ```
664
-
665
- ### SetValueOptions
128
+ ## 6. COMPLETE IMPORT EXAMPLE
666
129
 
667
130
  ```typescript
668
- interface SetValueOptions {
669
- emitEvent?: boolean; // Trigger change events (default: true)
670
- onlySelf?: boolean; // Don't propagate to parent (default: false)
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
- ### ValidationError
140
+ // Core functions
141
+ import { createForm, useFormControl } from '@reformer/core';
675
142
 
676
- ```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
- }
684
- ```
685
-
686
- ### FieldStatus
143
+ // Validators - from /validators submodule
144
+ import { required, min, max, email, validate, when } from '@reformer/core/validators';
687
145
 
688
- ```typescript
689
- type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
146
+ // Behaviors - from /behaviors submodule
147
+ import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
690
148
  ```
691
149
 
692
- ### Type Guards
150
+ ## 7. FORM TYPE DEFINITION
693
151
 
694
152
  ```typescript
695
- import { isFieldNode, isGroupNode, isArrayNode, getNodeType } from 'reformer';
696
-
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'
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
- ### Multi-step Form
159
+ // Optional fields - use undefined, not null
160
+ phone?: string;
161
+ age?: number;
706
162
 
707
- ```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
- };
163
+ // Enum/union types
164
+ status: 'active' | 'inactive';
718
165
 
719
- const handleNext = async () => {
720
- if (await validateStep()) {
721
- setStep(s => s + 1);
722
- }
166
+ // Nested objects
167
+ address: {
168
+ street: string;
169
+ city: string;
723
170
  };
724
171
 
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
- );
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.0.0",
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
- "llms.txt"
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.0",
80
- "react-dom": "^19.2.0",
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",