@reformer/core 1.1.0-beta.2 → 1.1.0-beta.4
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/dist/behaviors.js +2 -2
- package/dist/core/behavior/behavior-context.d.ts +6 -2
- package/dist/core/behavior/behavior-context.js +7 -2
- package/dist/core/types/form-context.d.ts +10 -4
- package/dist/{create-field-path-CdPF3lIK.js → create-field-path-DcXDTWil.js} +61 -58
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/{node-factory-D7DOnSSN.js → node-factory-DYXIgJmW.js} +106 -89
- package/dist/validators.js +104 -119
- package/llms.txt +282 -5
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| What | Where |
|
|
8
8
|
| ------------------------------------------------------------------------------------------- | --------------------------- |
|
|
9
|
-
| `createForm`, `useFormControl`, `useFormControlValue`
|
|
9
|
+
| `createForm`, `useFormControl`, `useFormControlValue`, `validateForm` | `@reformer/core` |
|
|
10
10
|
| `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
|
|
11
11
|
| `FormSchema`, `FieldConfig`, `ArrayNode` | `@reformer/core` |
|
|
12
12
|
| `required`, `min`, `max`, `minLength`, `maxLength`, `email` | `@reformer/core/validators` |
|
|
@@ -23,6 +23,26 @@
|
|
|
23
23
|
- Optional strings: `string` (empty string by default)
|
|
24
24
|
- Do NOT add `[key: string]: unknown` to form interfaces
|
|
25
25
|
|
|
26
|
+
### React Hooks Comparison (CRITICALLY IMPORTANT)
|
|
27
|
+
|
|
28
|
+
| Hook | Return Type | Subscribes To | Use Case |
|
|
29
|
+
|------|-------------|---------------|----------|
|
|
30
|
+
| `useFormControl(field)` | `{ value, errors, disabled, touched, ... }` | All signals | Full field state, form inputs |
|
|
31
|
+
| `useFormControlValue(field)` | `T` (value directly) | Only value signal | Conditional rendering |
|
|
32
|
+
|
|
33
|
+
⚠️ **CRITICAL**: Do NOT destructure `useFormControlValue`! It returns `T` directly, NOT `{ value: T }`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// ❌ WRONG - will always be undefined!
|
|
37
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
38
|
+
|
|
39
|
+
// ✅ CORRECT
|
|
40
|
+
const loanType = useFormControlValue(control.loanType);
|
|
41
|
+
|
|
42
|
+
// ✅ CORRECT - useFormControl returns object, destructuring OK
|
|
43
|
+
const { value, errors, disabled } = useFormControl(control.loanType);
|
|
44
|
+
```
|
|
45
|
+
|
|
26
46
|
## 2. API SIGNATURES
|
|
27
47
|
|
|
28
48
|
### Validators
|
|
@@ -89,7 +109,7 @@ transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transform
|
|
|
89
109
|
interface BehaviorContext<TForm> {
|
|
90
110
|
form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
|
|
91
111
|
setFieldValue: (path: string, value: any) => void;
|
|
92
|
-
|
|
112
|
+
// ⚠️ To READ field values, use: ctx.form.fieldName.value.value
|
|
93
113
|
}
|
|
94
114
|
```
|
|
95
115
|
|
|
@@ -121,6 +141,35 @@ const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
|
121
141
|
|
|
122
142
|
## 4. ⚠️ COMMON MISTAKES
|
|
123
143
|
|
|
144
|
+
### useFormControlValue (CRITICAL)
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// ❌ WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
148
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
149
|
+
// Result: loanType is ALWAYS undefined! Conditional rendering will fail.
|
|
150
|
+
|
|
151
|
+
// ✅ CORRECT
|
|
152
|
+
const loanType = useFormControlValue(control.loanType);
|
|
153
|
+
|
|
154
|
+
// ✅ ALSO CORRECT - useFormControl returns object
|
|
155
|
+
const { value, errors } = useFormControl(control.loanType);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Reading Field Values in BehaviorContext (CRITICAL)
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// ❌ WRONG - getFieldValue does NOT exist!
|
|
162
|
+
watchField(path.amount, (amount, ctx) => {
|
|
163
|
+
const rate = ctx.getFieldValue('rate'); // ERROR: Property 'getFieldValue' does not exist
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ✅ CORRECT - use ctx.form.fieldName.value.value
|
|
167
|
+
watchField(path.amount, (amount, ctx) => {
|
|
168
|
+
const rate = ctx.form.rate.value.value; // Read via signal
|
|
169
|
+
ctx.setFieldValue('total', amount * rate);
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
124
173
|
### Validators
|
|
125
174
|
|
|
126
175
|
```typescript
|
|
@@ -345,19 +394,57 @@ form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
|
345
394
|
|
|
346
395
|
## 9. ARRAY SCHEMA FORMAT
|
|
347
396
|
|
|
397
|
+
**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.
|
|
398
|
+
|
|
348
399
|
```typescript
|
|
349
400
|
// ✅ CORRECT - use tuple format for arrays
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
401
|
+
// The template item defines the sub-form schema for each array element
|
|
402
|
+
const itemSchema = {
|
|
403
|
+
id: { value: '', component: Input },
|
|
404
|
+
name: { value: '', component: Input },
|
|
405
|
+
price: { value: 0, component: Input, componentProps: { type: 'number' } },
|
|
353
406
|
};
|
|
354
407
|
|
|
408
|
+
const schema: FormSchema<MyForm> = {
|
|
409
|
+
items: [itemSchema], // Array of sub-forms
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Each array item is a GroupNode (sub-form) with its own controls:
|
|
413
|
+
form.items.map((item) => {
|
|
414
|
+
// item is a sub-form (GroupNode) - access fields like nested form
|
|
415
|
+
item.name.setValue('New Name');
|
|
416
|
+
item.price.value.value; // Get current value
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
355
421
|
// ❌ WRONG - object format is NOT supported
|
|
356
422
|
const schema = {
|
|
357
423
|
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
358
424
|
};
|
|
359
425
|
```
|
|
360
426
|
|
|
427
|
+
### Array Item as Sub-Form
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// Validation for array items (each item is a sub-form)
|
|
431
|
+
validateItems(path.items, (itemPath) => {
|
|
432
|
+
// itemPath provides paths to sub-form fields
|
|
433
|
+
required(itemPath.name);
|
|
434
|
+
min(itemPath.price, 0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Render array items - each item is a sub-form
|
|
438
|
+
{form.items.map((item, index) => (
|
|
439
|
+
<div key={item.id}>
|
|
440
|
+
{/* item is a sub-form - use FormField for each field */}
|
|
441
|
+
<FormField control={item.name} />
|
|
442
|
+
<FormField control={item.price} />
|
|
443
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
444
|
+
</div>
|
|
445
|
+
))}
|
|
446
|
+
```
|
|
447
|
+
|
|
361
448
|
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
362
449
|
|
|
363
450
|
```typescript
|
|
@@ -604,3 +691,193 @@ const schema: FormSchema<MyForm> = {
|
|
|
604
691
|
},
|
|
605
692
|
};
|
|
606
693
|
```
|
|
694
|
+
|
|
695
|
+
## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
696
|
+
|
|
697
|
+
### Why .value.value?
|
|
698
|
+
|
|
699
|
+
ReFormer uses `@preact/signals-core` for reactivity:
|
|
700
|
+
- `field.value` → `Signal<T>` (reactive container)
|
|
701
|
+
- `field.value.value` → `T` (actual value)
|
|
702
|
+
- `field.getValue()` → `T` (shorthand method, non-reactive)
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
// Reading values in different contexts:
|
|
706
|
+
|
|
707
|
+
// In React components - use hooks
|
|
708
|
+
const { value } = useFormControl(control.email); // Object with value
|
|
709
|
+
const email = useFormControlValue(control.email); // Value directly
|
|
710
|
+
|
|
711
|
+
// In BehaviorContext (watchField, etc.)
|
|
712
|
+
watchField(path.firstName, (firstName, ctx) => {
|
|
713
|
+
// ⚠️ ctx.form is typed as the PARENT GROUP of the watched field!
|
|
714
|
+
// For path.nested.field: ctx.form = NestedType, NOT RootForm!
|
|
715
|
+
|
|
716
|
+
const lastName = ctx.form.lastName.value.value; // Read sibling field
|
|
717
|
+
|
|
718
|
+
// Use setFieldValue with full path for root-level fields
|
|
719
|
+
ctx.setFieldValue('fullName', `${firstName} ${lastName}`);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Direct access on form controls
|
|
723
|
+
form.email.value.value; // Read current value
|
|
724
|
+
form.address.city.value.value; // Read nested value
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Reading Nested Values in watchField
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
// ⚠️ IMPORTANT: ctx.form type depends on the watched path!
|
|
731
|
+
|
|
732
|
+
// Watching root-level field
|
|
733
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
734
|
+
// ctx.form is MyForm - can access all fields
|
|
735
|
+
const rate = ctx.form.interestRate.value.value;
|
|
736
|
+
ctx.setFieldValue('monthlyPayment', amount * rate / 12);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Watching nested field
|
|
740
|
+
watchField(path.personalData.lastName, (lastName, ctx) => {
|
|
741
|
+
// ctx.form is PersonalData, NOT MyForm!
|
|
742
|
+
const firstName = ctx.form.firstName.value.value; // ✅ Works
|
|
743
|
+
const middleName = ctx.form.middleName.value.value; // ✅ Works
|
|
744
|
+
|
|
745
|
+
// For root-level field, use setFieldValue with full path
|
|
746
|
+
ctx.setFieldValue('fullName', `${lastName} ${firstName}`);
|
|
747
|
+
});
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## 17. COMPUTE FROM vs WATCH FIELD
|
|
751
|
+
|
|
752
|
+
### computeFrom - Same Nesting Level Only
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// ✅ Works: all source fields and target at same level
|
|
756
|
+
computeFrom(
|
|
757
|
+
[path.price, path.quantity],
|
|
758
|
+
path.total,
|
|
759
|
+
({ price, quantity }) => (price || 0) * (quantity || 0)
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
// ✅ Works: all nested at same level
|
|
763
|
+
computeFrom(
|
|
764
|
+
[path.address.houseNumber, path.address.streetName],
|
|
765
|
+
path.address.fullAddress,
|
|
766
|
+
({ houseNumber, streetName }) => `${houseNumber} ${streetName}`
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
// ❌ FAILS: different nesting levels
|
|
770
|
+
computeFrom(
|
|
771
|
+
[path.nested.price, path.nested.quantity],
|
|
772
|
+
path.rootTotal, // Different level - won't work!
|
|
773
|
+
...
|
|
774
|
+
);
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### watchField - Any Level
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
// ✅ Works for cross-level computation
|
|
781
|
+
watchField(path.nested.price, (price, ctx) => {
|
|
782
|
+
const quantity = ctx.form.quantity.value.value; // Sibling in nested
|
|
783
|
+
ctx.setFieldValue('rootTotal', price * quantity); // Full path to root
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// ✅ Works for multiple dependencies
|
|
787
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
788
|
+
const term = ctx.form.loanTerm.value.value;
|
|
789
|
+
const rate = ctx.form.interestRate.value.value;
|
|
790
|
+
|
|
791
|
+
if (amount && term && rate) {
|
|
792
|
+
const monthly = calculateMonthlyPayment(amount, term, rate);
|
|
793
|
+
ctx.setFieldValue('monthlyPayment', monthly);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Rule of Thumb
|
|
799
|
+
|
|
800
|
+
| Scenario | Use |
|
|
801
|
+
|----------|-----|
|
|
802
|
+
| All fields share same parent | `computeFrom` (simpler, auto-cleanup) |
|
|
803
|
+
| Fields at different levels | `watchField` (more flexible) |
|
|
804
|
+
| Multiple dependencies | `watchField` |
|
|
805
|
+
| Async computation | `watchField` with async callback |
|
|
806
|
+
|
|
807
|
+
## 18. ARRAY OPERATIONS
|
|
808
|
+
|
|
809
|
+
### Array Methods
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
// Add items
|
|
813
|
+
form.items.push({ name: '', price: 0 }); // Add to end
|
|
814
|
+
form.items.insert(0, { name: '', price: 0 }); // Insert at index
|
|
815
|
+
|
|
816
|
+
// Remove items
|
|
817
|
+
form.items.removeAt(index); // Remove by index
|
|
818
|
+
form.items.clear(); // Remove all items
|
|
819
|
+
|
|
820
|
+
// Reorder
|
|
821
|
+
form.items.move(fromIndex, toIndex); // Move item
|
|
822
|
+
|
|
823
|
+
// Access
|
|
824
|
+
form.items.length.value; // Current length (Signal)
|
|
825
|
+
form.items.map((item, index) => ...); // Iterate items
|
|
826
|
+
form.items.at(index); // Get item at index
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### Rendering Arrays
|
|
830
|
+
|
|
831
|
+
```tsx
|
|
832
|
+
function ItemsList({ form }: { form: GroupNodeWithControls<MyForm> }) {
|
|
833
|
+
const { length } = useFormControl(form.items);
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<div>
|
|
837
|
+
{form.items.map((item, index) => (
|
|
838
|
+
// item is GroupNode (sub-form) - each field is a control
|
|
839
|
+
<div key={item.id || index}>
|
|
840
|
+
<FormField control={item.name} />
|
|
841
|
+
<FormField control={item.price} />
|
|
842
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
843
|
+
</div>
|
|
844
|
+
))}
|
|
845
|
+
|
|
846
|
+
{length === 0 && <p>No items yet</p>}
|
|
847
|
+
|
|
848
|
+
<button onClick={() => form.items.push({ name: '', price: 0 })}>
|
|
849
|
+
Add Item
|
|
850
|
+
</button>
|
|
851
|
+
</div>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Array Cross-Validation
|
|
857
|
+
|
|
858
|
+
```typescript
|
|
859
|
+
// Validate uniqueness across array items
|
|
860
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
861
|
+
const items = ctx.form.items;
|
|
862
|
+
const names = items.map(item => item.name.value.value);
|
|
863
|
+
const uniqueNames = new Set(names);
|
|
864
|
+
|
|
865
|
+
if (names.length !== uniqueNames.size) {
|
|
866
|
+
return { code: 'duplicate', message: 'Item names must be unique' };
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}, { targetField: 'items' });
|
|
870
|
+
|
|
871
|
+
// Validate sum of percentages
|
|
872
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
873
|
+
const items = ctx.form.items;
|
|
874
|
+
const totalPercent = items.reduce(
|
|
875
|
+
(sum, item) => sum + (item.percentage.value.value || 0),
|
|
876
|
+
0
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
if (Math.abs(totalPercent - 100) > 0.01) {
|
|
880
|
+
return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}, { targetField: 'items' });
|