adaptform 2.0.5 → 2.2.0

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/index.mjs CHANGED
@@ -1,11 +1,332 @@
1
+ // src/core/state/value.ts
2
+ function copyValue(value, seen = /* @__PURE__ */ new WeakMap()) {
3
+ if (value === null || typeof value !== "object") return value;
4
+ if (seen.has(value)) return seen.get(value);
5
+ const remember = (copy) => {
6
+ seen.set(value, copy);
7
+ return copy;
8
+ };
9
+ if (value instanceof Date) return remember(new Date(value.getTime()));
10
+ if (value instanceof RegExp) return remember(new RegExp(value.source, value.flags));
11
+ if (value instanceof ArrayBuffer) return remember(value.slice(0));
12
+ if (ArrayBuffer.isView(value)) {
13
+ const bytes = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
14
+ return remember(value instanceof DataView ? new DataView(bytes) : new value.constructor(bytes));
15
+ }
16
+ if (value instanceof Map) {
17
+ const result = /* @__PURE__ */ new Map();
18
+ seen.set(value, result);
19
+ for (const [key, item] of value) result.set(copyValue(key, seen), copyValue(item, seen));
20
+ return result;
21
+ }
22
+ if (value instanceof Set) {
23
+ const result = /* @__PURE__ */ new Set();
24
+ seen.set(value, result);
25
+ for (const item of value) result.add(copyValue(item, seen));
26
+ return result;
27
+ }
28
+ if (Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
29
+ const result = Array.isArray(value) ? new Array(value.length) : Object.create(Object.getPrototypeOf(value));
30
+ seen.set(value, result);
31
+ for (const key of Object.keys(value)) Object.defineProperty(result, key, {
32
+ value: copyValue(value[key], seen),
33
+ enumerable: true,
34
+ writable: true,
35
+ configurable: true
36
+ });
37
+ return result;
38
+ }
39
+ return value;
40
+ }
41
+ function equalValue(a, b, seen = /* @__PURE__ */ new WeakMap()) {
42
+ if (Object.is(a, b)) return true;
43
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
44
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
45
+ if (seen.get(a)?.has(b)) return true;
46
+ const matches = seen.get(a) ?? /* @__PURE__ */ new WeakSet();
47
+ matches.add(b);
48
+ seen.set(a, matches);
49
+ if (a instanceof Date && b instanceof Date) return Object.is(a.getTime(), b.getTime());
50
+ if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags;
51
+ if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) return equalValue(new Uint8Array(a), new Uint8Array(b), seen);
52
+ if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
53
+ const av = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
54
+ const bv = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
55
+ return av.length === bv.length && av.every((byte, i) => byte === bv[i]);
56
+ }
57
+ if (a instanceof Map && b instanceof Map) return equalValue([...a], [...b], seen);
58
+ if (a instanceof Set && b instanceof Set) return equalValue([...a], [...b], seen);
59
+ if ("isLuxonDateTime" in a && a.isLuxonDateTime === true && "equals" in a && typeof a.equals === "function") {
60
+ return a.equals(b);
61
+ }
62
+ if (!Array.isArray(a) && Object.getPrototypeOf(a) !== Object.prototype && Object.getPrototypeOf(a) !== null) return false;
63
+ if (Array.isArray(a) && Array.isArray(b) && a.length !== b.length) return false;
64
+ const keys = Object.keys(a);
65
+ return keys.length === Object.keys(b).length && keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && equalValue(a[key], b[key], seen));
66
+ }
67
+
68
+ // src/core/state/snapshot.ts
69
+ function snapshotValue(value, seen = /* @__PURE__ */ new WeakMap()) {
70
+ if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
71
+ if (typeof value !== "object") return Object.freeze({ kind: "Opaque", type: typeof value });
72
+ const previous = seen.get(value);
73
+ if (previous) return previous;
74
+ if (value instanceof Date) return Object.freeze({ kind: "Date", timestamp: value.getTime() });
75
+ if (value instanceof RegExp) return Object.freeze({ kind: "RegExp", source: value.source, flags: value.flags });
76
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
77
+ const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
78
+ return Object.freeze({ kind: value.constructor.name, bytes: Object.freeze([...bytes]) });
79
+ }
80
+ if ("isLuxonDateTime" in value && value.isLuxonDateTime === true && "toISO" in value && typeof value.toISO === "function") {
81
+ return Object.freeze({ kind: "DateTime", iso: value.toISO() });
82
+ }
83
+ if (value instanceof Map || value instanceof Set) {
84
+ const result = { kind: value instanceof Map ? "Map" : "Set" };
85
+ seen.set(value, result);
86
+ result.entries = snapshotValue([...value], seen);
87
+ return Object.freeze(result);
88
+ }
89
+ if (Array.isArray(value)) {
90
+ const result = [];
91
+ seen.set(value, result);
92
+ for (const item of value) result.push(snapshotValue(item, seen));
93
+ return Object.freeze(result);
94
+ }
95
+ if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
96
+ const result = /* @__PURE__ */ Object.create(null);
97
+ seen.set(value, result);
98
+ for (const [key, item] of Object.entries(value)) result[key] = snapshotValue(item, seen);
99
+ return Object.freeze(result);
100
+ }
101
+ return Object.freeze({ kind: "Opaque", type: value.constructor?.name ?? "object" });
102
+ }
103
+ function freezeSnapshot(value, seen = /* @__PURE__ */ new WeakSet()) {
104
+ if (value === null || typeof value !== "object" || seen.has(value)) return value;
105
+ seen.add(value);
106
+ for (const item of Object.values(value)) freezeSnapshot(item, seen);
107
+ return Object.freeze(value);
108
+ }
109
+
110
+ // src/core/state/ObservableNode.ts
111
+ var mutationDepth = 0;
112
+ var flushing = false;
113
+ var publishing = false;
114
+ var batchDepth = 0;
115
+ var validations = /* @__PURE__ */ new Map();
116
+ var batchRoots = /* @__PURE__ */ new Map();
117
+ var pending = /* @__PURE__ */ new Set();
118
+ function flush() {
119
+ if (mutationDepth || flushing) return;
120
+ flushing = true;
121
+ let failure;
122
+ let failed = false;
123
+ try {
124
+ while (pending.size) {
125
+ const stores = [...pending];
126
+ const before = stores.map((store) => store.committed());
127
+ pending.clear();
128
+ for (const store of stores) store.refresh();
129
+ const changed = stores.filter((store, index) => !equalValue(before[index], store.committed()));
130
+ publishing = true;
131
+ try {
132
+ for (const store of changed) {
133
+ try {
134
+ store.notify();
135
+ } catch (error) {
136
+ if (!failed) {
137
+ failure = error;
138
+ failed = true;
139
+ }
140
+ }
141
+ }
142
+ } finally {
143
+ publishing = false;
144
+ }
145
+ }
146
+ } finally {
147
+ flushing = false;
148
+ }
149
+ if (failed) throw failure;
150
+ }
151
+ var SnapshotStore = class {
152
+ constructor(capture, getChildren) {
153
+ this.capture = capture;
154
+ this.getChildren = getChildren;
155
+ this.initialized = false;
156
+ this.listeners = /* @__PURE__ */ new Set();
157
+ this.parents = /* @__PURE__ */ new Set();
158
+ this.children = /* @__PURE__ */ new Set();
159
+ }
160
+ reconcile(seen = /* @__PURE__ */ new Set()) {
161
+ if (seen.has(this)) return;
162
+ seen.add(this);
163
+ const next = new Set(this.listeners.size || this.parents.size ? this.getChildren() : []);
164
+ for (const child of this.children) {
165
+ if (!next.has(child)) {
166
+ child.parents.delete(this);
167
+ child.reconcile(seen);
168
+ }
169
+ }
170
+ const previous = this.children;
171
+ this.children = next;
172
+ for (const child of next) {
173
+ if (!previous.has(child)) {
174
+ child.parents.add(this);
175
+ child.reconcile(seen);
176
+ }
177
+ }
178
+ }
179
+ read() {
180
+ if (!mutationDepth && !publishing) this.refresh();
181
+ else if (!this.initialized) this.refresh();
182
+ return this.snapshot;
183
+ }
184
+ committed() {
185
+ return this.snapshot;
186
+ }
187
+ refresh() {
188
+ this.reconcile();
189
+ const next = freezeSnapshot(this.capture());
190
+ if (this.initialized && equalValue(this.snapshot, next)) return false;
191
+ this.snapshot = next;
192
+ this.initialized = true;
193
+ return true;
194
+ }
195
+ subscribe(listener) {
196
+ this.read();
197
+ const registration = () => listener();
198
+ this.listeners.add(registration);
199
+ this.reconcile();
200
+ return () => {
201
+ this.listeners.delete(registration);
202
+ this.reconcile();
203
+ };
204
+ }
205
+ changed(seen = /* @__PURE__ */ new Set()) {
206
+ if (seen.has(this)) return;
207
+ seen.add(this);
208
+ this.reconcile();
209
+ if (this.listeners.size || this.parents.size || this.initialized) pending.add(this);
210
+ for (const parent of this.parents) parent.changed(seen);
211
+ }
212
+ notify() {
213
+ let failure;
214
+ let failed = false;
215
+ for (const listener of [...this.listeners]) {
216
+ if (!this.listeners.has(listener)) continue;
217
+ try {
218
+ listener();
219
+ } catch (error) {
220
+ if (!failed) {
221
+ failure = error;
222
+ failed = true;
223
+ }
224
+ }
225
+ }
226
+ if (failed) throw failure;
227
+ }
228
+ };
229
+ var ObservableNode = class {
230
+ constructor() {
231
+ this._stateStore = new SnapshotStore(
232
+ () => this.captureState(),
233
+ () => this.stateChildren().map((child) => child._stateStore)
234
+ );
235
+ }
236
+ stateChildren() {
237
+ return [];
238
+ }
239
+ contains(node, seen = /* @__PURE__ */ new Set()) {
240
+ if (this === node) return true;
241
+ if (seen.has(this)) return false;
242
+ seen.add(this);
243
+ return this.stateChildren().some((child) => child.contains(node, seen));
244
+ }
245
+ validateAfterMutation(callback) {
246
+ if (batchDepth) validations.set(this, callback);
247
+ else callback();
248
+ }
249
+ batchUpdate(callback, validate) {
250
+ this.getSnapshot();
251
+ return this.update(() => {
252
+ batchDepth++;
253
+ batchRoots.set(this, validate);
254
+ let failed = false;
255
+ let failure;
256
+ let result;
257
+ try {
258
+ result = callback();
259
+ if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result && typeof result.then === "function") {
260
+ throw new TypeError("batch callbacks must be synchronous");
261
+ }
262
+ } catch (error) {
263
+ failed = true;
264
+ failure = error;
265
+ } finally {
266
+ batchDepth--;
267
+ }
268
+ if (!batchDepth) {
269
+ const requested = [...validations];
270
+ const roots = [...batchRoots];
271
+ validations.clear();
272
+ batchRoots.clear();
273
+ const selected = roots.filter(([root]) => requested.some(([node]) => root.contains(node))).filter(([root], index, all) => !all.some(([other], otherIndex) => other !== root && other.contains(root) && (!root.contains(other) || otherIndex < index)));
274
+ const checks = [
275
+ ...selected.map(([, check]) => check),
276
+ ...requested.filter(([node]) => !selected.some(([root]) => root.contains(node))).map(([, check]) => check)
277
+ ];
278
+ for (const check of checks) {
279
+ try {
280
+ check();
281
+ } catch (error) {
282
+ if (!failed) {
283
+ failed = true;
284
+ failure = error;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ if (failed) throw failure;
290
+ return result;
291
+ });
292
+ }
293
+ subscribe(listener) {
294
+ return this._stateStore.subscribe(listener);
295
+ }
296
+ getSnapshot() {
297
+ return this._stateStore.read();
298
+ }
299
+ update(callback) {
300
+ mutationDepth++;
301
+ let failed = false;
302
+ try {
303
+ return callback();
304
+ } catch (error) {
305
+ failed = true;
306
+ throw error;
307
+ } finally {
308
+ this._stateStore.changed();
309
+ mutationDepth--;
310
+ if (failed) {
311
+ try {
312
+ flush();
313
+ } catch {
314
+ }
315
+ } else flush();
316
+ }
317
+ }
318
+ };
319
+
1
320
  // src/core/fields/Field.ts
2
- var Field = class {
321
+ var Field = class extends ObservableNode {
3
322
  constructor(TypeConstructor, defaultValue, options) {
323
+ super();
4
324
  this._rawValue = null;
5
325
  this._valueClear = null;
326
+ this._baseline = null;
6
327
  this._errors = [];
7
- this.isValid = null;
8
- this.isTouched = false;
328
+ this._isValid = null;
329
+ this._isTouched = false;
9
330
  this.fieldType = new TypeConstructor(options ?? {});
10
331
  this.defaultValue = defaultValue ?? null;
11
332
  for (const plugin of this.options.plugins ?? []) {
@@ -18,8 +339,36 @@ var Field = class {
18
339
  this._valueClear = null;
19
340
  this.isValid = null;
20
341
  }
342
+ this.isTouched = false;
343
+ this.commitBaseline();
344
+ }
345
+ get isValid() {
346
+ return this._isValid;
347
+ }
348
+ set isValid(value) {
349
+ this.update(() => {
350
+ this._isValid = value;
351
+ });
352
+ }
353
+ get isTouched() {
354
+ return this._isTouched;
355
+ }
356
+ set isTouched(value) {
357
+ this.update(() => {
358
+ this._isTouched = value;
359
+ });
21
360
  }
22
- // ========== Геттеры/сеттеры ==========
361
+ captureState() {
362
+ return {
363
+ rawValue: snapshotValue(this._rawValue),
364
+ valueClear: snapshotValue(this._valueClear),
365
+ errors: [...this._errors],
366
+ isValid: this.isValid,
367
+ isTouched: this.isTouched,
368
+ isDirty: this.isDirty
369
+ };
370
+ }
371
+ // ========== Getters and setters ==========
23
372
  get plugins() {
24
373
  return [...this.options.plugins ?? []].sort((a, b) => {
25
374
  return (a.pluginOptions?.priority ?? 0) - (b.pluginOptions?.priority ?? 0);
@@ -29,13 +378,30 @@ var Field = class {
29
378
  return this.fieldType.options;
30
379
  }
31
380
  set options(options) {
32
- let cur_options = this.fieldType.options;
33
- this.fieldType.options = Object.assign(cur_options, options);
381
+ this.update(() => {
382
+ const currentOptions = this.fieldType.options;
383
+ this.fieldType.options = Object.assign(currentOptions, options);
384
+ });
34
385
  }
35
386
  get rawValue() {
36
387
  return this._rawValue;
37
388
  }
38
389
  set rawValue(input) {
390
+ this.setRawValue(input);
391
+ }
392
+ setRawValue(input, validate = true) {
393
+ return this.update(() => {
394
+ this.assignRawValue(input);
395
+ this._errors = [];
396
+ this.isTouched = true;
397
+ if (validate) {
398
+ this.validateAfterMutation(() => this.checkAndSetValid());
399
+ } else {
400
+ this.isValid = null;
401
+ }
402
+ });
403
+ }
404
+ assignRawValue(input) {
39
405
  let rawValue = input;
40
406
  for (const plugin of this.plugins) {
41
407
  if (plugin.toRawValue) {
@@ -50,9 +416,6 @@ var Field = class {
50
416
  }
51
417
  this._rawValue = rawValue;
52
418
  this._valueClear = this.fieldType.cast(valueClear);
53
- this._errors = [];
54
- this.isTouched = true;
55
- this.checkAndSetValid();
56
419
  }
57
420
  get errors() {
58
421
  return this._errors;
@@ -61,17 +424,27 @@ var Field = class {
61
424
  return this._errors[0] ?? "";
62
425
  }
63
426
  set error(message) {
64
- if (message) {
65
- this._errors = [message];
66
- } else {
67
- this._errors = [];
68
- }
69
- this.isValid = message ? false : null;
427
+ this.update(() => {
428
+ if (message) {
429
+ this._errors = [message];
430
+ } else {
431
+ this._errors = [];
432
+ }
433
+ this.isValid = message ? false : null;
434
+ });
70
435
  }
71
436
  get valueClear() {
72
437
  return this._valueClear;
73
438
  }
74
- // ========== Валидация ==========
439
+ get isDirty() {
440
+ return !equalValue(this._valueClear, this._baseline);
441
+ }
442
+ commitBaseline() {
443
+ return this.update(() => {
444
+ this._baseline = copyValue(this._valueClear);
445
+ });
446
+ }
447
+ // ========== Validation ==========
75
448
  isRequired(form) {
76
449
  let isRequired;
77
450
  if (typeof this.options.isRequired === "function") {
@@ -82,107 +455,165 @@ var Field = class {
82
455
  return isRequired;
83
456
  }
84
457
  checkValid(form) {
85
- this._errors = [];
86
- if (this.isEmpty) {
87
- if (this.isRequired(form)) {
88
- this._errors.push(this.options.messages?.isRequired ?? "\u041F\u043E\u043B\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043B\u044F \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F");
89
- return false;
458
+ return this.update(() => {
459
+ this._errors = [];
460
+ if (this.isEmpty) {
461
+ if (this.isRequired(form)) {
462
+ this._errors.push(this.options.messages?.isRequired ?? "\u041F\u043E\u043B\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043B\u044F \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F");
463
+ return false;
464
+ }
465
+ return true;
90
466
  }
91
- return true;
92
- }
93
- if (this._valueClear === null) {
94
- this._errors.push(
95
- this.options.messages?.type ?? `\u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ${this.fieldType.getTypeName()}`
96
- );
97
- return false;
98
- }
99
- const typeErrors = this.fieldType.validate(this._valueClear, form);
100
- if (typeErrors.length > 0) {
101
- this._errors.push(...typeErrors);
102
- return false;
103
- }
104
- if (this.options.validate) {
105
- const result = this.options.validate(this._valueClear, form);
106
- if (result === false) {
107
- this._errors.push(this.options.messages?.validate ?? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u043E \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E");
467
+ if (this._valueClear === null) {
468
+ this._errors.push(
469
+ this.options.messages?.type ?? `\u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ${this.fieldType.getTypeName()}`
470
+ );
108
471
  return false;
109
472
  }
110
- if (typeof result === "string") {
111
- this._errors.push(result);
473
+ const typeErrors = this.fieldType.validate(this._valueClear, form);
474
+ if (typeErrors.length > 0) {
475
+ this._errors.push(...typeErrors);
112
476
  return false;
113
477
  }
114
- }
115
- return true;
478
+ if (this.options.validate) {
479
+ const result = this.options.validate(this._valueClear, form);
480
+ if (result === false) {
481
+ this._errors.push(this.options.messages?.validate ?? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u043E \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E");
482
+ return false;
483
+ }
484
+ if (typeof result === "string") {
485
+ this._errors.push(result);
486
+ return false;
487
+ }
488
+ }
489
+ return true;
490
+ });
116
491
  }
117
492
  checkAndSetValid(form) {
118
- this.isValid = this.checkValid(form);
119
- return this.isValid;
493
+ return this.update(() => {
494
+ this.isValid = this.checkValid(form);
495
+ return this.isValid;
496
+ });
120
497
  }
121
498
  get isEmpty() {
122
499
  let isEmpty = false;
123
500
  if (this._rawValue === null || this._rawValue === void 0) isEmpty = true;
124
- else if (this._rawValue === "") isEmpty = true;
501
+ else if (typeof this._rawValue === "string" && this._rawValue.trim() === "") isEmpty = true;
125
502
  else if (Array.isArray(this._rawValue) && this._rawValue.length === 0) isEmpty = true;
126
503
  return isEmpty;
127
504
  }
128
- // ========== Вспомогательные методы ==========
505
+ // ========== Helper methods ==========
129
506
  reset() {
130
- if (this.defaultValue !== null && this.defaultValue !== void 0) {
131
- this.rawValue = this.defaultValue;
132
- } else {
133
- this._rawValue = null;
134
- this._valueClear = null;
507
+ return this.update(() => {
508
+ if (this.defaultValue !== null && this.defaultValue !== void 0) {
509
+ this.assignRawValue(this.defaultValue);
510
+ } else {
511
+ this._rawValue = null;
512
+ this._valueClear = null;
513
+ }
514
+ this._errors = [];
135
515
  this.isValid = null;
136
- }
137
- this._errors = [];
138
- this.isTouched = false;
516
+ this.isTouched = false;
517
+ });
139
518
  }
140
519
  };
141
520
 
521
+ // src/core/fields/arrayUtils.ts
522
+ function isValidArrayIndex(index, length) {
523
+ return Number.isInteger(index) && index >= 0 && index < length;
524
+ }
525
+
142
526
  // src/core/fields/ArrayField.ts
143
- var ArrayField = class {
527
+ var ArrayField = class extends ObservableNode {
144
528
  constructor(factory, options = {}, initial = []) {
529
+ super();
145
530
  this.factory = factory;
146
531
  this._items = [];
147
532
  this._errors = [];
148
- this.isTouched = false;
533
+ this._baseline = [];
534
+ this._hasValidationResult = false;
535
+ this._isTouched = false;
149
536
  this.options = {
150
537
  isRequired: true,
151
538
  messages: {},
152
539
  ...options
153
540
  };
154
- this._items = initial.map((v) => this.factory(v));
155
- this.checkValid();
541
+ this._items = initial.map((value) => this.factory(value));
542
+ this.validateAfterMutation(() => this.checkValid());
543
+ this.commitBaseline();
544
+ }
545
+ captureState() {
546
+ return {
547
+ itemsValue: snapshotValue(this.itemsValue),
548
+ items: this._items.map((item) => item.getSnapshot()),
549
+ errors: [...this._errors],
550
+ isValid: this.isValid,
551
+ isTouched: this.isTouched,
552
+ isDirty: this.isDirty
553
+ };
554
+ }
555
+ stateChildren() {
556
+ return this._items;
557
+ }
558
+ get isTouched() {
559
+ return this._isTouched;
560
+ }
561
+ set isTouched(value) {
562
+ this.update(() => {
563
+ this._isTouched = value;
564
+ });
565
+ }
566
+ get isDirty() {
567
+ return !equalValue(this.itemsValue, this._baseline) || this._items.some((child) => child.isDirty);
568
+ }
569
+ commitBaseline() {
570
+ return this.update(() => {
571
+ for (const child of this._items) child.commitBaseline();
572
+ this._baseline = copyValue(this.itemsValue);
573
+ });
156
574
  }
157
575
  // ===== value =====
158
576
  set itemsValue(items) {
159
- this.clear();
160
- for (const i of items) {
161
- this.add(i, false);
162
- }
163
- this.checkValid();
577
+ this.setItemsValue(items);
578
+ }
579
+ setItemsValue(items, validate = true) {
580
+ return this.update(() => {
581
+ this._items = [];
582
+ this.error = null;
583
+ for (const item of items) {
584
+ this.add(item, false);
585
+ }
586
+ if (validate) this.validateAfterMutation(() => this.checkValid());
587
+ });
164
588
  }
165
589
  get itemsValue() {
166
- return this._items.map((i) => i.valueClear);
590
+ return this._items.map((item) => item.valueClear);
167
591
  }
168
592
  get items() {
169
593
  return this._items;
170
594
  }
171
595
  // ===== actions =====
172
- add(value, force_check) {
173
- const field = this.factory(value);
174
- this.isTouched = true;
175
- this._items.push(field);
176
- if (force_check) this.checkValid();
177
- return field;
596
+ add(value, force_check = true) {
597
+ return this.update(() => {
598
+ const field = this.factory(value);
599
+ this.isTouched = true;
600
+ this._items.push(field);
601
+ if (force_check) this.validateAfterMutation(() => this.checkValid());
602
+ return field;
603
+ });
178
604
  }
179
605
  remove(index) {
180
- this._items.splice(index, 1);
181
- this.checkValid();
606
+ return this.update(() => {
607
+ if (!isValidArrayIndex(index, this._items.length)) return;
608
+ this._items.splice(index, 1);
609
+ this.validateAfterMutation(() => this.checkValid());
610
+ });
182
611
  }
183
612
  clear() {
184
- this._items = [];
185
- this.checkValid();
613
+ return this.update(() => {
614
+ this._items = [];
615
+ this.validateAfterMutation(() => this.checkValid());
616
+ });
186
617
  }
187
618
  // ===== validation =====
188
619
  isRequired(form) {
@@ -195,123 +626,186 @@ var ArrayField = class {
195
626
  return isRequired;
196
627
  }
197
628
  checkValid(form) {
198
- this._errors = [];
199
- if (this.isRequired(form) && this._items.length === 0) {
200
- this._errors.push(
201
- this.options.messages?.isRequired ?? "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u044D\u043B\u0435\u043C\u0435\u043D\u0442"
202
- );
203
- return false;
204
- }
205
- if (this.options.minItems != null && this._items.length < this.options.minItems) {
206
- this._errors.push(
207
- this.options.messages?.minItems ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${this.options.minItems}`
208
- );
209
- return false;
210
- }
211
- if (this.options.maxItems != null && this._items.length > this.options.maxItems) {
212
- this._errors.push(
213
- this.options.messages?.maxItems ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${this.options.maxItems}`
214
- );
215
- return false;
216
- }
217
- let valid = true;
218
- for (const field of this._items) {
219
- const ok = field.checkAndSetValid(form);
220
- if (!ok) valid = false;
221
- }
222
- if (this.options.validate) {
223
- const values = this._items.map((i) => i.valueClear);
224
- const result = this.options.validate(values, form);
225
- if (result === false) {
629
+ return this.update(() => {
630
+ this._hasValidationResult = true;
631
+ this._errors = [];
632
+ if (this.isRequired(form) && this._items.length === 0) {
226
633
  this._errors.push(
227
- this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
634
+ this.options.messages?.isRequired ?? "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u044D\u043B\u0435\u043C\u0435\u043D\u0442"
228
635
  );
229
636
  return false;
230
637
  }
231
- if (typeof result === "string") {
232
- this._errors.push(result);
638
+ if (this.options.minItems != null && this._items.length < this.options.minItems) {
639
+ this._errors.push(
640
+ this.options.messages?.minItems ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${this.options.minItems}`
641
+ );
233
642
  return false;
234
643
  }
235
- }
236
- return valid && this._errors.length === 0;
644
+ if (this.options.maxItems != null && this._items.length > this.options.maxItems) {
645
+ this._errors.push(
646
+ this.options.messages?.maxItems ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${this.options.maxItems}`
647
+ );
648
+ return false;
649
+ }
650
+ let valid = true;
651
+ for (const field of this._items) {
652
+ const ok = field.checkAndSetValid(form);
653
+ if (!ok) valid = false;
654
+ }
655
+ if (this.options.validate) {
656
+ const values = this._items.map((item) => item.valueClear);
657
+ const result = this.options.validate(values, form);
658
+ if (result === false) {
659
+ this._errors.push(
660
+ this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
661
+ );
662
+ return false;
663
+ }
664
+ if (typeof result === "string") {
665
+ this._errors.push(result);
666
+ return false;
667
+ }
668
+ }
669
+ return valid && this._errors.length === 0;
670
+ });
237
671
  }
238
672
  checkAndSetValid(form) {
239
673
  return this.checkValid(form);
240
674
  }
241
675
  get isValid() {
676
+ if (!this._hasValidationResult) return false;
242
677
  if (this._errors.length > 0) return false;
243
- return this._items.every((i) => i.isValid === true);
678
+ return this._items.every((item) => item.isValid === true);
244
679
  }
245
680
  get error() {
246
681
  return this._errors[0] ?? "";
247
682
  }
248
683
  set error(message) {
249
- if (message) {
250
- this._errors = [message];
251
- } else {
252
- this._errors = [];
253
- }
684
+ this.update(() => {
685
+ if (message) {
686
+ this._errors = [message];
687
+ this._hasValidationResult = true;
688
+ } else {
689
+ this._errors = [];
690
+ this._hasValidationResult = false;
691
+ }
692
+ });
254
693
  }
255
694
  get errors() {
256
695
  return this._errors;
257
696
  }
258
697
  reset() {
259
- this._items.forEach((i) => i.reset());
260
- this._errors = [];
261
- this.isTouched = false;
698
+ return this.update(() => {
699
+ this._items.forEach((item) => item.reset());
700
+ this._errors = [];
701
+ this._hasValidationResult = false;
702
+ this.isTouched = false;
703
+ });
262
704
  }
263
705
  };
264
706
 
265
707
  // src/core/fields/ArrayFormField.ts
266
- var ArrayFormField = class {
708
+ var ArrayFormField = class extends ObservableNode {
267
709
  constructor(FormConstructor, options = {}, initial = []) {
710
+ super();
268
711
  this.FormConstructor = FormConstructor;
269
712
  this.options = options;
270
713
  this._forms = [];
271
714
  this._errors = [];
272
- this.isTouched = false;
715
+ this._baseline = [];
716
+ this._hasValidationResult = false;
717
+ this._isTouched = false;
273
718
  this._forms = initial.map((data) => {
274
719
  const form = new this.FormConstructor();
275
- form.fields = data;
720
+ form.setFields(data, false);
276
721
  return form;
277
722
  });
278
- this.checkValid();
723
+ this.validateAfterMutation(() => this.checkValid());
724
+ this.commitBaseline();
725
+ }
726
+ captureState() {
727
+ return {
728
+ formsFieldsValue: snapshotValue(this.formsFieldsValue),
729
+ forms: this._forms.map((form) => form.getSnapshot()),
730
+ errors: [...this._errors],
731
+ isValid: this.isValid,
732
+ isTouched: this.isTouched,
733
+ isDirty: this.isDirty
734
+ };
735
+ }
736
+ stateChildren() {
737
+ return this._forms;
738
+ }
739
+ get isTouched() {
740
+ return this._isTouched;
741
+ }
742
+ set isTouched(value) {
743
+ this.update(() => {
744
+ this._isTouched = value;
745
+ });
746
+ }
747
+ get isDirty() {
748
+ return !equalValue(this.formsFieldsValue, this._baseline) || this._forms.some((child) => child.isDirty);
749
+ }
750
+ commitBaseline() {
751
+ return this.update(() => {
752
+ for (const child of this._forms) child.commitBaseline();
753
+ this._baseline = copyValue(this.formsFieldsValue);
754
+ });
279
755
  }
280
756
  // ===== value =====
281
757
  set forms(forms) {
282
- this.clear();
283
- for (const f of forms) {
284
- this.add(f, false);
285
- }
286
- this.checkValid();
758
+ this.setForms(forms);
759
+ }
760
+ setForms(forms, validate = true) {
761
+ return this.update(() => {
762
+ this._forms = [];
763
+ for (const formData of forms) {
764
+ this.add(formData, false);
765
+ }
766
+ if (validate) this.validateAfterMutation(() => this.checkValid());
767
+ });
287
768
  }
288
769
  get forms() {
289
770
  return this._forms;
290
771
  }
291
772
  get formsFieldsValue() {
292
- return this._forms.map((i) => i.fieldsValue);
773
+ return this._forms.map((form) => form.fieldsValue);
293
774
  }
294
775
  // ===== actions =====
295
- add(data, force_check) {
296
- const form = new this.FormConstructor();
297
- if (data) form.fields = data;
298
- this.isTouched = true;
299
- this._forms.push(form);
300
- if (force_check) this.checkValid();
301
- return form;
776
+ add(data, force_check = true) {
777
+ return this.update(() => {
778
+ const form = new this.FormConstructor();
779
+ if (data !== void 0 && data !== null) {
780
+ form.setFields(data, false);
781
+ }
782
+ form.commitBaseline();
783
+ this.isTouched = true;
784
+ this._forms.push(form);
785
+ if (force_check) this.validateAfterMutation(() => this.checkValid());
786
+ return form;
787
+ });
302
788
  }
303
789
  remove(index) {
304
- this._forms.splice(index, 1);
305
- this.checkValid();
790
+ return this.update(() => {
791
+ if (!isValidArrayIndex(index, this._forms.length)) return;
792
+ this._forms.splice(index, 1);
793
+ this.validateAfterMutation(() => this.checkValid());
794
+ });
306
795
  }
307
796
  clear() {
308
- this._forms = [];
309
- this.checkValid();
797
+ return this.update(() => {
798
+ this._forms = [];
799
+ this.validateAfterMutation(() => this.checkValid());
800
+ });
310
801
  }
311
802
  reset() {
312
- this._forms.forEach((f) => f.reset());
313
- this._errors = [];
314
- this.isTouched = false;
803
+ return this.update(() => {
804
+ this._forms.forEach((form) => form.reset());
805
+ this._errors = [];
806
+ this._hasValidationResult = false;
807
+ this.isTouched = false;
808
+ });
315
809
  }
316
810
  // ===== validation =====
317
811
  isRequired(form) {
@@ -324,51 +818,55 @@ var ArrayFormField = class {
324
818
  return isRequired;
325
819
  }
326
820
  checkValid(form) {
327
- this._errors = [];
328
- if (this.isRequired(form) && this.forms.length === 0) {
329
- this._errors.push(
330
- this.options.messages?.isRequired ?? "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u044D\u043B\u0435\u043C\u0435\u043D\u0442"
331
- );
332
- return false;
333
- }
334
- if (this.options.minForms != null && this._forms.length < this.options.minForms) {
335
- this._errors.push(
336
- this.options.messages?.minForms ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${this.options.minForms}`
337
- );
338
- return false;
339
- }
340
- if (this.options.maxForms != null && this._forms.length > this.options.maxForms) {
341
- this._errors.push(
342
- this.options.messages?.maxForms ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${this.options.maxForms}`
343
- );
344
- return false;
345
- }
346
- let valid = true;
347
- for (const form2 of this.forms) {
348
- const ok = form2.checkValid(form2);
349
- if (!ok) valid = false;
350
- }
351
- if (this.options.validate) {
352
- const result = this.options.validate(this.forms, form);
353
- if (result === false) {
821
+ return this.update(() => {
822
+ this._hasValidationResult = true;
823
+ this._errors = [];
824
+ if (this.isRequired(form) && this._forms.length === 0) {
354
825
  this._errors.push(
355
- this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
826
+ this.options.messages?.isRequired ?? "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u044D\u043B\u0435\u043C\u0435\u043D\u0442"
356
827
  );
357
828
  return false;
358
829
  }
359
- if (typeof result === "string") {
360
- this._errors.push(result);
830
+ if (this.options.minForms != null && this._forms.length < this.options.minForms) {
831
+ this._errors.push(
832
+ this.options.messages?.minForms ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${this.options.minForms}`
833
+ );
361
834
  return false;
362
835
  }
363
- }
364
- return valid && this._errors.length === 0;
836
+ if (this.options.maxForms != null && this._forms.length > this.options.maxForms) {
837
+ this._errors.push(
838
+ this.options.messages?.maxForms ?? `\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${this.options.maxForms}`
839
+ );
840
+ return false;
841
+ }
842
+ let valid = true;
843
+ for (const nestedForm of this._forms) {
844
+ const ok = nestedForm.checkValid(nestedForm);
845
+ if (!ok) valid = false;
846
+ }
847
+ if (this.options.validate) {
848
+ const result = this.options.validate(this._forms, form);
849
+ if (result === false) {
850
+ this._errors.push(
851
+ this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
852
+ );
853
+ return false;
854
+ }
855
+ if (typeof result === "string") {
856
+ this._errors.push(result);
857
+ return false;
858
+ }
859
+ }
860
+ return valid && this._errors.length === 0;
861
+ });
365
862
  }
366
863
  checkAndSetValid(form) {
367
864
  return this.checkValid(form);
368
865
  }
369
866
  get isValid() {
867
+ if (!this._hasValidationResult) return false;
370
868
  if (this._errors.length > 0) return false;
371
- return this._forms.every((i) => i.checkValid() === true);
869
+ return this._forms.every((form) => form.isValid);
372
870
  }
373
871
  get errors() {
374
872
  return this._errors;
@@ -377,11 +875,15 @@ var ArrayFormField = class {
377
875
  return this._errors[0] ?? "";
378
876
  }
379
877
  set error(message) {
380
- if (message) {
381
- this._errors = [message];
382
- } else {
383
- this._errors = [];
384
- }
878
+ this.update(() => {
879
+ if (message) {
880
+ this._errors = [message];
881
+ this._hasValidationResult = true;
882
+ } else {
883
+ this._errors = [];
884
+ this._hasValidationResult = false;
885
+ }
886
+ });
385
887
  }
386
888
  };
387
889
 
@@ -393,19 +895,19 @@ var ErrorsSchema = class _ErrorsSchema {
393
895
  this._valueTransformers = [];
394
896
  }
395
897
  /**
396
- * Добавляет статическое правило - точное соответствие ключа ошибки
397
- * @param errorKey - точный ключ ошибки из ответа сервера
398
- * @param fieldName - имя поля в форме или функция для динамического определения
898
+ * Add a static rule that matches an exact error key.
899
+ * @param errorKey - Exact error key returned by the server.
900
+ * @param fieldName - Form field name or a function that resolves it dynamically.
399
901
  */
400
902
  staticKey(errorKey, fieldName) {
401
903
  this._staticRules.set(errorKey, fieldName);
402
904
  return this;
403
905
  }
404
906
  /**
405
- * Добавляет динамическое правило с регулярным выражением
406
- * @param pattern - регулярное выражение для сопоставления ключа ошибки
407
- * @param mapping - имя поля или функция для извлечения имени поля из совпадения
408
- * @param options - дополнительные опции (приоритет и т.д.)
907
+ * Add a dynamic rule using a regular expression.
908
+ * @param pattern - Regular expression used to match an error key.
909
+ * @param mapping - Field name or function that derives it from the match.
910
+ * @param options - Additional options, such as rule priority.
409
911
  */
410
912
  dynamicKey(pattern, mapping, options) {
411
913
  this._dynamicRules.push({
@@ -417,22 +919,22 @@ var ErrorsSchema = class _ErrorsSchema {
417
919
  return this;
418
920
  }
419
921
  /**
420
- * Устанавливает обработчик для ошибок, которые не подошли ни под одно правило
922
+ * Set a handler for errors that do not match any rule.
421
923
  */
422
924
  fallback(handler) {
423
925
  this._fallbackHandler = handler;
424
926
  return this;
425
927
  }
426
928
  /**
427
- * Добавляет трансформер для значений ошибок
428
- * Полезно для нормализации сообщений (например, если сервер возвращает массив ошибок)
929
+ * Add a transformer for error values.
930
+ * Useful for normalizing messages, such as server responses containing arrays.
429
931
  */
430
932
  transformValue(transformer) {
431
933
  this._valueTransformers.push(transformer);
432
934
  return this;
433
935
  }
434
936
  /**
435
- * Быстрый метод для создания схемы из простого объекта маппинга
937
+ * Create a schema from a simple mapping object.
436
938
  */
437
939
  static fromObject(mapping) {
438
940
  const schema = new _ErrorsSchema();
@@ -442,24 +944,35 @@ var ErrorsSchema = class _ErrorsSchema {
442
944
  return schema;
443
945
  }
444
946
  /**
445
- * Создает схему для массива однотипных форм
947
+ * Create a schema for an array of similarly structured forms.
446
948
  */
447
949
  static forArray(itemSchema, arrayFieldName, indexExtractor) {
448
950
  const schema = new _ErrorsSchema();
951
+ for (const transformer of itemSchema._valueTransformers) {
952
+ schema.transformValue(transformer);
953
+ }
954
+ const escapedArrayFieldName = arrayFieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
955
+ const indexPattern = indexExtractor ? "(.+?)" : "(?:\\[(\\d+)\\]|\\.(\\d+))";
449
956
  schema.dynamicKey(
450
- new RegExp(`^${arrayFieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]\\.(.+)$`),
451
- (match) => {
452
- const index = match[1];
453
- const fieldName = match[2];
454
- return `${arrayFieldName}.${index}.${fieldName}`;
957
+ new RegExp(`^${escapedArrayFieldName}${indexPattern}\\.(.+)$`),
958
+ (match, value) => {
959
+ const sourceKey = match[0];
960
+ const index = indexExtractor ? indexExtractor(sourceKey) : Number(match[1] ?? match[2]);
961
+ if (!Number.isInteger(index) || index < 0) {
962
+ return "";
963
+ }
964
+ const fieldPath = match[indexExtractor ? 2 : 3];
965
+ const itemMapping = itemSchema.findMapping(fieldPath, value);
966
+ const mappedFieldPath = itemMapping ? typeof itemMapping === "function" ? itemMapping(value) : itemMapping : fieldPath;
967
+ return `${arrayFieldName}.${index}.${mappedFieldPath}`;
455
968
  },
456
969
  { priority: 100 }
457
970
  );
458
971
  return schema;
459
972
  }
460
973
  /**
461
- * Основной метод парсинга ошибок с поддержкой трансформеров
462
- * Поддерживает вложенные структуры через точечную нотацию
974
+ * Parse errors while applying configured value transformers.
975
+ * Supports nested structures using dot notation.
463
976
  */
464
977
  parseErrors(errors, prefix = "") {
465
978
  const result = {};
@@ -487,17 +1000,12 @@ var ErrorsSchema = class _ErrorsSchema {
487
1000
  } else {
488
1001
  result[mapping] = typeof transformedValue === "string" ? transformedValue : JSON.stringify(transformedValue);
489
1002
  }
490
- } else if (this._fallbackHandler) {
491
- const fieldName = this._fallbackHandler(fullKey, transformedValue);
492
- if (fieldName) {
493
- result[fieldName] = typeof transformedValue === "string" ? transformedValue : JSON.stringify(transformedValue);
494
- }
495
1003
  }
496
1004
  }
497
1005
  return result;
498
1006
  }
499
1007
  /**
500
- * Парсит ошибки сервера и возвращает расширенную информацию
1008
+ * Parse server errors and return detailed metadata.
501
1009
  */
502
1010
  parseErrorsDetailed(errors, prefix = "") {
503
1011
  const result = {};
@@ -529,7 +1037,7 @@ var ErrorsSchema = class _ErrorsSchema {
529
1037
  return result;
530
1038
  }
531
1039
  /**
532
- * Создает схему для вложенной формы
1040
+ * Create a schema for a nested form.
533
1041
  */
534
1042
  nest(prefix) {
535
1043
  const nestedSchema = new _ErrorsSchema();
@@ -554,7 +1062,7 @@ var ErrorsSchema = class _ErrorsSchema {
554
1062
  return nestedSchema;
555
1063
  }
556
1064
  /**
557
- * Клонирует текущую схему
1065
+ * Clone the current schema.
558
1066
  */
559
1067
  clone() {
560
1068
  const cloned = new _ErrorsSchema();
@@ -569,7 +1077,7 @@ var ErrorsSchema = class _ErrorsSchema {
569
1077
  return cloned;
570
1078
  }
571
1079
  /**
572
- * Объединяет текущую схему с другой
1080
+ * Merge the current schema with another schema.
573
1081
  */
574
1082
  merge(other) {
575
1083
  const merged = this.clone();
@@ -596,7 +1104,7 @@ var ErrorsSchema = class _ErrorsSchema {
596
1104
  for (const rule of this._dynamicRules) {
597
1105
  const match = key.match(rule.pattern);
598
1106
  if (match) {
599
- const fieldName = typeof rule.mapping === "function" ? rule.mapping(match) : rule.mapping;
1107
+ const fieldName = typeof rule.mapping === "function" ? rule.mapping(match, value) : rule.mapping;
600
1108
  return fieldName ? { fieldName, matchedBy: "dynamic" } : null;
601
1109
  }
602
1110
  }
@@ -614,7 +1122,7 @@ var ErrorsSchema = class _ErrorsSchema {
614
1122
  const match = key.match(rule.pattern);
615
1123
  if (match) {
616
1124
  if (typeof rule.mapping === "function") {
617
- const fieldName = rule.mapping(match);
1125
+ const fieldName = rule.mapping(match, value);
618
1126
  return fieldName;
619
1127
  }
620
1128
  return rule.mapping;
@@ -630,11 +1138,29 @@ var ErrorsSchema = class _ErrorsSchema {
630
1138
  var DefaultErrorsSchema = new ErrorsSchema().fallback((key) => key);
631
1139
 
632
1140
  // src/core/Form.ts
633
- var Form = class {
1141
+ var Form = class extends ObservableNode {
634
1142
  constructor() {
1143
+ super(...arguments);
635
1144
  this._globalErrors = {};
636
1145
  this.errorsShema = DefaultErrorsSchema;
637
1146
  }
1147
+ captureState() {
1148
+ return {
1149
+ fieldsValue: snapshotValue(this.fieldsValue),
1150
+ allErrors: this.allErrors,
1151
+ globalErrors: this.globalErrors,
1152
+ fields: Object.fromEntries(Object.entries(this._fields).map(([key, field]) => [key, field.getSnapshot()])),
1153
+ isValid: this.isValid,
1154
+ isTouched: this.isTouched,
1155
+ isDirty: this.isDirty
1156
+ };
1157
+ }
1158
+ stateChildren() {
1159
+ return Object.values(this._fields);
1160
+ }
1161
+ batch(callback) {
1162
+ return this.batchUpdate(callback, () => this.checkValid());
1163
+ }
638
1164
  get _fields() {
639
1165
  const fields = {};
640
1166
  for (const key of Object.keys(this)) {
@@ -646,15 +1172,17 @@ var Form = class {
646
1172
  return fields;
647
1173
  }
648
1174
  checkValid(form) {
649
- let isFormValid = true;
650
- const fields = this._fields;
651
- for (const key of Object.keys(fields)) {
652
- const field = fields[key];
653
- if (!field.checkAndSetValid(form ?? this)) {
654
- isFormValid = false;
1175
+ return this.update(() => {
1176
+ let isFormValid = true;
1177
+ const fields = this._fields;
1178
+ for (const key of Object.keys(fields)) {
1179
+ const field = fields[key];
1180
+ if (!field.checkAndSetValid(form ?? this)) {
1181
+ isFormValid = false;
1182
+ }
655
1183
  }
656
- }
657
- return isFormValid;
1184
+ return isFormValid && Object.keys(this._globalErrors).length === 0;
1185
+ });
658
1186
  }
659
1187
  get fieldsValue() {
660
1188
  const data = {};
@@ -671,16 +1199,22 @@ var Form = class {
671
1199
  return data;
672
1200
  }
673
1201
  set fields(data) {
674
- const fields = this._fields;
675
- for (const key of Object.keys(data)) {
676
- const field = fields[key];
677
- if (field) {
678
- if (field instanceof Field) field.rawValue = data[key];
679
- else if (field instanceof ArrayField) field.itemsValue = data[key];
680
- else if (field instanceof ArrayFormField) field.forms = data[key];
681
- else console.error(`Invalid field "${key}" type`);
1202
+ this.setFields(data);
1203
+ }
1204
+ setFields(data, validate = true) {
1205
+ return this.update(() => {
1206
+ const fields = this._fields;
1207
+ for (const key of Object.keys(data)) {
1208
+ const field = fields[key];
1209
+ if (field) {
1210
+ if (field instanceof Field) field.setRawValue(data[key], false);
1211
+ else if (field instanceof ArrayField) field.setItemsValue(data[key], false);
1212
+ else if (field instanceof ArrayFormField) field.setForms(data[key], false);
1213
+ else console.error(`Invalid field "${key}" type`);
1214
+ }
682
1215
  }
683
- }
1216
+ if (validate) this.validateAfterMutation(() => this.checkValid());
1217
+ });
684
1218
  }
685
1219
  get allErrors() {
686
1220
  const errors = {};
@@ -695,7 +1229,7 @@ var Form = class {
695
1229
  } else {
696
1230
  const fieldErrors = field.errors;
697
1231
  if (fieldErrors.length > 0) {
698
- errors[key] = fieldErrors;
1232
+ errors[key] = [...fieldErrors];
699
1233
  }
700
1234
  }
701
1235
  }
@@ -708,7 +1242,7 @@ var Form = class {
708
1242
  return errors;
709
1243
  }
710
1244
  /**
711
- * Рекурсивно собирает все ошибки из ArrayFormField
1245
+ * Recursively collect all errors from an ArrayFormField.
712
1246
  */
713
1247
  collectArrayFormErrors(arrayField) {
714
1248
  const allErrors = [];
@@ -728,19 +1262,34 @@ var Form = class {
728
1262
  return { ...this._globalErrors };
729
1263
  }
730
1264
  set errors(errors) {
731
- const fields = this._fields;
732
- this._globalErrors = {};
733
- for (const key of Object.keys(fields)) {
734
- fields[key].error = null;
735
- }
736
- const parsedErrors = this.errorsShema.parseErrors(errors);
737
- for (const [fieldName, errorMessage] of Object.entries(parsedErrors)) {
738
- this.applyErrorToField(fieldName, errorMessage, fields, this);
739
- }
1265
+ this.update(() => {
1266
+ this.clearErrorsRecursively();
1267
+ const fields = this._fields;
1268
+ const parsedErrors = this.errorsShema.parseErrors(errors);
1269
+ for (const [fieldName, errorMessage] of Object.entries(parsedErrors)) {
1270
+ this.applyErrorToField(fieldName, errorMessage, fields, this);
1271
+ }
1272
+ });
1273
+ }
1274
+ clearErrorsRecursively() {
1275
+ this.update(() => {
1276
+ this._globalErrors = {};
1277
+ for (const field of Object.values(this._fields)) {
1278
+ if (field instanceof Field) {
1279
+ field.error = null;
1280
+ } else if (field instanceof ArrayField) {
1281
+ field.error = null;
1282
+ for (const item of field.items) item.error = null;
1283
+ } else if (field instanceof ArrayFormField) {
1284
+ field.error = null;
1285
+ for (const nestedForm of field.forms) nestedForm.clearErrorsRecursively();
1286
+ }
1287
+ }
1288
+ });
740
1289
  }
741
1290
  /**
742
- * Применяет ошибку к соответствующему полю
743
- * Поддерживает вложенные поля через точечную нотацию
1291
+ * Apply an error to the corresponding field.
1292
+ * Supports nested fields using dot notation.
744
1293
  */
745
1294
  applyErrorToField(fieldPath, errorMessage, fields, targetForm) {
746
1295
  if (!targetForm) {
@@ -788,17 +1337,31 @@ var Form = class {
788
1337
  }
789
1338
  }
790
1339
  setGlobalError(key, message) {
791
- this._globalErrors[key] = message;
1340
+ return this.update(() => {
1341
+ this._globalErrors[key] = message;
1342
+ });
792
1343
  }
793
1344
  clearGlobalErrors() {
794
- this._globalErrors = {};
1345
+ return this.update(() => {
1346
+ this._globalErrors = {};
1347
+ });
795
1348
  }
796
1349
  reset() {
797
- const fields = this._fields;
798
- for (const key of Object.keys(fields)) {
799
- fields[key].reset();
800
- }
801
- this._globalErrors = {};
1350
+ return this.update(() => {
1351
+ const fields = this._fields;
1352
+ for (const key of Object.keys(fields)) {
1353
+ fields[key].reset();
1354
+ }
1355
+ this._globalErrors = {};
1356
+ });
1357
+ }
1358
+ get isDirty() {
1359
+ return Object.values(this._fields).some((field) => field.isDirty);
1360
+ }
1361
+ commitBaseline() {
1362
+ return this.update(() => {
1363
+ for (const field of Object.values(this._fields)) field.commitBaseline();
1364
+ });
802
1365
  }
803
1366
  get isTouched() {
804
1367
  const fields = this._fields;
@@ -807,7 +1370,7 @@ var Form = class {
807
1370
  }
808
1371
  return false;
809
1372
  }
810
- // Проверить, валидна ли форма
1373
+ // Check whether the form is valid.
811
1374
  get isValid() {
812
1375
  const fields = this._fields;
813
1376
  for (const key of Object.keys(fields)) {
@@ -815,32 +1378,50 @@ var Form = class {
815
1378
  }
816
1379
  return Object.keys(this._globalErrors).length === 0;
817
1380
  }
818
- // Получить все значения в виде FormData (для отправки на сервер)
1381
+ // Serialize form values for multipart requests.
819
1382
  toFormData() {
820
1383
  const formData = new FormData();
821
1384
  const data = this.fieldsValue;
822
1385
  for (const [key, value] of Object.entries(data)) {
823
- if (value instanceof Date) {
1386
+ if (value === null || value === void 0) continue;
1387
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
1388
+ formData.append(key, value);
1389
+ } else if (value instanceof Date) {
824
1390
  formData.append(key, value.toISOString());
825
- } else if (value !== null && value !== void 0) {
1391
+ } else if (typeof value === "object") {
1392
+ const serialized = JSON.stringify(value);
1393
+ if (serialized !== void 0) formData.append(key, serialized);
1394
+ } else {
826
1395
  formData.append(key, String(value));
827
1396
  }
828
1397
  }
829
1398
  return formData;
830
1399
  }
831
- // Получить все значения в виде JSON
1400
+ // Get all values in JSON-compatible form.
832
1401
  toJSON() {
833
- const data = this.fieldsValue;
834
1402
  const json = {};
835
- for (const [key, value] of Object.entries(data)) {
836
- if (value instanceof Date) {
837
- json[key] = value.toISOString();
838
- } else {
839
- json[key] = value;
840
- }
1403
+ for (const [key, value] of Object.entries(this.fieldsValue)) {
1404
+ json[key] = this.serializeJsonValue(value);
841
1405
  }
842
1406
  return json;
843
1407
  }
1408
+ serializeJsonValue(value) {
1409
+ if (value instanceof Date) {
1410
+ return value.toISOString();
1411
+ }
1412
+ if (Array.isArray(value)) {
1413
+ return value.map((item) => this.serializeJsonValue(item));
1414
+ }
1415
+ if (value !== null && typeof value === "object") {
1416
+ const prototype = Object.getPrototypeOf(value);
1417
+ if (prototype === Object.prototype || prototype === null) {
1418
+ return Object.fromEntries(
1419
+ Object.entries(value).map(([key, nestedValue]) => [key, this.serializeJsonValue(nestedValue)])
1420
+ );
1421
+ }
1422
+ }
1423
+ return value;
1424
+ }
844
1425
  };
845
1426
 
846
1427
  // src/core/plugins/FieldPlugin.ts
@@ -860,13 +1441,13 @@ var MaskPlugin = class extends BasePlugin {
860
1441
  this.maskParts = [];
861
1442
  this.config = config;
862
1443
  }
863
- init(field) {
1444
+ init(_field) {
864
1445
  this.parseMask();
865
1446
  }
866
- toRawValue(value, field) {
1447
+ toRawValue(value, _field) {
867
1448
  return this.applyMask(value);
868
1449
  }
869
- toValueClear(value, field) {
1450
+ toValueClear(value, _field) {
870
1451
  return this.removeMask(value);
871
1452
  }
872
1453
  parseMask() {
@@ -1065,8 +1646,12 @@ var StringType = class extends FieldType {
1065
1646
  this.options.messages?.maxLength ?? `\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u0434\u043B\u0438\u043D\u0430 ${this.options.maxLength} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432`
1066
1647
  );
1067
1648
  }
1068
- if (this.options.pattern != null && !this.options.pattern.test(value)) {
1069
- errors.push(this.options.messages?.pattern ?? "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
1649
+ if (this.options.pattern != null) {
1650
+ const { source, flags } = this.options.pattern;
1651
+ const stablePattern = new RegExp(source, flags.replace(/[gy]/g, ""));
1652
+ if (!stablePattern.test(value)) {
1653
+ errors.push(this.options.messages?.pattern ?? "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
1654
+ }
1070
1655
  }
1071
1656
  return errors;
1072
1657
  }
@@ -1079,15 +1664,21 @@ var StringType = class extends FieldType {
1079
1664
  var NumberType = class extends FieldType {
1080
1665
  cast(rawValue) {
1081
1666
  if (rawValue === null || rawValue === void 0) return null;
1082
- if (typeof rawValue === "number" && !isNaN(rawValue)) return rawValue;
1667
+ if (typeof rawValue === "number") return Number.isFinite(rawValue) ? rawValue : null;
1083
1668
  if (typeof rawValue === "string") {
1084
- const num = Number(rawValue);
1085
- return isNaN(num) ? null : num;
1669
+ const normalized = rawValue.trim();
1670
+ if (normalized.length === 0) return null;
1671
+ const num = Number(normalized);
1672
+ return Number.isFinite(num) ? num : null;
1086
1673
  }
1087
1674
  return null;
1088
1675
  }
1089
1676
  validate(value) {
1090
1677
  const errors = [];
1678
+ if (!Number.isFinite(value)) {
1679
+ errors.push(this.options.messages?.invalid ?? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C");
1680
+ return errors;
1681
+ }
1091
1682
  if (!Number.isInteger(value)) {
1092
1683
  errors.push(this.options.messages?.invalid ?? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C");
1093
1684
  }
@@ -1132,7 +1723,7 @@ var DecimalType = class extends FieldType {
1132
1723
  if (typeof rawValue === "string") {
1133
1724
  return this.castString(rawValue);
1134
1725
  }
1135
- if (typeof rawValue === "number" && !isNaN(rawValue)) {
1726
+ if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
1136
1727
  return rawValue;
1137
1728
  }
1138
1729
  return null;
@@ -1148,27 +1739,19 @@ var DecimalType = class extends FieldType {
1148
1739
  }
1149
1740
  validate(value) {
1150
1741
  const errors = [];
1151
- value = typeof value === "number" ? this.numberToString(value) : value;
1742
+ value = typeof value === "number" ? String(value) : value;
1152
1743
  if (!this.isValidNumberString(value)) {
1153
1744
  errors.push(this.options.messages?.invalid ?? "\u041F\u0440\u0438\u043C\u0435\u0440: 1.00");
1154
1745
  return errors;
1155
1746
  }
1156
- const maxDigits = this.options.maxDigits || 8;
1157
- const decimalPlaces = this.options.decimalPlaces || 2;
1747
+ const maxDigits = this.options.maxDigits ?? 8;
1748
+ const decimalPlaces = this.options.decimalPlaces ?? 2;
1158
1749
  const maxIntDigits = maxDigits - decimalPlaces;
1159
1750
  const isNegative = value.startsWith("-");
1160
1751
  const normalizedValue = isNegative ? value.substring(1) : value;
1161
1752
  const parts = normalizedValue.split(".");
1162
1753
  const intPart = parts[0];
1163
1754
  const decPart = parts.length > 1 ? parts[1] : "";
1164
- if (normalizedValue.endsWith(".")) {
1165
- if (intPart.length > maxIntDigits) {
1166
- errors.push(
1167
- this.options.messages?.maxDigits ?? `\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0437\u043D\u0430\u043A\u043E\u0432 \u043F\u0435\u0440\u0435\u0434 \u0437\u0430\u043F\u044F\u0442\u043E\u0439 ${maxIntDigits}`
1168
- );
1169
- }
1170
- return errors;
1171
- }
1172
1755
  if (intPart.length === 0) {
1173
1756
  errors.push(this.options.messages?.invalid ?? "\u041F\u0440\u0438\u043C\u0435\u0440: 1.00");
1174
1757
  return errors;
@@ -1228,10 +1811,6 @@ var DecimalType = class extends FieldType {
1228
1811
  if (dotCount > 1) return false;
1229
1812
  return pattern.test(value);
1230
1813
  }
1231
- numberToString(value) {
1232
- const decimalPlaces = this.options.decimalPlaces;
1233
- return value.toFixed(decimalPlaces);
1234
- }
1235
1814
  getTypeName() {
1236
1815
  return "\u0434\u0435\u0441\u044F\u0442\u0438\u0447\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E";
1237
1816
  }
@@ -1308,7 +1887,7 @@ var DateType = class extends FieldType {
1308
1887
  getTypeName() {
1309
1888
  return this.options.isDatetime ? "\u0434\u0430\u0442\u0430 \u0438 \u0432\u0440\u0435\u043C\u044F" : "\u0434\u0430\u0442\u0430";
1310
1889
  }
1311
- // Сравнение дат с учётом времени
1890
+ // Compare dates with optional time precision.
1312
1891
  compareDates(value, boundary, type) {
1313
1892
  if (this.options.isDatetime) {
1314
1893
  if (type === "min") {
@@ -1331,6 +1910,6 @@ var DateType = class extends FieldType {
1331
1910
  }
1332
1911
  };
1333
1912
 
1334
- export { ArrayField, ArrayFormField, BasePlugin, BooleanType, DateType, DecimalType, Field, FieldType, Form, MaskPlugin, NumberType, StringType };
1913
+ export { ArrayField, ArrayFormField, BasePlugin, BooleanType, DateType, DecimalType, DefaultErrorsSchema, ErrorsSchema, Field, FieldType, Form, MaskPlugin, NumberType, StringType };
1335
1914
  //# sourceMappingURL=index.mjs.map
1336
1915
  //# sourceMappingURL=index.mjs.map