@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/llms.txt CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  | What | Where |
8
8
  | ------------------------------------------------------------------------------------------- | --------------------------- |
9
- | `createForm`, `useFormControl`, `useFormControlValue` | `@reformer/core` |
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
- getFieldValue: (path: string) => unknown;
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
- const schema = {
351
- items: [itemSchema] as [typeof itemSchema],
352
- properties: [propertySchema] as [typeof propertySchema],
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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reformer/core",
3
- "version": "1.1.0-beta.2",
3
+ "version": "1.1.0-beta.4",
4
4
  "description": "Reactive form state management library for React with signals-based architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",