@reformer/core 1.1.0-beta.4 → 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/validation/validate-form.js +6 -6
- package/dist/create-field-path-nXfTtl55.js +283 -0
- package/dist/{create-field-path-DcXDTWil.js → registry-helpers-BfCZcMkO.js} +63 -344
- package/dist/validation-context-cWXmh_Ho.js +156 -0
- package/dist/validators.js +6 -6
- package/llms.txt +414 -3
- package/package.json +1 -1
- package/dist/node-factory-DYXIgJmW.js +0 -3217
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { V as d } from "./registry-helpers-BfCZcMkO.js";
|
|
2
|
+
function g() {
|
|
3
|
+
return l("");
|
|
4
|
+
}
|
|
5
|
+
function l(t) {
|
|
6
|
+
return new Proxy({}, {
|
|
7
|
+
get(e, i) {
|
|
8
|
+
if (typeof i == "symbol")
|
|
9
|
+
return;
|
|
10
|
+
if (i === "__path")
|
|
11
|
+
return t || i;
|
|
12
|
+
if (i === "__key") {
|
|
13
|
+
const a = t.split(".");
|
|
14
|
+
return a[a.length - 1] || i;
|
|
15
|
+
}
|
|
16
|
+
if (i === "then" || i === "catch" || i === "finally" || i === "constructor" || i === "toString" || i === "valueOf" || i === "toJSON")
|
|
17
|
+
return;
|
|
18
|
+
const n = t ? `${t}.${i}` : i, o = {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
__key: i,
|
|
21
|
+
__path: n,
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
__formType: void 0,
|
|
24
|
+
__fieldType: void 0
|
|
25
|
+
};
|
|
26
|
+
return new Proxy(o, {
|
|
27
|
+
get(a, r) {
|
|
28
|
+
if (typeof r != "symbol") {
|
|
29
|
+
if (r === "__path") return n;
|
|
30
|
+
if (r === "__key") return i;
|
|
31
|
+
if (r !== "__formType" && r !== "__fieldType" && !(r === "then" || r === "catch" || r === "finally" || r === "constructor" || r === "toString" || r === "valueOf" || r === "toJSON"))
|
|
32
|
+
return l(`${n}.${r}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function m(t) {
|
|
40
|
+
if (typeof t == "string")
|
|
41
|
+
return t;
|
|
42
|
+
if (t && typeof t == "object") {
|
|
43
|
+
const e = t.__path;
|
|
44
|
+
if (typeof e == "string")
|
|
45
|
+
return e;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Invalid field path node: " + JSON.stringify(t));
|
|
48
|
+
}
|
|
49
|
+
function F(t) {
|
|
50
|
+
const e = m(t);
|
|
51
|
+
return l(e);
|
|
52
|
+
}
|
|
53
|
+
function V(t) {
|
|
54
|
+
if (t && typeof t == "object" && "__key" in t)
|
|
55
|
+
return t.__key;
|
|
56
|
+
if (typeof t == "string") {
|
|
57
|
+
const e = t.split(".");
|
|
58
|
+
return e[e.length - 1];
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Invalid field path node");
|
|
61
|
+
}
|
|
62
|
+
function s(t) {
|
|
63
|
+
return t == null ? !1 : typeof t == "object" && "value" in t && "setValue" in t && "getValue" in t && "validate" in t;
|
|
64
|
+
}
|
|
65
|
+
function c(t) {
|
|
66
|
+
return t == null ? !1 : s(t) && "validators" in t && "asyncValidators" in t && // FieldNode имеет markAsTouched метод
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
typeof t.markAsTouched == "function" && // У FieldNode нет fields или items
|
|
69
|
+
!("fields" in t) && !("items" in t);
|
|
70
|
+
}
|
|
71
|
+
function u(t) {
|
|
72
|
+
return t == null ? !1 : s(t) && "applyValidationSchema" in t && "applyBehaviorSchema" in t && "getFieldByPath" in t && // GroupNode НЕ имеет items/push/removeAt (это ArrayNode)
|
|
73
|
+
!("items" in t) && !("push" in t) && !("removeAt" in t);
|
|
74
|
+
}
|
|
75
|
+
function y(t) {
|
|
76
|
+
return t == null ? !1 : s(t) && "items" in t && "length" in t && "push" in t && "removeAt" in t && "at" in t && // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
typeof t.push == "function" && // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
typeof t.removeAt == "function";
|
|
79
|
+
}
|
|
80
|
+
function N(t) {
|
|
81
|
+
return c(t) ? "FieldNode" : u(t) ? "GroupNode" : y(t) ? "ArrayNode" : s(t) ? "FormNode" : "Unknown";
|
|
82
|
+
}
|
|
83
|
+
function f(t) {
|
|
84
|
+
return c(t) ? [t] : u(t) ? Array.from(t.getAllFields()).flatMap(f) : y(t) ? t.map((e) => f(e)).flat() : [];
|
|
85
|
+
}
|
|
86
|
+
async function w(t, e) {
|
|
87
|
+
const i = new d();
|
|
88
|
+
i.beginRegistration();
|
|
89
|
+
let n = [], o = !1;
|
|
90
|
+
try {
|
|
91
|
+
const a = g();
|
|
92
|
+
e(a), n = i.getCurrentContext()?.getValidators() || [], i.cancelRegistration(), o = !0, t.clearErrors();
|
|
93
|
+
const h = f(t);
|
|
94
|
+
return await Promise.all(h.map((_) => _.validate())), n.length > 0 && await t.applyContextualValidators(n), t.valid.value;
|
|
95
|
+
} catch (a) {
|
|
96
|
+
throw o || i.cancelRegistration(), a;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
class A {
|
|
100
|
+
_form;
|
|
101
|
+
control;
|
|
102
|
+
/**
|
|
103
|
+
* Форма с типизированным Proxy-доступом к полям
|
|
104
|
+
*/
|
|
105
|
+
form;
|
|
106
|
+
constructor(e, i, n) {
|
|
107
|
+
this._form = e, this.control = n, this.form = e._proxyInstance || e.getProxy();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Получить текущее значение поля (внутренний метод для validation-applicator)
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
value() {
|
|
114
|
+
return this.control.value.value;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Безопасно установить значение поля по строковому пути
|
|
118
|
+
* Автоматически использует emitEvent: false для предотвращения циклов
|
|
119
|
+
*/
|
|
120
|
+
setFieldValue(e, i) {
|
|
121
|
+
const n = this._form.getFieldByPath(e);
|
|
122
|
+
n && s(n) && n.setValue(i, { emitEvent: !1 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
class T {
|
|
126
|
+
_form;
|
|
127
|
+
/**
|
|
128
|
+
* Форма с типизированным Proxy-доступом к полям
|
|
129
|
+
*/
|
|
130
|
+
form;
|
|
131
|
+
constructor(e) {
|
|
132
|
+
this._form = e, this.form = e._proxyInstance || e.getProxy();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Безопасно установить значение поля по строковому пути
|
|
136
|
+
* Автоматически использует emitEvent: false для предотвращения циклов
|
|
137
|
+
*/
|
|
138
|
+
setFieldValue(e, i) {
|
|
139
|
+
const n = this._form.getFieldByPath(e);
|
|
140
|
+
n && s(n) && n.setValue(i, { emitEvent: !1 });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
T,
|
|
145
|
+
A as V,
|
|
146
|
+
s as a,
|
|
147
|
+
u as b,
|
|
148
|
+
g as c,
|
|
149
|
+
y as d,
|
|
150
|
+
m as e,
|
|
151
|
+
V as f,
|
|
152
|
+
N as g,
|
|
153
|
+
c as i,
|
|
154
|
+
F as t,
|
|
155
|
+
w as v
|
|
156
|
+
};
|
package/dist/validators.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { T as H, V as P,
|
|
3
|
-
import { g as u } from "./
|
|
4
|
-
import { V as
|
|
1
|
+
import { e as c, t as l, c as i } from "./validation-context-cWXmh_Ho.js";
|
|
2
|
+
import { T as H, V as P, f as W, v as M } from "./validation-context-cWXmh_Ho.js";
|
|
3
|
+
import { g as u } from "./registry-helpers-BfCZcMkO.js";
|
|
4
|
+
import { V as j } from "./registry-helpers-BfCZcMkO.js";
|
|
5
5
|
function n(r, e, a) {
|
|
6
6
|
if (!r) return;
|
|
7
7
|
const m = c(r);
|
|
@@ -256,7 +256,7 @@ function I(r, e) {
|
|
|
256
256
|
export {
|
|
257
257
|
H as TreeValidationContextImpl,
|
|
258
258
|
P as ValidationContextImpl,
|
|
259
|
-
|
|
259
|
+
j as ValidationRegistry,
|
|
260
260
|
$ as apply,
|
|
261
261
|
o as applyWhen,
|
|
262
262
|
i as createFieldPath,
|
|
@@ -277,7 +277,7 @@ export {
|
|
|
277
277
|
T as url,
|
|
278
278
|
n as validate,
|
|
279
279
|
A as validateAsync,
|
|
280
|
-
|
|
280
|
+
M as validateForm,
|
|
281
281
|
I as validateItems,
|
|
282
282
|
_ as validateTree
|
|
283
283
|
};
|
package/llms.txt
CHANGED
|
@@ -43,6 +43,78 @@ const loanType = useFormControlValue(control.loanType);
|
|
|
43
43
|
const { value, errors, disabled } = useFormControl(control.loanType);
|
|
44
44
|
```
|
|
45
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
|
+
|
|
46
118
|
## 2. API SIGNATURES
|
|
47
119
|
|
|
48
120
|
### Validators
|
|
@@ -68,9 +140,48 @@ validate(path, validator: (value, ctx) => ValidationError | null)
|
|
|
68
140
|
validateAsync(path, validator: async (value, ctx) => ValidationError | null)
|
|
69
141
|
validateTree(validator: (ctx) => ValidationError | null, options?: { targetField?: string })
|
|
70
142
|
|
|
71
|
-
// Conditional validation
|
|
143
|
+
// Conditional validation (3 arguments!)
|
|
72
144
|
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
73
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
|
+
|
|
74
185
|
// Array validators
|
|
75
186
|
notEmpty(path, options?: { message?: string })
|
|
76
187
|
validateItems(arrayPath, itemValidatorsFn: (itemPath) => void)
|
|
@@ -392,6 +503,25 @@ form.address.city.value.value; // Get current value
|
|
|
392
503
|
form.items.push({ id: '1', name: 'Item' }); // Array operations
|
|
393
504
|
```
|
|
394
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
|
+
|
|
395
525
|
## 9. ARRAY SCHEMA FORMAT
|
|
396
526
|
|
|
397
527
|
**Array items are sub-forms!** Each array element is a complete sub-form with its own fields, validation, and behavior.
|
|
@@ -522,6 +652,61 @@ export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
522
652
|
step1Validation(path);
|
|
523
653
|
step2Validation(path);
|
|
524
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
|
+
}
|
|
525
710
|
```
|
|
526
711
|
|
|
527
712
|
## 13. ⚠️ EXTENDED COMMON MISTAKES
|
|
@@ -642,6 +827,123 @@ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
642
827
|
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
643
828
|
| Complex | Full colocation with `steps/` and `sub-forms/` |
|
|
644
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
|
+
|
|
645
947
|
## 15. NON-EXISTENT API (DO NOT USE)
|
|
646
948
|
|
|
647
949
|
⚠️ **The following APIs do NOT exist in @reformer/core:**
|
|
@@ -652,6 +954,13 @@ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
|
652
954
|
| `FieldSchema` | `FieldConfig<T>` | Type for individual field config |
|
|
653
955
|
| `when()` | `applyWhen()` | Conditional validation function |
|
|
654
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 |
|
|
655
964
|
|
|
656
965
|
### Common Import Errors
|
|
657
966
|
|
|
@@ -692,6 +1001,89 @@ const schema: FormSchema<MyForm> = {
|
|
|
692
1001
|
};
|
|
693
1002
|
```
|
|
694
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
|
+
|
|
695
1087
|
## 16. READING FIELD VALUES (CRITICALLY IMPORTANT)
|
|
696
1088
|
|
|
697
1089
|
### Why .value.value?
|
|
@@ -806,6 +1198,25 @@ watchField(path.loanAmount, (amount, ctx) => {
|
|
|
806
1198
|
|
|
807
1199
|
## 18. ARRAY OPERATIONS
|
|
808
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
|
+
|
|
809
1220
|
### Array Methods
|
|
810
1221
|
|
|
811
1222
|
```typescript
|
|
@@ -820,10 +1231,10 @@ form.items.clear(); // Remove all items
|
|
|
820
1231
|
// Reorder
|
|
821
1232
|
form.items.move(fromIndex, toIndex); // Move item
|
|
822
1233
|
|
|
823
|
-
// Access
|
|
1234
|
+
// Access (use .at(), NOT brackets!)
|
|
824
1235
|
form.items.length.value; // Current length (Signal)
|
|
825
1236
|
form.items.map((item, index) => ...); // Iterate items
|
|
826
|
-
form.items.at(index); // Get item at index
|
|
1237
|
+
form.items.at(index); // Get item at index (NOT items[index]!)
|
|
827
1238
|
```
|
|
828
1239
|
|
|
829
1240
|
### Rendering Arrays
|