@reformer/core 1.1.0-beta.3 → 1.1.0-beta.5
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 +23 -22
- 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/core/validation/validate-form.js +6 -6
- package/dist/create-field-path-nXfTtl55.js +283 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/{create-field-path-CdPF3lIK.js → registry-helpers-BfCZcMkO.js} +79 -357
- package/dist/validation-context-cWXmh_Ho.js +156 -0
- package/dist/validators.js +105 -120
- package/llms.txt +653 -3
- package/package.json +1 -1
- package/dist/node-factory-D7DOnSSN.js +0 -3200
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,98 @@
|
|
|
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
|
+
|
|
46
|
+
## 1.5 QUICK START - Minimal Working Form
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createForm, useFormControl } from '@reformer/core';
|
|
50
|
+
import { required, email } from '@reformer/core/validators';
|
|
51
|
+
import type { GroupNodeWithControls } from '@reformer/core';
|
|
52
|
+
|
|
53
|
+
// 1. Define form type
|
|
54
|
+
interface ContactForm {
|
|
55
|
+
name: string;
|
|
56
|
+
email: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Create form schema with validation
|
|
60
|
+
const form = createForm<ContactForm>({
|
|
61
|
+
form: {
|
|
62
|
+
name: { value: '', component: Input },
|
|
63
|
+
email: { value: '', component: Input },
|
|
64
|
+
},
|
|
65
|
+
validation: (path) => {
|
|
66
|
+
required(path.name, { message: 'Name is required' });
|
|
67
|
+
required(path.email, { message: 'Email is required' });
|
|
68
|
+
email(path.email, { message: 'Invalid email format' });
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. Use in React component
|
|
73
|
+
function ContactFormComponent() {
|
|
74
|
+
const nameCtrl = useFormControl(form.name);
|
|
75
|
+
const emailCtrl = useFormControl(form.email);
|
|
76
|
+
|
|
77
|
+
const handleSubmit = async () => {
|
|
78
|
+
await form.submit((values) => {
|
|
79
|
+
console.log('Form submitted:', values);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
|
85
|
+
<div>
|
|
86
|
+
<input
|
|
87
|
+
value={nameCtrl.value}
|
|
88
|
+
onChange={(e) => form.name.setValue(e.target.value)}
|
|
89
|
+
disabled={nameCtrl.disabled}
|
|
90
|
+
/>
|
|
91
|
+
{nameCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<input
|
|
95
|
+
value={emailCtrl.value}
|
|
96
|
+
onChange={(e) => form.email.setValue(e.target.value)}
|
|
97
|
+
disabled={emailCtrl.disabled}
|
|
98
|
+
/>
|
|
99
|
+
{emailCtrl.errors.map((err) => <span key={err.code}>{err.message}</span>)}
|
|
100
|
+
</div>
|
|
101
|
+
<button type="submit">Send</button>
|
|
102
|
+
</form>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. Pass form to child components via props (NOT context!)
|
|
107
|
+
interface FormStepProps {
|
|
108
|
+
form: GroupNodeWithControls<ContactForm>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function FormStep({ form }: FormStepProps) {
|
|
112
|
+
// Access form fields directly
|
|
113
|
+
const { value } = useFormControl(form.name);
|
|
114
|
+
return <div>Name: {value}</div>;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
26
118
|
## 2. API SIGNATURES
|
|
27
119
|
|
|
28
120
|
### Validators
|
|
@@ -48,9 +140,48 @@ validate(path, validator: (value, ctx) => ValidationError | null)
|
|
|
48
140
|
validateAsync(path, validator: async (value, ctx) => ValidationError | null)
|
|
49
141
|
validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
|
|
50
142
|
|
|
51
|
-
// Conditional validation
|
|
143
|
+
// Conditional validation (3 arguments!)
|
|
52
144
|
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
53
145
|
|
|
146
|
+
// ⚠️ applyWhen Examples - CRITICALLY IMPORTANT
|
|
147
|
+
// applyWhen takes 3 arguments: triggerField, condition, validators
|
|
148
|
+
|
|
149
|
+
// Example 1: Simple condition
|
|
150
|
+
applyWhen(
|
|
151
|
+
path.loanType, // 1st: field to watch
|
|
152
|
+
(type) => type === 'mortgage', // 2nd: condition on field value
|
|
153
|
+
(p) => { // 3rd: validators to apply
|
|
154
|
+
required(p.propertyValue);
|
|
155
|
+
min(p.propertyValue, 100000);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Example 2: Nested field as trigger
|
|
160
|
+
applyWhen(
|
|
161
|
+
path.address.country,
|
|
162
|
+
(country) => country === 'US',
|
|
163
|
+
(p) => {
|
|
164
|
+
required(p.address.state);
|
|
165
|
+
pattern(p.address.zip, /^\d{5}(-\d{4})?$/);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Example 3: Boolean trigger
|
|
170
|
+
applyWhen(
|
|
171
|
+
path.hasInsurance,
|
|
172
|
+
(has) => has === true,
|
|
173
|
+
(p) => {
|
|
174
|
+
required(p.insuranceCompany);
|
|
175
|
+
required(p.policyNumber);
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ❌ WRONG - only 2 arguments (React Hook Form pattern)
|
|
180
|
+
applyWhen(
|
|
181
|
+
(form) => form.loanType === 'mortgage', // WRONG!
|
|
182
|
+
() => { required(path.propertyValue); }
|
|
183
|
+
);
|
|
184
|
+
|
|
54
185
|
// Array validators
|
|
55
186
|
notEmpty(path, options?: { message?: string })
|
|
56
187
|
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
|
|
@@ -89,7 +220,7 @@ transformers.trim, transformers.toUpperCase, transformers.toLowerCase, transform
|
|
|
89
220
|
interface BehaviorContext<TForm> {
|
|
90
221
|
form: GroupNodeWithControls<TForm>; // Form proxy with typed field access
|
|
91
222
|
setFieldValue: (path: string, value: any) => void;
|
|
92
|
-
|
|
223
|
+
// ⚠️ To READ field values, use: ctx.form.fieldName.value.value
|
|
93
224
|
}
|
|
94
225
|
```
|
|
95
226
|
|
|
@@ -121,6 +252,35 @@ const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
|
|
|
121
252
|
|
|
122
253
|
## 4. ⚠️ COMMON MISTAKES
|
|
123
254
|
|
|
255
|
+
### useFormControlValue (CRITICAL)
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// ❌ WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
259
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
260
|
+
// Result: loanType is ALWAYS undefined! Conditional rendering will fail.
|
|
261
|
+
|
|
262
|
+
// ✅ CORRECT
|
|
263
|
+
const loanType = useFormControlValue(control.loanType);
|
|
264
|
+
|
|
265
|
+
// ✅ ALSO CORRECT - useFormControl returns object
|
|
266
|
+
const { value, errors } = useFormControl(control.loanType);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Reading Field Values in BehaviorContext (CRITICAL)
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// ❌ WRONG - getFieldValue does NOT exist!
|
|
273
|
+
watchField(path.amount, (amount, ctx) => {
|
|
274
|
+
const rate = ctx.getFieldValue('rate'); // ERROR: Property 'getFieldValue' does not exist
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ✅ CORRECT - use ctx.form.fieldName.value.value
|
|
278
|
+
watchField(path.amount, (amount, ctx) => {
|
|
279
|
+
const rate = ctx.form.rate.value.value; // Read via signal
|
|
280
|
+
ctx.setFieldValue('total', amount * rate);
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
124
284
|
### Validators
|
|
125
285
|
|
|
126
286
|
```typescript
|
|
@@ -343,6 +503,25 @@ form.address.city.value.value; // Get current value
|
|
|
343
503
|
form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
344
504
|
```
|
|
345
505
|
|
|
506
|
+
### ⚠️ createForm Returns a Proxy
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// createForm() returns GroupNodeWithControls<T> (a Proxy wrapper around GroupNode)
|
|
510
|
+
// This enables type-safe field access:
|
|
511
|
+
const form = createForm<MyForm>({...});
|
|
512
|
+
|
|
513
|
+
form.email // FieldNode<string> - TypeScript knows the type!
|
|
514
|
+
form.address.city // FieldNode<string> - nested access works
|
|
515
|
+
form.items.at(0) // GroupNodeWithControls<ItemType> - array items
|
|
516
|
+
|
|
517
|
+
// ⚠️ IMPORTANT: Proxy doesn't pass instanceof checks!
|
|
518
|
+
// Use type guards instead:
|
|
519
|
+
import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
|
|
520
|
+
|
|
521
|
+
if (isFieldNode(node)) { /* ... */ } // ✅ Works with Proxy
|
|
522
|
+
if (node instanceof FieldNode) { /* ... */ } // ❌ Fails with Proxy!
|
|
523
|
+
```
|
|
524
|
+
|
|
346
525
|
## 9. ARRAY SCHEMA FORMAT
|
|
347
526
|
|
|
348
527
|
**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.
|
|
@@ -473,6 +652,61 @@ export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
473
652
|
step1Validation(path);
|
|
474
653
|
step2Validation(path);
|
|
475
654
|
};
|
|
655
|
+
|
|
656
|
+
// Using validateForm() for step validation
|
|
657
|
+
import { validateForm } from '@reformer/core';
|
|
658
|
+
|
|
659
|
+
const goToNextStep = async () => {
|
|
660
|
+
const currentValidation = STEP_VALIDATIONS[currentStep];
|
|
661
|
+
const isValid = await validateForm(form, currentValidation);
|
|
662
|
+
|
|
663
|
+
if (!isValid) {
|
|
664
|
+
form.markAsTouched(); // Show errors on current step fields
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
setCurrentStep(currentStep + 1);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Full form submit with all validations
|
|
672
|
+
const handleSubmit = async () => {
|
|
673
|
+
const isValid = await validateForm(form, fullValidation);
|
|
674
|
+
|
|
675
|
+
if (isValid) {
|
|
676
|
+
await form.submit(onSubmit);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Multi-Step Component Example
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
function MultiStepForm() {
|
|
685
|
+
const [step, setStep] = useState(1);
|
|
686
|
+
|
|
687
|
+
const nextStep = async () => {
|
|
688
|
+
const validation = STEP_VALIDATIONS[step];
|
|
689
|
+
if (await validateForm(form, validation)) {
|
|
690
|
+
setStep(step + 1);
|
|
691
|
+
} else {
|
|
692
|
+
form.markAsTouched();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
<div>
|
|
698
|
+
{step === 1 && <Step1Fields form={form} />}
|
|
699
|
+
{step === 2 && <Step2Fields form={form} />}
|
|
700
|
+
|
|
701
|
+
<button onClick={() => setStep(step - 1)} disabled={step === 1}>
|
|
702
|
+
Back
|
|
703
|
+
</button>
|
|
704
|
+
<button onClick={step === 2 ? handleSubmit : nextStep}>
|
|
705
|
+
{step === 2 ? 'Submit' : 'Next'}
|
|
706
|
+
</button>
|
|
707
|
+
</div>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
476
710
|
```
|
|
477
711
|
|
|
478
712
|
## 13. ⚠️ EXTENDED COMMON MISTAKES
|
|
@@ -593,6 +827,123 @@ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
593
827
|
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
594
828
|
| Complex | Full colocation with `steps/` and `sub-forms/` |
|
|
595
829
|
|
|
830
|
+
## 14.5 UI COMPONENT PATTERNS
|
|
831
|
+
|
|
832
|
+
ReFormer does NOT provide UI components - you create them yourself or use a UI library.
|
|
833
|
+
|
|
834
|
+
### Generic FormField Component
|
|
835
|
+
|
|
836
|
+
```tsx
|
|
837
|
+
import type { FieldNode } from '@reformer/core';
|
|
838
|
+
import { useFormControl } from '@reformer/core';
|
|
839
|
+
|
|
840
|
+
interface FormFieldProps<T> {
|
|
841
|
+
control: FieldNode<T>;
|
|
842
|
+
label?: string;
|
|
843
|
+
type?: 'text' | 'email' | 'number' | 'password';
|
|
844
|
+
placeholder?: string;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function FormField<T extends string | number>({
|
|
848
|
+
control,
|
|
849
|
+
label,
|
|
850
|
+
type = 'text',
|
|
851
|
+
placeholder
|
|
852
|
+
}: FormFieldProps<T>) {
|
|
853
|
+
const { value, errors, disabled, touched } = useFormControl(control);
|
|
854
|
+
const showError = touched && errors.length > 0;
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<div className="form-field">
|
|
858
|
+
{label && <label>{label}</label>}
|
|
859
|
+
<input
|
|
860
|
+
type={type}
|
|
861
|
+
value={value ?? ''}
|
|
862
|
+
onChange={(e) => {
|
|
863
|
+
const val = type === 'number'
|
|
864
|
+
? Number(e.target.value) as T
|
|
865
|
+
: e.target.value as T;
|
|
866
|
+
control.setValue(val);
|
|
867
|
+
}}
|
|
868
|
+
onBlur={() => control.markAsTouched()}
|
|
869
|
+
disabled={disabled}
|
|
870
|
+
placeholder={placeholder}
|
|
871
|
+
className={showError ? 'error' : ''}
|
|
872
|
+
/>
|
|
873
|
+
{showError && (
|
|
874
|
+
<span className="error-message">{errors[0].message}</span>
|
|
875
|
+
)}
|
|
876
|
+
</div>
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Usage
|
|
881
|
+
<FormField control={form.email} label="Email" type="email" />
|
|
882
|
+
<FormField control={form.age} label="Age" type="number" />
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### FormField for Select
|
|
886
|
+
|
|
887
|
+
```tsx
|
|
888
|
+
interface SelectFieldProps<T extends string> {
|
|
889
|
+
control: FieldNode<T>;
|
|
890
|
+
label?: string;
|
|
891
|
+
options: Array<{ value: T; label: string }>;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function SelectField<T extends string>({
|
|
895
|
+
control,
|
|
896
|
+
label,
|
|
897
|
+
options
|
|
898
|
+
}: SelectFieldProps<T>) {
|
|
899
|
+
const { value, errors, disabled, touched } = useFormControl(control);
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
<div className="form-field">
|
|
903
|
+
{label && <label>{label}</label>}
|
|
904
|
+
<select
|
|
905
|
+
value={value}
|
|
906
|
+
onChange={(e) => control.setValue(e.target.value as T)}
|
|
907
|
+
disabled={disabled}
|
|
908
|
+
>
|
|
909
|
+
{options.map((opt) => (
|
|
910
|
+
<option key={opt.value} value={opt.value}>
|
|
911
|
+
{opt.label}
|
|
912
|
+
</option>
|
|
913
|
+
))}
|
|
914
|
+
</select>
|
|
915
|
+
{touched && errors[0] && (
|
|
916
|
+
<span className="error-message">{errors[0].message}</span>
|
|
917
|
+
)}
|
|
918
|
+
</div>
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### Integration with UI Libraries
|
|
924
|
+
|
|
925
|
+
```tsx
|
|
926
|
+
// With shadcn/ui
|
|
927
|
+
import { Input } from '@/components/ui/input';
|
|
928
|
+
import { Label } from '@/components/ui/label';
|
|
929
|
+
|
|
930
|
+
function ShadcnFormField({ control, label }: FormFieldProps<string>) {
|
|
931
|
+
const { value, errors, disabled } = useFormControl(control);
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
<div className="space-y-2">
|
|
935
|
+
<Label>{label}</Label>
|
|
936
|
+
<Input
|
|
937
|
+
value={value}
|
|
938
|
+
onChange={(e) => control.setValue(e.target.value)}
|
|
939
|
+
disabled={disabled}
|
|
940
|
+
/>
|
|
941
|
+
{errors[0] && <p className="text-red-500">{errors[0].message}</p>}
|
|
942
|
+
</div>
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
596
947
|
## 15. NON-EXISTENT API (DO NOT USE)
|
|
597
948
|
|
|
598
949
|
⚠️ **The following APIs do NOT exist in @reformer/core:**
|
|
@@ -603,6 +954,13 @@ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
603
954
|
| `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
|
|
604
955
|
| `when()` | `applyWhen()` | Conditional validation function |
|
|
605
956
|
| `FormFields` | `FieldNode<T>` | Type for field nodes |
|
|
957
|
+
| `FormInstance<T>` | `GroupNodeWithControls<T>` | Form type for component props |
|
|
958
|
+
| `useArrayField()` | `form.items.push/map/removeAt` | Use ArrayNode methods directly |
|
|
959
|
+
| `FormProvider` | `<Component form={form} />` | Pass form via props, no context |
|
|
960
|
+
| `formState` | `form.valid`, `form.dirty`, etc. | Separate signals on form |
|
|
961
|
+
| `control` prop | Not needed | Form IS the control |
|
|
962
|
+
| `register('field')` | `useFormControl(form.field)` | Type-safe field access |
|
|
963
|
+
| `getFieldValue()` | `ctx.form.field.value.value` | Read via signals |
|
|
606
964
|
|
|
607
965
|
### Common Import Errors
|
|
608
966
|
|
|
@@ -642,3 +1000,295 @@ const schema: FormSchema<MyForm> = {
|
|
|
642
1000
|
},
|
|
643
1001
|
};
|
|
644
1002
|
```
|
|
1003
|
+
|
|
1004
|
+
## 15.5 REACT HOOK FORM MIGRATION
|
|
1005
|
+
|
|
1006
|
+
If you're familiar with React Hook Form, here's how to translate patterns to ReFormer:
|
|
1007
|
+
|
|
1008
|
+
| React Hook Form | ReFormer | Notes |
|
|
1009
|
+
|-----------------|----------|-------|
|
|
1010
|
+
| `const { register, handleSubmit } = useForm()` | `const form = createForm({...})` | Call OUTSIDE component |
|
|
1011
|
+
| `register('fieldName')` | `useFormControl(form.fieldName)` | Returns control object |
|
|
1012
|
+
| `watch('fieldName')` | `useFormControlValue(form.fieldName)` | Returns value directly (NOT `{ value }`) |
|
|
1013
|
+
| `setValue('field', value)` | `form.field.setValue(value)` | Direct method call |
|
|
1014
|
+
| `getValues('field')` | `form.field.value.value` | Signal-based |
|
|
1015
|
+
| `formState.errors` | `useFormControl(form.field).errors` | Per-field errors array |
|
|
1016
|
+
| `formState.isValid` | `form.valid.value` | Signal |
|
|
1017
|
+
| `formState.isDirty` | `form.dirty.value` | Signal |
|
|
1018
|
+
| `handleSubmit(onSubmit)` | `form.submit(onSubmit)` | Built-in validation |
|
|
1019
|
+
| `<FormProvider form={...}>` | `<Component form={form} />` | Props, NOT context |
|
|
1020
|
+
| `useFormContext()` | Not needed | Pass form via props |
|
|
1021
|
+
| `useFieldArray({ name: 'items' })` | `form.items` (ArrayNode) | Direct array access |
|
|
1022
|
+
| `fields.map((field, index) => ...)` | `form.items.map((item, index) => ...)` | item is sub-form |
|
|
1023
|
+
| `append(data)` | `form.items.push(data)` | Add item |
|
|
1024
|
+
| `remove(index)` | `form.items.removeAt(index)` | Remove item |
|
|
1025
|
+
| `control` prop | Not needed | Form IS the control |
|
|
1026
|
+
|
|
1027
|
+
### Key Differences
|
|
1028
|
+
|
|
1029
|
+
1. **No Provider/Context** - Pass form directly via props
|
|
1030
|
+
```typescript
|
|
1031
|
+
// ❌ React Hook Form pattern - doesn't exist in ReFormer
|
|
1032
|
+
<FormProvider form={form}>
|
|
1033
|
+
<NestedComponent />
|
|
1034
|
+
</FormProvider>
|
|
1035
|
+
|
|
1036
|
+
// ✅ ReFormer pattern - pass via props
|
|
1037
|
+
<NestedComponent form={form} />
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
2. **Form Creation Location** - Create outside component
|
|
1041
|
+
```typescript
|
|
1042
|
+
// ❌ WRONG - creates new form on every render
|
|
1043
|
+
function MyComponent() {
|
|
1044
|
+
const form = createForm({...});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// ✅ CORRECT - create once, outside component
|
|
1048
|
+
const form = createForm({...});
|
|
1049
|
+
function MyComponent() {
|
|
1050
|
+
const ctrl = useFormControl(form.name);
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
3. **Type-Safe Field Access** - No string paths
|
|
1055
|
+
```typescript
|
|
1056
|
+
// React Hook Form
|
|
1057
|
+
register('user.address.city')
|
|
1058
|
+
|
|
1059
|
+
// ReFormer - fully typed
|
|
1060
|
+
form.user.address.city.setValue('New York')
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
4. **Array Items are Sub-Forms**
|
|
1064
|
+
```typescript
|
|
1065
|
+
// React Hook Form
|
|
1066
|
+
fields.map((field, index) => (
|
|
1067
|
+
<input {...register(`items.${index}.name`)} />
|
|
1068
|
+
))
|
|
1069
|
+
|
|
1070
|
+
// ReFormer - each item is a typed GroupNode
|
|
1071
|
+
form.items.map((item, index) => (
|
|
1072
|
+
<FormField control={item.name} /> // item.name is FieldNode
|
|
1073
|
+
))
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
5. **Reading Values in Behaviors**
|
|
1077
|
+
```typescript
|
|
1078
|
+
// React Hook Form
|
|
1079
|
+
watch('fieldName')
|
|
1080
|
+
|
|
1081
|
+
// ReFormer in watchField callback
|
|
1082
|
+
watchField(path.trigger, (value, ctx) => {
|
|
1083
|
+
const other = ctx.form.otherField.value.value; // Signal access
|
|
1084
|
+
});
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
1088
|
+
|
|
1089
|
+
### Why .value.value?
|
|
1090
|
+
|
|
1091
|
+
ReFormer uses `@preact/signals-core` for reactivity:
|
|
1092
|
+
- `field.value` → `Signal<T>` (reactive container)
|
|
1093
|
+
- `field.value.value` → `T` (actual value)
|
|
1094
|
+
- `field.getValue()` → `T` (shorthand method, non-reactive)
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
// Reading values in different contexts:
|
|
1098
|
+
|
|
1099
|
+
// In React components - use hooks
|
|
1100
|
+
const { value } = useFormControl(control.email); // Object with value
|
|
1101
|
+
const email = useFormControlValue(control.email); // Value directly
|
|
1102
|
+
|
|
1103
|
+
// In BehaviorContext (watchField, etc.)
|
|
1104
|
+
watchField(path.firstName, (firstName, ctx) => {
|
|
1105
|
+
// ⚠️ ctx.form is typed as the PARENT GROUP of the watched field!
|
|
1106
|
+
// For path.nested.field: ctx.form = NestedType, NOT RootForm!
|
|
1107
|
+
|
|
1108
|
+
const lastName = ctx.form.lastName.value.value; // Read sibling field
|
|
1109
|
+
|
|
1110
|
+
// Use setFieldValue with full path for root-level fields
|
|
1111
|
+
ctx.setFieldValue('fullName', `${firstName} ${lastName}`);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Direct access on form controls
|
|
1115
|
+
form.email.value.value; // Read current value
|
|
1116
|
+
form.address.city.value.value; // Read nested value
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### Reading Nested Values in watchField
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
// ⚠️ IMPORTANT: ctx.form type depends on the watched path!
|
|
1123
|
+
|
|
1124
|
+
// Watching root-level field
|
|
1125
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
1126
|
+
// ctx.form is MyForm - can access all fields
|
|
1127
|
+
const rate = ctx.form.interestRate.value.value;
|
|
1128
|
+
ctx.setFieldValue('monthlyPayment', amount * rate / 12);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// Watching nested field
|
|
1132
|
+
watchField(path.personalData.lastName, (lastName, ctx) => {
|
|
1133
|
+
// ctx.form is PersonalData, NOT MyForm!
|
|
1134
|
+
const firstName = ctx.form.firstName.value.value; // ✅ Works
|
|
1135
|
+
const middleName = ctx.form.middleName.value.value; // ✅ Works
|
|
1136
|
+
|
|
1137
|
+
// For root-level field, use setFieldValue with full path
|
|
1138
|
+
ctx.setFieldValue('fullName', `${lastName} ${firstName}`);
|
|
1139
|
+
});
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
## 17. COMPUTE FROM vs WATCH FIELD
|
|
1143
|
+
|
|
1144
|
+
### computeFrom - Same Nesting Level Only
|
|
1145
|
+
|
|
1146
|
+
```typescript
|
|
1147
|
+
// ✅ Works: all source fields and target at same level
|
|
1148
|
+
computeFrom(
|
|
1149
|
+
[path.price, path.quantity],
|
|
1150
|
+
path.total,
|
|
1151
|
+
({ price, quantity }) => (price || 0) * (quantity || 0)
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
// ✅ Works: all nested at same level
|
|
1155
|
+
computeFrom(
|
|
1156
|
+
[path.address.houseNumber, path.address.streetName],
|
|
1157
|
+
path.address.fullAddress,
|
|
1158
|
+
({ houseNumber, streetName }) => `${houseNumber} ${streetName}`
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// ❌ FAILS: different nesting levels
|
|
1162
|
+
computeFrom(
|
|
1163
|
+
[path.nested.price, path.nested.quantity],
|
|
1164
|
+
path.rootTotal, // Different level - won't work!
|
|
1165
|
+
...
|
|
1166
|
+
);
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
### watchField - Any Level
|
|
1170
|
+
|
|
1171
|
+
```typescript
|
|
1172
|
+
// ✅ Works for cross-level computation
|
|
1173
|
+
watchField(path.nested.price, (price, ctx) => {
|
|
1174
|
+
const quantity = ctx.form.quantity.value.value; // Sibling in nested
|
|
1175
|
+
ctx.setFieldValue('rootTotal', price * quantity); // Full path to root
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// ✅ Works for multiple dependencies
|
|
1179
|
+
watchField(path.loanAmount, (amount, ctx) => {
|
|
1180
|
+
const term = ctx.form.loanTerm.value.value;
|
|
1181
|
+
const rate = ctx.form.interestRate.value.value;
|
|
1182
|
+
|
|
1183
|
+
if (amount && term && rate) {
|
|
1184
|
+
const monthly = calculateMonthlyPayment(amount, term, rate);
|
|
1185
|
+
ctx.setFieldValue('monthlyPayment', monthly);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
### Rule of Thumb
|
|
1191
|
+
|
|
1192
|
+
| Scenario | Use |
|
|
1193
|
+
|----------|-----|
|
|
1194
|
+
| All fields share same parent | `computeFrom` (simpler, auto-cleanup) |
|
|
1195
|
+
| Fields at different levels | `watchField` (more flexible) |
|
|
1196
|
+
| Multiple dependencies | `watchField` |
|
|
1197
|
+
| Async computation | `watchField` with async callback |
|
|
1198
|
+
|
|
1199
|
+
## 18. ARRAY OPERATIONS
|
|
1200
|
+
|
|
1201
|
+
### ⚠️ Array Access - CRITICAL
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
// ❌ WRONG - bracket notation does NOT work!
|
|
1205
|
+
const first = form.items[0]; // undefined or error
|
|
1206
|
+
const second = form.items[1]; // undefined or error
|
|
1207
|
+
|
|
1208
|
+
// ✅ CORRECT - use .at() method
|
|
1209
|
+
const first = form.items.at(0); // GroupNodeWithControls<ItemType> | undefined
|
|
1210
|
+
const second = form.items.at(1); // GroupNodeWithControls<ItemType> | undefined
|
|
1211
|
+
|
|
1212
|
+
// ✅ CORRECT - iterate with map (most common pattern)
|
|
1213
|
+
form.items.map((item, index) => {
|
|
1214
|
+
// item is fully typed GroupNode
|
|
1215
|
+
item.name.setValue('New Name');
|
|
1216
|
+
item.price.value.value; // read value
|
|
1217
|
+
});
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### Array Methods
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
// Add items
|
|
1224
|
+
form.items.push({ name: '', price: 0 }); // Add to end
|
|
1225
|
+
form.items.insert(0, { name: '', price: 0 }); // Insert at index
|
|
1226
|
+
|
|
1227
|
+
// Remove items
|
|
1228
|
+
form.items.removeAt(index); // Remove by index
|
|
1229
|
+
form.items.clear(); // Remove all items
|
|
1230
|
+
|
|
1231
|
+
// Reorder
|
|
1232
|
+
form.items.move(fromIndex, toIndex); // Move item
|
|
1233
|
+
|
|
1234
|
+
// Access (use .at(), NOT brackets!)
|
|
1235
|
+
form.items.length.value; // Current length (Signal)
|
|
1236
|
+
form.items.map((item, index) => ...); // Iterate items
|
|
1237
|
+
form.items.at(index); // Get item at index (NOT items[index]!)
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
### Rendering Arrays
|
|
1241
|
+
|
|
1242
|
+
```tsx
|
|
1243
|
+
function ItemsList({ form }: { form: GroupNodeWithControls<MyForm> }) {
|
|
1244
|
+
const { length } = useFormControl(form.items);
|
|
1245
|
+
|
|
1246
|
+
return (
|
|
1247
|
+
<div>
|
|
1248
|
+
{form.items.map((item, index) => (
|
|
1249
|
+
// item is GroupNode (sub-form) - each field is a control
|
|
1250
|
+
<div key={item.id || index}>
|
|
1251
|
+
<FormField control={item.name} />
|
|
1252
|
+
<FormField control={item.price} />
|
|
1253
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
1254
|
+
</div>
|
|
1255
|
+
))}
|
|
1256
|
+
|
|
1257
|
+
{length === 0 && <p>No items yet</p>}
|
|
1258
|
+
|
|
1259
|
+
<button onClick={() => form.items.push({ name: '', price: 0 })}>
|
|
1260
|
+
Add Item
|
|
1261
|
+
</button>
|
|
1262
|
+
</div>
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Array Cross-Validation
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
// Validate uniqueness across array items
|
|
1271
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
1272
|
+
const items = ctx.form.items;
|
|
1273
|
+
const names = items.map(item => item.name.value.value);
|
|
1274
|
+
const uniqueNames = new Set(names);
|
|
1275
|
+
|
|
1276
|
+
if (names.length !== uniqueNames.size) {
|
|
1277
|
+
return { code: 'duplicate', message: 'Item names must be unique' };
|
|
1278
|
+
}
|
|
1279
|
+
return null;
|
|
1280
|
+
}, { targetField: 'items' });
|
|
1281
|
+
|
|
1282
|
+
// Validate sum of percentages
|
|
1283
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
1284
|
+
const items = ctx.form.items;
|
|
1285
|
+
const totalPercent = items.reduce(
|
|
1286
|
+
(sum, item) => sum + (item.percentage.value.value || 0),
|
|
1287
|
+
0
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
if (Math.abs(totalPercent - 100) > 0.01) {
|
|
1291
|
+
return { code: 'invalid_total', message: 'Percentages must sum to 100%' };
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}, { targetField: 'items' });
|