@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.
@@ -1,44 +1,45 @@
1
- import { h as g, t as o, j as f, a as x, G as y, A } from "./node-factory-D7DOnSSN.js";
2
- import { T as G, V as U, k as B } from "./node-factory-D7DOnSSN.js";
3
- import { g as u, V as w } from "./create-field-path-CdPF3lIK.js";
4
- function m(r, e, a) {
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 t = g(r);
7
- u().registerSync(t, e, a);
7
+ const m = c(r);
8
+ u().registerSync(m, e, a);
8
9
  }
9
- function D(r, e, a) {
10
- const t = g(r);
11
- u().registerAsync(t, e, a);
10
+ function A(r, e, a) {
11
+ const m = c(r);
12
+ u().registerAsync(m, e, a);
12
13
  }
13
- function b(r, e) {
14
+ function _(r, e) {
14
15
  u().registerTree(r, e);
15
16
  }
16
- function R(r, e) {
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
- ), t = Array.isArray(e) ? e : [e];
24
- for (const n of a) {
25
- const s = o(n);
26
- for (const c of t)
27
- c(s);
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 p(r, e, a) {
31
- const t = g(r);
32
- u().enterCondition(t, e);
31
+ function o(r, e, a) {
32
+ const m = c(r);
33
+ u().enterCondition(m, e);
33
34
  try {
34
- const n = f();
35
- a(n);
35
+ const s = i();
36
+ a(s);
36
37
  } finally {
37
38
  u().exitCondition();
38
39
  }
39
40
  }
40
- function N(r, e) {
41
- r && m(r, (a) => a == null || a === "" ? {
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 L(r, e, a) {
52
- r && m(r, (t) => t == null ? null : t < e ? {
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: t, ...a?.params }
56
+ params: { min: e, actual: m, ...a?.params }
56
57
  } : null);
57
58
  }
58
- function C(r, e, a) {
59
- r && m(r, (t) => t == null ? null : t > e ? {
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: t, ...a?.params }
63
+ params: { max: e, actual: m, ...a?.params }
63
64
  } : null);
64
65
  }
65
- function _(r, e, a) {
66
- r && m(r, (t) => t && t.length < e ? {
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: t.length, ...a?.params }
70
+ params: { minLength: e, actualLength: m.length, ...a?.params }
70
71
  } : null);
71
72
  }
72
- function T(r, e, a) {
73
- r && m(r, (t) => t && t.length > e ? {
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: t.length, ...a?.params }
77
+ params: { maxLength: e, actualLength: m.length, ...a?.params }
77
78
  } : null);
78
79
  }
79
- function V(r, e) {
80
+ function L(r, e) {
80
81
  if (!r) return;
81
82
  const a = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
82
- m(r, (t) => t ? a.test(t) ? null : {
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 q(r, e, a) {
89
- r && m(r, (t) => t ? e.test(t) ? null : {
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 z(r, e) {
96
+ function T(r, e) {
96
97
  if (!r) return;
97
- const a = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i, t = /^https?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i;
98
- m(r, (n) => n ? (e?.requireProtocol ? t : a).test(n) ? e?.allowedProtocols && e.allowedProtocols.length > 0 && !e.allowedProtocols.some(
99
- (l) => n.toLowerCase().startsWith(`${l}://`)
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 I(r, e) {
111
+ function C(r, e) {
111
112
  if (!r) return;
112
- const a = e?.format || "any", t = {
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
- m(r, (n) => {
123
- if (!n)
123
+ n(r, (s) => {
124
+ if (!s)
124
125
  return null;
125
- if (!t[a].test(n)) {
126
- const c = {
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 || c[a],
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 P(r, e) {
142
- r && m(r, (a) => a == null ? null : typeof a != "number" || isNaN(a) ? {
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 H(r, e) {
173
- r && m(r, (a) => {
173
+ function q(r, e) {
174
+ r && n(r, (a) => {
174
175
  if (!a)
175
176
  return null;
176
- let t;
177
+ let m;
177
178
  if (a instanceof Date)
178
- t = a;
179
+ m = a;
179
180
  else if (typeof a == "string")
180
- t = new Date(a);
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(t.getTime()))
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 n = /* @__PURE__ */ new Date();
194
- if (n.setHours(0, 0, 0, 0), e?.minDate) {
195
- const s = new Date(e.minDate);
196
- if (s.setHours(0, 0, 0, 0), t < s)
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 || `Дата должна быть не ранее ${s.toLocaleDateString()}`,
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 s = new Date(e.maxDate);
205
- if (s.setHours(0, 0, 0, 0), t > s)
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 || `Дата должна быть не позднее ${s.toLocaleDateString()}`,
209
+ message: e?.message || `Дата должна быть не позднее ${t.toLocaleDateString()}`,
209
210
  params: { maxDate: e.maxDate, ...e?.params }
210
211
  };
211
212
  }
212
- if (e?.noFuture && t > n)
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 && t < n)
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 s = Math.floor(
226
- (n.getTime() - t.getTime()) / 315576e5
226
+ const t = Math.floor(
227
+ (s.getTime() - m.getTime()) / 315576e5
227
228
  );
228
- if (e?.minAge !== void 0 && s < e.minAge)
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: s, ...e?.params }
233
+ params: { minAge: e.minAge, currentAge: t, ...e?.params }
233
234
  };
234
- if (e?.maxAge !== void 0 && s > e.maxAge)
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: s, ...e?.params }
239
+ params: { maxAge: e.maxAge, currentAge: t, ...e?.params }
239
240
  };
240
241
  }
241
242
  return null;
242
243
  });
243
244
  }
244
- function M(r, e) {
245
- r && _(r, 1, {
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 W(r, e) {
251
+ function I(r, e) {
251
252
  if (!r) return;
252
- const a = g(r);
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
- G as TreeValidationContextImpl,
273
- U as ValidationContextImpl,
274
- w as ValidationRegistry,
275
- R as apply,
276
- p as applyWhen,
277
- f as createFieldPath,
278
- H as date,
279
- V as email,
280
- B as extractKey,
281
- g as extractPath,
282
- C as max,
283
- T as maxLength,
284
- L as min,
285
- _ as minLength,
286
- M as notEmpty,
287
- P as number,
288
- q as pattern,
289
- I as phone,
290
- N as required,
291
- o as toFieldPath,
292
- z as url,
293
- m as validate,
294
- D as validateAsync,
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
- W as validateItems,
297
- b as validateTree
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` | `@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
@@ -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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reformer/core",
3
- "version": "1.1.0-beta.3",
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",