adaptform 2.0.4 → 2.1.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
@@ -8,7 +8,7 @@ var Field = class {
8
8
  this._errors = [];
9
9
  this.isValid = null;
10
10
  this.isTouched = false;
11
- this.filed_type = new TypeConstructor(options ?? {});
11
+ this.fieldType = new TypeConstructor(options ?? {});
12
12
  this.defaultValue = defaultValue ?? null;
13
13
  for (const plugin of this.options.plugins ?? []) {
14
14
  plugin.init?.(this);
@@ -20,24 +20,38 @@ var Field = class {
20
20
  this._valueClear = null;
21
21
  this.isValid = null;
22
22
  }
23
+ this.isTouched = false;
23
24
  }
24
- // ========== Геттеры/сеттеры ==========
25
+ // ========== Getters and setters ==========
25
26
  get plugins() {
26
27
  return [...this.options.plugins ?? []].sort((a, b) => {
27
28
  return (a.pluginOptions?.priority ?? 0) - (b.pluginOptions?.priority ?? 0);
28
29
  });
29
30
  }
30
31
  get options() {
31
- return this.filed_type.options;
32
+ return this.fieldType.options;
32
33
  }
33
34
  set options(options) {
34
- let cur_options = this.filed_type.options;
35
- this.filed_type.options = Object.assign(cur_options, options);
35
+ const currentOptions = this.fieldType.options;
36
+ this.fieldType.options = Object.assign(currentOptions, options);
36
37
  }
37
38
  get rawValue() {
38
39
  return this._rawValue;
39
40
  }
40
41
  set rawValue(input) {
42
+ this.setRawValue(input);
43
+ }
44
+ setRawValue(input, validate = true) {
45
+ this.assignRawValue(input);
46
+ this._errors = [];
47
+ this.isTouched = true;
48
+ if (validate) {
49
+ this.checkAndSetValid();
50
+ } else {
51
+ this.isValid = null;
52
+ }
53
+ }
54
+ assignRawValue(input) {
41
55
  let rawValue = input;
42
56
  for (const plugin of this.plugins) {
43
57
  if (plugin.toRawValue) {
@@ -51,10 +65,7 @@ var Field = class {
51
65
  }
52
66
  }
53
67
  this._rawValue = rawValue;
54
- this._valueClear = this.filed_type.cast(valueClear);
55
- this._errors = [];
56
- this.isTouched = true;
57
- this.checkAndSetValid();
68
+ this._valueClear = this.fieldType.cast(valueClear);
58
69
  }
59
70
  get errors() {
60
71
  return this._errors;
@@ -73,7 +84,7 @@ var Field = class {
73
84
  get valueClear() {
74
85
  return this._valueClear;
75
86
  }
76
- // ========== Валидация ==========
87
+ // ========== Validation ==========
77
88
  isRequired(form) {
78
89
  let isRequired;
79
90
  if (typeof this.options.isRequired === "function") {
@@ -94,11 +105,11 @@ var Field = class {
94
105
  }
95
106
  if (this._valueClear === null) {
96
107
  this._errors.push(
97
- this.options.messages?.type ?? `\u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ${this.filed_type.getTypeName()}`
108
+ this.options.messages?.type ?? `\u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ${this.fieldType.getTypeName()}`
98
109
  );
99
110
  return false;
100
111
  }
101
- const typeErrors = this.filed_type.validate(this._valueClear, form);
112
+ const typeErrors = this.fieldType.validate(this._valueClear, form);
102
113
  if (typeErrors.length > 0) {
103
114
  this._errors.push(...typeErrors);
104
115
  return false;
@@ -123,55 +134,61 @@ var Field = class {
123
134
  get isEmpty() {
124
135
  let isEmpty = false;
125
136
  if (this._rawValue === null || this._rawValue === void 0) isEmpty = true;
126
- else if (this._rawValue === "") isEmpty = true;
137
+ else if (typeof this._rawValue === "string" && this._rawValue.trim() === "") isEmpty = true;
127
138
  else if (Array.isArray(this._rawValue) && this._rawValue.length === 0) isEmpty = true;
128
139
  return isEmpty;
129
140
  }
130
- // ========== Вспомогательные методы ==========
141
+ // ========== Helper methods ==========
131
142
  reset() {
132
143
  if (this.defaultValue !== null && this.defaultValue !== void 0) {
133
- this.rawValue = this.defaultValue;
134
- this.isValid = true;
144
+ this.assignRawValue(this.defaultValue);
135
145
  } else {
136
146
  this._rawValue = null;
137
147
  this._valueClear = null;
138
- this.isValid = null;
139
148
  }
140
149
  this._errors = [];
150
+ this.isValid = null;
141
151
  this.isTouched = false;
142
152
  }
143
153
  };
144
154
 
155
+ // src/core/fields/arrayUtils.ts
156
+ function isValidArrayIndex(index, length) {
157
+ return Number.isInteger(index) && index >= 0 && index < length;
158
+ }
159
+
145
160
  // src/core/fields/ArrayField.ts
146
161
  var ArrayField = class {
147
162
  constructor(factory, options = {}, initial = []) {
148
163
  this.factory = factory;
149
164
  this._items = [];
150
165
  this._errors = [];
166
+ this._hasValidationResult = false;
151
167
  this.isTouched = false;
152
168
  this.options = {
153
169
  isRequired: true,
154
170
  messages: {},
155
171
  ...options
156
172
  };
157
- this._items = initial.map((v) => this.factory(v));
173
+ this._items = initial.map((value) => this.factory(value));
174
+ this.checkValid();
158
175
  }
159
176
  // ===== value =====
160
177
  set itemsValue(items) {
161
178
  this.clear();
162
- for (const i of items) {
163
- this.add(i, false);
179
+ for (const item of items) {
180
+ this.add(item, false);
164
181
  }
165
182
  this.checkValid();
166
183
  }
167
184
  get itemsValue() {
168
- return this._items.map((i) => i.valueClear);
185
+ return this._items.map((item) => item.valueClear);
169
186
  }
170
187
  get items() {
171
188
  return this._items;
172
189
  }
173
190
  // ===== actions =====
174
- add(value, force_check) {
191
+ add(value, force_check = true) {
175
192
  const field = this.factory(value);
176
193
  this.isTouched = true;
177
194
  this._items.push(field);
@@ -179,6 +196,7 @@ var ArrayField = class {
179
196
  return field;
180
197
  }
181
198
  remove(index) {
199
+ if (!isValidArrayIndex(index, this._items.length)) return;
182
200
  this._items.splice(index, 1);
183
201
  this.checkValid();
184
202
  }
@@ -197,6 +215,7 @@ var ArrayField = class {
197
215
  return isRequired;
198
216
  }
199
217
  checkValid(form) {
218
+ this._hasValidationResult = true;
200
219
  this._errors = [];
201
220
  if (this.isRequired(form) && this._items.length === 0) {
202
221
  this._errors.push(
@@ -222,7 +241,8 @@ var ArrayField = class {
222
241
  if (!ok) valid = false;
223
242
  }
224
243
  if (this.options.validate) {
225
- const result = this.options.validate(this._items, form);
244
+ const values = this._items.map((item) => item.valueClear);
245
+ const result = this.options.validate(values, form);
226
246
  if (result === false) {
227
247
  this._errors.push(
228
248
  this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
@@ -240,7 +260,9 @@ var ArrayField = class {
240
260
  return this.checkValid(form);
241
261
  }
242
262
  get isValid() {
243
- return this._errors.length === 0 && this._items.every((i) => i.isValid === true);
263
+ if (!this._hasValidationResult) return false;
264
+ if (this._errors.length > 0) return false;
265
+ return this._items.every((item) => item.isValid === true);
244
266
  }
245
267
  get error() {
246
268
  return this._errors[0] ?? "";
@@ -248,16 +270,19 @@ var ArrayField = class {
248
270
  set error(message) {
249
271
  if (message) {
250
272
  this._errors = [message];
273
+ this._hasValidationResult = true;
251
274
  } else {
252
275
  this._errors = [];
276
+ this._hasValidationResult = false;
253
277
  }
254
278
  }
255
279
  get errors() {
256
280
  return this._errors;
257
281
  }
258
282
  reset() {
259
- this._items.forEach((i) => i.reset());
283
+ this._items.forEach((item) => item.reset());
260
284
  this._errors = [];
285
+ this._hasValidationResult = false;
261
286
  this.isTouched = false;
262
287
  }
263
288
  };
@@ -269,37 +294,45 @@ var ArrayFormField = class {
269
294
  this.options = options;
270
295
  this._forms = [];
271
296
  this._errors = [];
297
+ this._hasValidationResult = false;
272
298
  this.isTouched = false;
273
299
  this._forms = initial.map((data) => {
274
300
  const form = new this.FormConstructor();
275
- form.fields = data;
301
+ form.setFields(data, false);
276
302
  return form;
277
303
  });
304
+ this.checkValid();
278
305
  }
279
306
  // ===== value =====
280
307
  set forms(forms) {
281
- this.clear();
282
- for (const f of forms) {
283
- this.add(f, false);
308
+ this.setForms(forms);
309
+ }
310
+ setForms(forms, validate = true) {
311
+ this._forms = [];
312
+ for (const formData of forms) {
313
+ this.add(formData, false);
284
314
  }
285
- this.checkValid();
315
+ if (validate) this.checkValid();
286
316
  }
287
317
  get forms() {
288
318
  return this._forms;
289
319
  }
290
320
  get formsFieldsValue() {
291
- return this._forms.map((i) => i.fieldsValue);
321
+ return this._forms.map((form) => form.fieldsValue);
292
322
  }
293
323
  // ===== actions =====
294
- add(data, force_check) {
324
+ add(data, force_check = true) {
295
325
  const form = new this.FormConstructor();
296
- if (data) form.fields = data;
326
+ if (data !== void 0 && data !== null) {
327
+ form.setFields(data, false);
328
+ }
297
329
  this.isTouched = true;
298
330
  this._forms.push(form);
299
331
  if (force_check) this.checkValid();
300
332
  return form;
301
333
  }
302
334
  remove(index) {
335
+ if (!isValidArrayIndex(index, this._forms.length)) return;
303
336
  this._forms.splice(index, 1);
304
337
  this.checkValid();
305
338
  }
@@ -308,8 +341,9 @@ var ArrayFormField = class {
308
341
  this.checkValid();
309
342
  }
310
343
  reset() {
311
- this._forms.forEach((f) => f.reset());
344
+ this._forms.forEach((form) => form.reset());
312
345
  this._errors = [];
346
+ this._hasValidationResult = false;
313
347
  this.isTouched = false;
314
348
  }
315
349
  // ===== validation =====
@@ -323,8 +357,9 @@ var ArrayFormField = class {
323
357
  return isRequired;
324
358
  }
325
359
  checkValid(form) {
360
+ this._hasValidationResult = true;
326
361
  this._errors = [];
327
- if (this.isRequired(form) && this.forms.length === 0) {
362
+ if (this.isRequired(form) && this._forms.length === 0) {
328
363
  this._errors.push(
329
364
  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"
330
365
  );
@@ -343,12 +378,12 @@ var ArrayFormField = class {
343
378
  return false;
344
379
  }
345
380
  let valid = true;
346
- for (const form2 of this.forms) {
347
- const ok = form2.checkValid(form2);
381
+ for (const nestedForm of this._forms) {
382
+ const ok = nestedForm.checkValid(nestedForm);
348
383
  if (!ok) valid = false;
349
384
  }
350
385
  if (this.options.validate) {
351
- const result = this.options.validate(this.forms, form);
386
+ const result = this.options.validate(this._forms, form);
352
387
  if (result === false) {
353
388
  this._errors.push(
354
389
  this.options.messages?.validate ?? "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438"
@@ -366,7 +401,9 @@ var ArrayFormField = class {
366
401
  return this.checkValid(form);
367
402
  }
368
403
  get isValid() {
369
- return this._errors.length === 0 && this._forms.every((i) => i.checkValid() === true);
404
+ if (!this._hasValidationResult) return false;
405
+ if (this._errors.length > 0) return false;
406
+ return this._forms.every((form) => form.isValid);
370
407
  }
371
408
  get errors() {
372
409
  return this._errors;
@@ -377,16 +414,269 @@ var ArrayFormField = class {
377
414
  set error(message) {
378
415
  if (message) {
379
416
  this._errors = [message];
417
+ this._hasValidationResult = true;
380
418
  } else {
381
419
  this._errors = [];
420
+ this._hasValidationResult = false;
382
421
  }
383
422
  }
384
423
  };
385
424
 
425
+ // src/core/ErrorsShema.ts
426
+ var ErrorsSchema = class _ErrorsSchema {
427
+ constructor() {
428
+ this._staticRules = /* @__PURE__ */ new Map();
429
+ this._dynamicRules = [];
430
+ this._valueTransformers = [];
431
+ }
432
+ /**
433
+ * Add a static rule that matches an exact error key.
434
+ * @param errorKey - Exact error key returned by the server.
435
+ * @param fieldName - Form field name or a function that resolves it dynamically.
436
+ */
437
+ staticKey(errorKey, fieldName) {
438
+ this._staticRules.set(errorKey, fieldName);
439
+ return this;
440
+ }
441
+ /**
442
+ * Add a dynamic rule using a regular expression.
443
+ * @param pattern - Regular expression used to match an error key.
444
+ * @param mapping - Field name or function that derives it from the match.
445
+ * @param options - Additional options, such as rule priority.
446
+ */
447
+ dynamicKey(pattern, mapping, options) {
448
+ this._dynamicRules.push({
449
+ pattern,
450
+ mapping,
451
+ priority: options?.priority ?? 0
452
+ });
453
+ this._dynamicRules.sort((a, b) => b.priority - a.priority);
454
+ return this;
455
+ }
456
+ /**
457
+ * Set a handler for errors that do not match any rule.
458
+ */
459
+ fallback(handler) {
460
+ this._fallbackHandler = handler;
461
+ return this;
462
+ }
463
+ /**
464
+ * Add a transformer for error values.
465
+ * Useful for normalizing messages, such as server responses containing arrays.
466
+ */
467
+ transformValue(transformer) {
468
+ this._valueTransformers.push(transformer);
469
+ return this;
470
+ }
471
+ /**
472
+ * Create a schema from a simple mapping object.
473
+ */
474
+ static fromObject(mapping) {
475
+ const schema = new _ErrorsSchema();
476
+ for (const [errorKey, fieldName] of Object.entries(mapping)) {
477
+ schema.staticKey(errorKey, fieldName);
478
+ }
479
+ return schema;
480
+ }
481
+ /**
482
+ * Create a schema for an array of similarly structured forms.
483
+ */
484
+ static forArray(itemSchema, arrayFieldName, indexExtractor) {
485
+ const schema = new _ErrorsSchema();
486
+ for (const transformer of itemSchema._valueTransformers) {
487
+ schema.transformValue(transformer);
488
+ }
489
+ const escapedArrayFieldName = arrayFieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
490
+ const indexPattern = indexExtractor ? "(.+?)" : "(?:\\[(\\d+)\\]|\\.(\\d+))";
491
+ schema.dynamicKey(
492
+ new RegExp(`^${escapedArrayFieldName}${indexPattern}\\.(.+)$`),
493
+ (match, value) => {
494
+ const sourceKey = match[0];
495
+ const index = indexExtractor ? indexExtractor(sourceKey) : Number(match[1] ?? match[2]);
496
+ if (!Number.isInteger(index) || index < 0) {
497
+ return "";
498
+ }
499
+ const fieldPath = match[indexExtractor ? 2 : 3];
500
+ const itemMapping = itemSchema.findMapping(fieldPath, value);
501
+ const mappedFieldPath = itemMapping ? typeof itemMapping === "function" ? itemMapping(value) : itemMapping : fieldPath;
502
+ return `${arrayFieldName}.${index}.${mappedFieldPath}`;
503
+ },
504
+ { priority: 100 }
505
+ );
506
+ return schema;
507
+ }
508
+ /**
509
+ * Parse errors while applying configured value transformers.
510
+ * Supports nested structures using dot notation.
511
+ */
512
+ parseErrors(errors, prefix = "") {
513
+ const result = {};
514
+ for (const [key, value] of Object.entries(errors)) {
515
+ const fullKey = prefix ? `${prefix}.${key}` : key;
516
+ if (this.isNestedObject(value)) {
517
+ const nestedErrors = this.parseErrors(value, fullKey);
518
+ Object.assign(result, nestedErrors);
519
+ continue;
520
+ }
521
+ if (value === null || value === void 0) {
522
+ continue;
523
+ }
524
+ let transformedValue = value;
525
+ for (const transformer of this._valueTransformers) {
526
+ transformedValue = transformer(transformedValue);
527
+ }
528
+ const mapping = this.findMapping(fullKey, transformedValue);
529
+ if (mapping) {
530
+ if (typeof mapping === "function") {
531
+ const fieldName = mapping(transformedValue);
532
+ if (fieldName) {
533
+ result[fieldName] = typeof transformedValue === "string" ? transformedValue : JSON.stringify(transformedValue);
534
+ }
535
+ } else {
536
+ result[mapping] = typeof transformedValue === "string" ? transformedValue : JSON.stringify(transformedValue);
537
+ }
538
+ }
539
+ }
540
+ return result;
541
+ }
542
+ /**
543
+ * Parse server errors and return detailed metadata.
544
+ */
545
+ parseErrorsDetailed(errors, prefix = "") {
546
+ const result = {};
547
+ for (const [key, value] of Object.entries(errors)) {
548
+ const fullKey = prefix ? `${prefix}.${key}` : key;
549
+ if (this.isNestedObject(value)) {
550
+ const nestedErrors = this.parseErrorsDetailed(value, fullKey);
551
+ Object.assign(result, nestedErrors);
552
+ continue;
553
+ }
554
+ if (value === null || value === void 0) {
555
+ continue;
556
+ }
557
+ let transformedValue = value;
558
+ for (const transformer of this._valueTransformers) {
559
+ transformedValue = transformer(transformedValue);
560
+ }
561
+ const matchResult = this.findMappingDetailed(fullKey, transformedValue);
562
+ if (matchResult) {
563
+ const message = typeof transformedValue === "string" ? transformedValue : JSON.stringify(transformedValue);
564
+ result[matchResult.fieldName] = {
565
+ fieldName: matchResult.fieldName,
566
+ message,
567
+ sourceKey: fullKey,
568
+ matchedBy: matchResult.matchedBy
569
+ };
570
+ }
571
+ }
572
+ return result;
573
+ }
574
+ /**
575
+ * Create a schema for a nested form.
576
+ */
577
+ nest(prefix) {
578
+ const nestedSchema = new _ErrorsSchema();
579
+ for (const [key, mapping] of this._staticRules.entries()) {
580
+ nestedSchema.staticKey(`${prefix}.${key}`, mapping);
581
+ }
582
+ for (const rule of this._dynamicRules) {
583
+ const newPattern = new RegExp(`^${prefix}\\.${rule.pattern.source.replace(/^\^/, "")}`);
584
+ nestedSchema.dynamicKey(newPattern, rule.mapping, { priority: rule.priority });
585
+ }
586
+ for (const transformer of this._valueTransformers) {
587
+ nestedSchema.transformValue(transformer);
588
+ }
589
+ if (this._fallbackHandler) {
590
+ nestedSchema.fallback((key, value) => {
591
+ if (key.startsWith(`${prefix}.`)) {
592
+ return this._fallbackHandler(key.slice(prefix.length + 1), value);
593
+ }
594
+ return null;
595
+ });
596
+ }
597
+ return nestedSchema;
598
+ }
599
+ /**
600
+ * Clone the current schema.
601
+ */
602
+ clone() {
603
+ const cloned = new _ErrorsSchema();
604
+ for (const [key, mapping] of this._staticRules.entries()) {
605
+ cloned._staticRules.set(key, mapping);
606
+ }
607
+ for (const rule of this._dynamicRules) {
608
+ cloned._dynamicRules.push({ ...rule });
609
+ }
610
+ cloned._fallbackHandler = this._fallbackHandler;
611
+ cloned._valueTransformers = [...this._valueTransformers];
612
+ return cloned;
613
+ }
614
+ /**
615
+ * Merge the current schema with another schema.
616
+ */
617
+ merge(other) {
618
+ const merged = this.clone();
619
+ for (const [key, mapping] of other._staticRules.entries()) {
620
+ merged._staticRules.set(key, mapping);
621
+ }
622
+ merged._dynamicRules.push(...other._dynamicRules);
623
+ merged._dynamicRules.sort((a, b) => b.priority - a.priority);
624
+ merged._valueTransformers.push(...other._valueTransformers);
625
+ if (other._fallbackHandler) {
626
+ merged._fallbackHandler = other._fallbackHandler;
627
+ }
628
+ return merged;
629
+ }
630
+ isNestedObject(value) {
631
+ return value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
632
+ }
633
+ findMappingDetailed(key, value) {
634
+ if (this._staticRules.has(key)) {
635
+ const mapping = this._staticRules.get(key);
636
+ const fieldName = typeof mapping === "function" ? mapping(value) : mapping;
637
+ return fieldName ? { fieldName, matchedBy: "static" } : null;
638
+ }
639
+ for (const rule of this._dynamicRules) {
640
+ const match = key.match(rule.pattern);
641
+ if (match) {
642
+ const fieldName = typeof rule.mapping === "function" ? rule.mapping(match, value) : rule.mapping;
643
+ return fieldName ? { fieldName, matchedBy: "dynamic" } : null;
644
+ }
645
+ }
646
+ if (this._fallbackHandler) {
647
+ const fieldName = this._fallbackHandler(key, value);
648
+ return fieldName ? { fieldName, matchedBy: "fallback" } : null;
649
+ }
650
+ return null;
651
+ }
652
+ findMapping(key, value) {
653
+ if (this._staticRules.has(key)) {
654
+ return this._staticRules.get(key);
655
+ }
656
+ for (const rule of this._dynamicRules) {
657
+ const match = key.match(rule.pattern);
658
+ if (match) {
659
+ if (typeof rule.mapping === "function") {
660
+ const fieldName = rule.mapping(match, value);
661
+ return fieldName;
662
+ }
663
+ return rule.mapping;
664
+ }
665
+ }
666
+ if (this._fallbackHandler) {
667
+ const fieldName = this._fallbackHandler(key, value);
668
+ return fieldName;
669
+ }
670
+ return null;
671
+ }
672
+ };
673
+ var DefaultErrorsSchema = new ErrorsSchema().fallback((key) => key);
674
+
386
675
  // src/core/Form.ts
387
676
  var Form = class {
388
677
  constructor() {
389
678
  this._globalErrors = {};
679
+ this.errorsShema = DefaultErrorsSchema;
390
680
  }
391
681
  get _fields() {
392
682
  const fields = {};
@@ -407,7 +697,7 @@ var Form = class {
407
697
  isFormValid = false;
408
698
  }
409
699
  }
410
- return isFormValid;
700
+ return isFormValid && Object.keys(this._globalErrors).length === 0;
411
701
  }
412
702
  get fieldsValue() {
413
703
  const data = {};
@@ -424,24 +714,36 @@ var Form = class {
424
714
  return data;
425
715
  }
426
716
  set fields(data) {
717
+ this.setFields(data);
718
+ }
719
+ setFields(data, validate = true) {
427
720
  const fields = this._fields;
428
721
  for (const key of Object.keys(data)) {
429
722
  const field = fields[key];
430
723
  if (field) {
431
- if (field instanceof Field) field.rawValue = data[key];
724
+ if (field instanceof Field) field.setRawValue(data[key], false);
432
725
  else if (field instanceof ArrayField) field.itemsValue = data[key];
433
- else if (field instanceof ArrayFormField) field.forms = data[key];
726
+ else if (field instanceof ArrayFormField) field.setForms(data[key], false);
434
727
  else console.error(`Invalid field "${key}" type`);
435
728
  }
436
729
  }
730
+ if (validate) this.checkValid();
437
731
  }
438
732
  get allErrors() {
439
733
  const errors = {};
440
734
  const fields = this._fields;
441
735
  for (const key of Object.keys(fields)) {
442
- const fieldErrors = fields[key].errors;
443
- if (fieldErrors.length > 0) {
444
- errors[key] = fieldErrors;
736
+ const field = fields[key];
737
+ if (field instanceof ArrayFormField) {
738
+ const formErrors = this.collectArrayFormErrors(field);
739
+ if (formErrors.length > 0) {
740
+ errors[key] = formErrors;
741
+ }
742
+ } else {
743
+ const fieldErrors = field.errors;
744
+ if (fieldErrors.length > 0) {
745
+ errors[key] = fieldErrors;
746
+ }
445
747
  }
446
748
  }
447
749
  for (const key of Object.keys(this._globalErrors)) {
@@ -452,20 +754,95 @@ var Form = class {
452
754
  }
453
755
  return errors;
454
756
  }
757
+ /**
758
+ * Recursively collect all errors from an ArrayFormField.
759
+ */
760
+ collectArrayFormErrors(arrayField) {
761
+ const allErrors = [];
762
+ allErrors.push(...arrayField.errors);
763
+ for (let i = 0; i < arrayField.forms.length; i++) {
764
+ const form = arrayField.forms[i];
765
+ const formAllErrors = form.allErrors;
766
+ for (const [fieldName, fieldErrors] of Object.entries(formAllErrors)) {
767
+ for (const error of fieldErrors) {
768
+ allErrors.push(`\u0424\u043E\u0440\u043C\u0430 ${i + 1}, ${fieldName}: ${error}`);
769
+ }
770
+ }
771
+ }
772
+ return allErrors;
773
+ }
455
774
  get globalErrors() {
456
775
  return { ...this._globalErrors };
457
776
  }
458
777
  set errors(errors) {
778
+ this.clearErrorsRecursively();
459
779
  const fields = this._fields;
780
+ const parsedErrors = this.errorsShema.parseErrors(errors);
781
+ for (const [fieldName, errorMessage] of Object.entries(parsedErrors)) {
782
+ this.applyErrorToField(fieldName, errorMessage, fields, this);
783
+ }
784
+ }
785
+ clearErrorsRecursively() {
460
786
  this._globalErrors = {};
461
- for (const key of Object.keys(errors)) {
462
- const keys = key.split(".");
463
- const cleanKey = keys.length > 1 ? keys[1] : key;
464
- if (cleanKey in fields && fields[cleanKey]) {
465
- fields[cleanKey].error = errors[key];
787
+ for (const field of Object.values(this._fields)) {
788
+ if (field instanceof Field) {
789
+ field.error = null;
790
+ } else if (field instanceof ArrayField) {
791
+ field.error = null;
792
+ for (const item of field.items) item.error = null;
793
+ } else if (field instanceof ArrayFormField) {
794
+ field.error = null;
795
+ for (const nestedForm of field.forms) nestedForm.clearErrorsRecursively();
796
+ }
797
+ }
798
+ }
799
+ /**
800
+ * Apply an error to the corresponding field.
801
+ * Supports nested fields using dot notation.
802
+ */
803
+ applyErrorToField(fieldPath, errorMessage, fields, targetForm) {
804
+ if (!targetForm) {
805
+ console.error("applyErrorToField: targetForm is undefined");
806
+ return;
807
+ }
808
+ if (fieldPath in fields && fields[fieldPath]) {
809
+ fields[fieldPath].error = errorMessage;
810
+ return;
811
+ }
812
+ const parts = fieldPath.split(".");
813
+ const rootField = parts[0];
814
+ if (rootField in fields && fields[rootField]) {
815
+ const field = fields[rootField];
816
+ if (parts.length === 1) {
817
+ field.error = errorMessage;
818
+ } else if (parts.length > 1) {
819
+ this.applyNestedError(field, parts.slice(1), errorMessage, fieldPath, targetForm);
820
+ }
821
+ } else {
822
+ targetForm.setGlobalError(fieldPath, errorMessage);
823
+ }
824
+ }
825
+ applyNestedError(field, pathParts, errorMessage, fullPath, targetForm) {
826
+ if (!targetForm) {
827
+ console.error("applyNestedError: targetForm is undefined");
828
+ return;
829
+ }
830
+ const [currentPart, ...remainingParts] = pathParts;
831
+ if (field instanceof ArrayFormField) {
832
+ const index = parseInt(currentPart);
833
+ if (!isNaN(index) && index >= 0 && index < field.forms.length) {
834
+ const form = field.forms[index];
835
+ if (remainingParts.length > 0) {
836
+ const formFields = form["_fields"];
837
+ this.applyErrorToField(remainingParts.join("."), errorMessage, formFields, form);
838
+ } else {
839
+ form.setGlobalError(`index_${index}`, errorMessage);
840
+ }
466
841
  } else {
467
- this._globalErrors[key] = errors[key];
842
+ targetForm.setGlobalError(fullPath, errorMessage);
468
843
  }
844
+ } else {
845
+ targetForm.setGlobalError(fullPath, errorMessage);
469
846
  }
470
847
  }
471
848
  setGlobalError(key, message) {
@@ -488,7 +865,7 @@ var Form = class {
488
865
  }
489
866
  return false;
490
867
  }
491
- // Проверить, валидна ли форма
868
+ // Check whether the form is valid.
492
869
  get isValid() {
493
870
  const fields = this._fields;
494
871
  for (const key of Object.keys(fields)) {
@@ -496,32 +873,50 @@ var Form = class {
496
873
  }
497
874
  return Object.keys(this._globalErrors).length === 0;
498
875
  }
499
- // Получить все значения в виде FormData (для отправки на сервер)
876
+ // Serialize form values for multipart requests.
500
877
  toFormData() {
501
878
  const formData = new FormData();
502
879
  const data = this.fieldsValue;
503
880
  for (const [key, value] of Object.entries(data)) {
504
- if (value instanceof Date) {
881
+ if (value === null || value === void 0) continue;
882
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
883
+ formData.append(key, value);
884
+ } else if (value instanceof Date) {
505
885
  formData.append(key, value.toISOString());
506
- } else if (value !== null && value !== void 0) {
886
+ } else if (typeof value === "object") {
887
+ const serialized = JSON.stringify(value);
888
+ if (serialized !== void 0) formData.append(key, serialized);
889
+ } else {
507
890
  formData.append(key, String(value));
508
891
  }
509
892
  }
510
893
  return formData;
511
894
  }
512
- // Получить все значения в виде JSON
895
+ // Get all values in JSON-compatible form.
513
896
  toJSON() {
514
- const data = this.fieldsValue;
515
897
  const json = {};
516
- for (const [key, value] of Object.entries(data)) {
517
- if (value instanceof Date) {
518
- json[key] = value.toISOString();
519
- } else {
520
- json[key] = value;
521
- }
898
+ for (const [key, value] of Object.entries(this.fieldsValue)) {
899
+ json[key] = this.serializeJsonValue(value);
522
900
  }
523
901
  return json;
524
902
  }
903
+ serializeJsonValue(value) {
904
+ if (value instanceof Date) {
905
+ return value.toISOString();
906
+ }
907
+ if (Array.isArray(value)) {
908
+ return value.map((item) => this.serializeJsonValue(item));
909
+ }
910
+ if (value !== null && typeof value === "object") {
911
+ const prototype = Object.getPrototypeOf(value);
912
+ if (prototype === Object.prototype || prototype === null) {
913
+ return Object.fromEntries(
914
+ Object.entries(value).map(([key, nestedValue]) => [key, this.serializeJsonValue(nestedValue)])
915
+ );
916
+ }
917
+ }
918
+ return value;
919
+ }
525
920
  };
526
921
 
527
922
  // src/core/plugins/FieldPlugin.ts
@@ -541,13 +936,13 @@ var MaskPlugin = class extends BasePlugin {
541
936
  this.maskParts = [];
542
937
  this.config = config;
543
938
  }
544
- init(field) {
939
+ init(_field) {
545
940
  this.parseMask();
546
941
  }
547
- toRawValue(value, field) {
942
+ toRawValue(value, _field) {
548
943
  return this.applyMask(value);
549
944
  }
550
- toValueClear(value, field) {
945
+ toValueClear(value, _field) {
551
946
  return this.removeMask(value);
552
947
  }
553
948
  parseMask() {
@@ -746,8 +1141,12 @@ var StringType = class extends FieldType {
746
1141
  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`
747
1142
  );
748
1143
  }
749
- if (this.options.pattern != null && !this.options.pattern.test(value)) {
750
- errors.push(this.options.messages?.pattern ?? "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
1144
+ if (this.options.pattern != null) {
1145
+ const { source, flags } = this.options.pattern;
1146
+ const stablePattern = new RegExp(source, flags.replace(/[gy]/g, ""));
1147
+ if (!stablePattern.test(value)) {
1148
+ errors.push(this.options.messages?.pattern ?? "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
1149
+ }
751
1150
  }
752
1151
  return errors;
753
1152
  }
@@ -760,15 +1159,21 @@ var StringType = class extends FieldType {
760
1159
  var NumberType = class extends FieldType {
761
1160
  cast(rawValue) {
762
1161
  if (rawValue === null || rawValue === void 0) return null;
763
- if (typeof rawValue === "number" && !isNaN(rawValue)) return rawValue;
1162
+ if (typeof rawValue === "number") return Number.isFinite(rawValue) ? rawValue : null;
764
1163
  if (typeof rawValue === "string") {
765
- const num = Number(rawValue);
766
- return isNaN(num) ? null : num;
1164
+ const normalized = rawValue.trim();
1165
+ if (normalized.length === 0) return null;
1166
+ const num = Number(normalized);
1167
+ return Number.isFinite(num) ? num : null;
767
1168
  }
768
1169
  return null;
769
1170
  }
770
1171
  validate(value) {
771
1172
  const errors = [];
1173
+ if (!Number.isFinite(value)) {
1174
+ 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");
1175
+ return errors;
1176
+ }
772
1177
  if (!Number.isInteger(value)) {
773
1178
  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");
774
1179
  }
@@ -813,7 +1218,7 @@ var DecimalType = class extends FieldType {
813
1218
  if (typeof rawValue === "string") {
814
1219
  return this.castString(rawValue);
815
1220
  }
816
- if (typeof rawValue === "number" && !isNaN(rawValue)) {
1221
+ if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
817
1222
  return rawValue;
818
1223
  }
819
1224
  return null;
@@ -829,13 +1234,13 @@ var DecimalType = class extends FieldType {
829
1234
  }
830
1235
  validate(value) {
831
1236
  const errors = [];
832
- value = typeof value === "number" ? this.numberToString(value) : value;
1237
+ value = typeof value === "number" ? String(value) : value;
833
1238
  if (!this.isValidNumberString(value)) {
834
1239
  errors.push(this.options.messages?.invalid ?? "\u041F\u0440\u0438\u043C\u0435\u0440: 1.00");
835
1240
  return errors;
836
1241
  }
837
- const maxDigits = this.options.maxDigits || 8;
838
- const decimalPlaces = this.options.decimalPlaces || 2;
1242
+ const maxDigits = this.options.maxDigits ?? 8;
1243
+ const decimalPlaces = this.options.decimalPlaces ?? 2;
839
1244
  const maxIntDigits = maxDigits - decimalPlaces;
840
1245
  const isNegative = value.startsWith("-");
841
1246
  const normalizedValue = isNegative ? value.substring(1) : value;
@@ -909,10 +1314,6 @@ var DecimalType = class extends FieldType {
909
1314
  if (dotCount > 1) return false;
910
1315
  return pattern.test(value);
911
1316
  }
912
- numberToString(value) {
913
- const decimalPlaces = this.options.decimalPlaces;
914
- return value.toFixed(decimalPlaces);
915
- }
916
1317
  getTypeName() {
917
1318
  return "\u0434\u0435\u0441\u044F\u0442\u0438\u0447\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E";
918
1319
  }
@@ -989,7 +1390,7 @@ var DateType = class extends FieldType {
989
1390
  getTypeName() {
990
1391
  return this.options.isDatetime ? "\u0434\u0430\u0442\u0430 \u0438 \u0432\u0440\u0435\u043C\u044F" : "\u0434\u0430\u0442\u0430";
991
1392
  }
992
- // Сравнение дат с учётом времени
1393
+ // Compare dates with optional time precision.
993
1394
  compareDates(value, boundary, type) {
994
1395
  if (this.options.isDatetime) {
995
1396
  if (type === "min") {
@@ -1018,6 +1419,8 @@ exports.BasePlugin = BasePlugin;
1018
1419
  exports.BooleanType = BooleanType;
1019
1420
  exports.DateType = DateType;
1020
1421
  exports.DecimalType = DecimalType;
1422
+ exports.DefaultErrorsSchema = DefaultErrorsSchema;
1423
+ exports.ErrorsSchema = ErrorsSchema;
1021
1424
  exports.Field = Field;
1022
1425
  exports.FieldType = FieldType;
1023
1426
  exports.Form = Form;