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