@reformer/core 1.1.0-beta.3 → 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 +241 -2
- package/package.json +1 -1
package/dist/validators.js
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
import { h as
|
|
2
|
-
import { T as
|
|
3
|
-
import { g as u
|
|
4
|
-
|
|
1
|
+
import { h as c, t as l, j as i } from "./node-factory-DYXIgJmW.js";
|
|
2
|
+
import { T as H, V as P, k as W, v as j } from "./node-factory-DYXIgJmW.js";
|
|
3
|
+
import { g as u } from "./create-field-path-DcXDTWil.js";
|
|
4
|
+
import { V as M } from "./create-field-path-DcXDTWil.js";
|
|
5
|
+
function n(r, e, a) {
|
|
5
6
|
if (!r) return;
|
|
6
|
-
const
|
|
7
|
-
u().registerSync(
|
|
7
|
+
const m = c(r);
|
|
8
|
+
u().registerSync(m, e, a);
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
u().registerAsync(
|
|
10
|
+
function A(r, e, a) {
|
|
11
|
+
const m = c(r);
|
|
12
|
+
u().registerAsync(m, e, a);
|
|
12
13
|
}
|
|
13
|
-
function
|
|
14
|
+
function _(r, e) {
|
|
14
15
|
u().registerTree(r, e);
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
+
function $(r, e) {
|
|
17
18
|
if (!Array.isArray(r) && !Array.isArray(e) && r && !("__key" in r) && !("__path" in r)) {
|
|
18
19
|
e(r);
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
const a = (Array.isArray(r) ? r : [r]).filter(
|
|
22
23
|
Boolean
|
|
23
|
-
),
|
|
24
|
-
for (const
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
|
|
24
|
+
), m = Array.isArray(e) ? e : [e];
|
|
25
|
+
for (const s of a) {
|
|
26
|
+
const t = l(s);
|
|
27
|
+
for (const g of m)
|
|
28
|
+
g(t);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
u().enterCondition(
|
|
31
|
+
function o(r, e, a) {
|
|
32
|
+
const m = c(r);
|
|
33
|
+
u().enterCondition(m, e);
|
|
33
34
|
try {
|
|
34
|
-
const
|
|
35
|
-
a(
|
|
35
|
+
const s = i();
|
|
36
|
+
a(s);
|
|
36
37
|
} finally {
|
|
37
38
|
u().exitCondition();
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
function
|
|
41
|
-
r &&
|
|
41
|
+
function w(r, e) {
|
|
42
|
+
r && n(r, (a) => a == null || a === "" ? {
|
|
42
43
|
code: "required",
|
|
43
44
|
message: e?.message || "Поле обязательно для заполнения",
|
|
44
45
|
params: e?.params
|
|
@@ -48,55 +49,55 @@ function N(r, e) {
|
|
|
48
49
|
params: e?.params
|
|
49
50
|
} : null);
|
|
50
51
|
}
|
|
51
|
-
function
|
|
52
|
-
r &&
|
|
52
|
+
function D(r, e, a) {
|
|
53
|
+
r && n(r, (m) => m == null ? null : m < e ? {
|
|
53
54
|
code: "min",
|
|
54
55
|
message: a?.message || `Минимальное значение: ${e}`,
|
|
55
|
-
params: { min: e, actual:
|
|
56
|
+
params: { min: e, actual: m, ...a?.params }
|
|
56
57
|
} : null);
|
|
57
58
|
}
|
|
58
|
-
function
|
|
59
|
-
r &&
|
|
59
|
+
function h(r, e, a) {
|
|
60
|
+
r && n(r, (m) => m == null ? null : m > e ? {
|
|
60
61
|
code: "max",
|
|
61
62
|
message: a?.message || `Максимальное значение: ${e}`,
|
|
62
|
-
params: { max: e, actual:
|
|
63
|
+
params: { max: e, actual: m, ...a?.params }
|
|
63
64
|
} : null);
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
-
r &&
|
|
66
|
+
function d(r, e, a) {
|
|
67
|
+
r && n(r, (m) => m && m.length < e ? {
|
|
67
68
|
code: "minLength",
|
|
68
69
|
message: a?.message || `Минимальная длина: ${e} символов`,
|
|
69
|
-
params: { minLength: e, actualLength:
|
|
70
|
+
params: { minLength: e, actualLength: m.length, ...a?.params }
|
|
70
71
|
} : null);
|
|
71
72
|
}
|
|
72
|
-
function
|
|
73
|
-
r &&
|
|
73
|
+
function b(r, e, a) {
|
|
74
|
+
r && n(r, (m) => m && m.length > e ? {
|
|
74
75
|
code: "maxLength",
|
|
75
76
|
message: a?.message || `Максимальная длина: ${e} символов`,
|
|
76
|
-
params: { maxLength: e, actualLength:
|
|
77
|
+
params: { maxLength: e, actualLength: m.length, ...a?.params }
|
|
77
78
|
} : null);
|
|
78
79
|
}
|
|
79
|
-
function
|
|
80
|
+
function L(r, e) {
|
|
80
81
|
if (!r) return;
|
|
81
82
|
const a = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
82
|
-
|
|
83
|
+
n(r, (m) => m ? a.test(m) ? null : {
|
|
83
84
|
code: "email",
|
|
84
85
|
message: e?.message || "Неверный формат email",
|
|
85
86
|
params: e?.params
|
|
86
87
|
} : null);
|
|
87
88
|
}
|
|
88
|
-
function
|
|
89
|
-
r &&
|
|
89
|
+
function R(r, e, a) {
|
|
90
|
+
r && n(r, (m) => m ? e.test(m) ? null : {
|
|
90
91
|
code: "pattern",
|
|
91
92
|
message: a?.message || "Значение не соответствует требуемому формату",
|
|
92
93
|
params: { pattern: e.source, ...a?.params }
|
|
93
94
|
} : null);
|
|
94
95
|
}
|
|
95
|
-
function
|
|
96
|
+
function T(r, e) {
|
|
96
97
|
if (!r) return;
|
|
97
|
-
const a = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i,
|
|
98
|
-
|
|
99
|
-
(
|
|
98
|
+
const a = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i, m = /^https?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i;
|
|
99
|
+
n(r, (s) => s ? (e?.requireProtocol ? m : a).test(s) ? e?.allowedProtocols && e.allowedProtocols.length > 0 && !e.allowedProtocols.some(
|
|
100
|
+
(f) => s.toLowerCase().startsWith(`${f}://`)
|
|
100
101
|
) ? {
|
|
101
102
|
code: "url_protocol",
|
|
102
103
|
message: e?.message || `URL должен использовать один из протоколов: ${e.allowedProtocols.join(", ")}`,
|
|
@@ -107,9 +108,9 @@ function z(r, e) {
|
|
|
107
108
|
params: e?.params
|
|
108
109
|
} : null);
|
|
109
110
|
}
|
|
110
|
-
function
|
|
111
|
+
function C(r, e) {
|
|
111
112
|
if (!r) return;
|
|
112
|
-
const a = e?.format || "any",
|
|
113
|
+
const a = e?.format || "any", m = {
|
|
113
114
|
// Международный формат: +1234567890 или +1 234 567 8900
|
|
114
115
|
international: /^\+?[1-9]\d{1,14}$/,
|
|
115
116
|
// Российский формат: +7 (XXX) XXX-XX-XX, 8 (XXX) XXX-XX-XX, и вариации
|
|
@@ -119,11 +120,11 @@ function I(r, e) {
|
|
|
119
120
|
// Любой формат: минимум 10 цифр с возможными разделителями
|
|
120
121
|
any: /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}$/
|
|
121
122
|
};
|
|
122
|
-
|
|
123
|
-
if (!
|
|
123
|
+
n(r, (s) => {
|
|
124
|
+
if (!s)
|
|
124
125
|
return null;
|
|
125
|
-
if (!
|
|
126
|
-
const
|
|
126
|
+
if (!m[a].test(s)) {
|
|
127
|
+
const g = {
|
|
127
128
|
international: "Введите телефон в международном формате (например, +1234567890)",
|
|
128
129
|
ru: "Введите российский номер телефона (например, +7 900 123-45-67)",
|
|
129
130
|
us: "Введите американский номер телефона (например, (123) 456-7890)",
|
|
@@ -131,15 +132,15 @@ function I(r, e) {
|
|
|
131
132
|
};
|
|
132
133
|
return {
|
|
133
134
|
code: "phone",
|
|
134
|
-
message: e?.message ||
|
|
135
|
+
message: e?.message || g[a],
|
|
135
136
|
params: { format: a, ...e?.params }
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
return null;
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
|
-
function
|
|
142
|
-
r &&
|
|
142
|
+
function N(r, e) {
|
|
143
|
+
r && n(r, (a) => a == null ? null : typeof a != "number" || isNaN(a) ? {
|
|
143
144
|
code: "number",
|
|
144
145
|
message: e?.message || "Значение должно быть числом",
|
|
145
146
|
params: e?.params
|
|
@@ -169,130 +170,114 @@ function P(r, e) {
|
|
|
169
170
|
params: e?.params
|
|
170
171
|
} : null);
|
|
171
172
|
}
|
|
172
|
-
function
|
|
173
|
-
r &&
|
|
173
|
+
function q(r, e) {
|
|
174
|
+
r && n(r, (a) => {
|
|
174
175
|
if (!a)
|
|
175
176
|
return null;
|
|
176
|
-
let
|
|
177
|
+
let m;
|
|
177
178
|
if (a instanceof Date)
|
|
178
|
-
|
|
179
|
+
m = a;
|
|
179
180
|
else if (typeof a == "string")
|
|
180
|
-
|
|
181
|
+
m = new Date(a);
|
|
181
182
|
else
|
|
182
183
|
return {
|
|
183
184
|
code: "date_invalid",
|
|
184
185
|
message: e?.message || "Неверный формат даты",
|
|
185
186
|
params: e?.params
|
|
186
187
|
};
|
|
187
|
-
if (isNaN(
|
|
188
|
+
if (isNaN(m.getTime()))
|
|
188
189
|
return {
|
|
189
190
|
code: "date_invalid",
|
|
190
191
|
message: e?.message || "Неверный формат даты",
|
|
191
192
|
params: e?.params
|
|
192
193
|
};
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
194
|
+
const s = /* @__PURE__ */ new Date();
|
|
195
|
+
if (s.setHours(0, 0, 0, 0), e?.minDate) {
|
|
196
|
+
const t = new Date(e.minDate);
|
|
197
|
+
if (t.setHours(0, 0, 0, 0), m < t)
|
|
197
198
|
return {
|
|
198
199
|
code: "date_min",
|
|
199
|
-
message: e?.message || `Дата должна быть не ранее ${
|
|
200
|
+
message: e?.message || `Дата должна быть не ранее ${t.toLocaleDateString()}`,
|
|
200
201
|
params: { minDate: e.minDate, ...e?.params }
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
if (e?.maxDate) {
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
205
|
+
const t = new Date(e.maxDate);
|
|
206
|
+
if (t.setHours(0, 0, 0, 0), m > t)
|
|
206
207
|
return {
|
|
207
208
|
code: "date_max",
|
|
208
|
-
message: e?.message || `Дата должна быть не позднее ${
|
|
209
|
+
message: e?.message || `Дата должна быть не позднее ${t.toLocaleDateString()}`,
|
|
209
210
|
params: { maxDate: e.maxDate, ...e?.params }
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
|
-
if (e?.noFuture &&
|
|
213
|
+
if (e?.noFuture && m > s)
|
|
213
214
|
return {
|
|
214
215
|
code: "date_future",
|
|
215
216
|
message: e?.message || "Дата не может быть в будущем",
|
|
216
217
|
params: e?.params
|
|
217
218
|
};
|
|
218
|
-
if (e?.noPast &&
|
|
219
|
+
if (e?.noPast && m < s)
|
|
219
220
|
return {
|
|
220
221
|
code: "date_past",
|
|
221
222
|
message: e?.message || "Дата не может быть в прошлом",
|
|
222
223
|
params: e?.params
|
|
223
224
|
};
|
|
224
225
|
if (e?.minAge !== void 0 || e?.maxAge !== void 0) {
|
|
225
|
-
const
|
|
226
|
-
(
|
|
226
|
+
const t = Math.floor(
|
|
227
|
+
(s.getTime() - m.getTime()) / 315576e5
|
|
227
228
|
);
|
|
228
|
-
if (e?.minAge !== void 0 &&
|
|
229
|
+
if (e?.minAge !== void 0 && t < e.minAge)
|
|
229
230
|
return {
|
|
230
231
|
code: "date_min_age",
|
|
231
232
|
message: e?.message || `Минимальный возраст: ${e.minAge} лет`,
|
|
232
|
-
params: { minAge: e.minAge, currentAge:
|
|
233
|
+
params: { minAge: e.minAge, currentAge: t, ...e?.params }
|
|
233
234
|
};
|
|
234
|
-
if (e?.maxAge !== void 0 &&
|
|
235
|
+
if (e?.maxAge !== void 0 && t > e.maxAge)
|
|
235
236
|
return {
|
|
236
237
|
code: "date_max_age",
|
|
237
238
|
message: e?.message || `Максимальный возраст: ${e.maxAge} лет`,
|
|
238
|
-
params: { maxAge: e.maxAge, currentAge:
|
|
239
|
+
params: { maxAge: e.maxAge, currentAge: t, ...e?.params }
|
|
239
240
|
};
|
|
240
241
|
}
|
|
241
242
|
return null;
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
|
-
function
|
|
245
|
-
r &&
|
|
245
|
+
function z(r, e) {
|
|
246
|
+
r && d(r, 1, {
|
|
246
247
|
message: e?.message || "Массив не должен быть пустым",
|
|
247
248
|
params: { minLength: 1, ...e?.params }
|
|
248
249
|
});
|
|
249
250
|
}
|
|
250
|
-
function
|
|
251
|
+
function I(r, e) {
|
|
251
252
|
if (!r) return;
|
|
252
|
-
const a =
|
|
253
|
+
const a = c(r);
|
|
253
254
|
u().registerArrayItemValidation(a, e);
|
|
254
255
|
}
|
|
255
|
-
function i(r) {
|
|
256
|
-
return r instanceof x ? [r] : r instanceof y ? Array.from(r.getAllFields()).flatMap(i) : r instanceof A ? r.map((e) => i(e)).flat() : [];
|
|
257
|
-
}
|
|
258
|
-
async function j(r, e) {
|
|
259
|
-
const a = new w();
|
|
260
|
-
a.beginRegistration();
|
|
261
|
-
let t = [], n = !1;
|
|
262
|
-
try {
|
|
263
|
-
const s = f();
|
|
264
|
-
e(s), t = a.getCurrentContext()?.getValidators() || [], a.cancelRegistration(), n = !0, r.clearErrors();
|
|
265
|
-
const l = i(r);
|
|
266
|
-
return await Promise.all(l.map((d) => d.validate())), t.length > 0 && await r.applyContextualValidators(t), r.valid.value;
|
|
267
|
-
} catch (s) {
|
|
268
|
-
throw n || a.cancelRegistration(), s;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
256
|
export {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
257
|
+
H as TreeValidationContextImpl,
|
|
258
|
+
P as ValidationContextImpl,
|
|
259
|
+
M as ValidationRegistry,
|
|
260
|
+
$ as apply,
|
|
261
|
+
o as applyWhen,
|
|
262
|
+
i as createFieldPath,
|
|
263
|
+
q as date,
|
|
264
|
+
L as email,
|
|
265
|
+
W as extractKey,
|
|
266
|
+
c as extractPath,
|
|
267
|
+
h as max,
|
|
268
|
+
b as maxLength,
|
|
269
|
+
D as min,
|
|
270
|
+
d as minLength,
|
|
271
|
+
z as notEmpty,
|
|
272
|
+
N as number,
|
|
273
|
+
R as pattern,
|
|
274
|
+
C as phone,
|
|
275
|
+
w as required,
|
|
276
|
+
l as toFieldPath,
|
|
277
|
+
T as url,
|
|
278
|
+
n as validate,
|
|
279
|
+
A as validateAsync,
|
|
295
280
|
j as validateForm,
|
|
296
|
-
|
|
297
|
-
|
|
281
|
+
I as validateItems,
|
|
282
|
+
_ as validateTree
|
|
298
283
|
};
|
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
|
|
@@ -642,3 +691,193 @@ const schema: FormSchema<MyForm> = {
|
|
|
642
691
|
},
|
|
643
692
|
};
|
|
644
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' });
|