@blueprint-ts/core 4.1.0-beta.3 → 4.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,19 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { reactive, computed, toRaw, watch } from 'vue';
2
- import { camelCase, upperFirst, cloneDeep, isEqual } from 'lodash-es';
3
- import isEqualWith from 'lodash-es/isEqualWith';
11
+ import { camelCase, upperFirst, cloneDeep, debounce, isEqual } from 'lodash-es';
4
12
  import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver';
5
13
  import { PropertyAwareArray } from './PropertyAwareArray';
14
+ import { PropertyAwareObject, PROPERTY_AWARE_OBJECT_MARKER } from './PropertyAwareObject';
15
+ import { StrictPersistenceRestorePolicy } from './persistence/StrictPersistenceRestorePolicy';
16
+ import { BaseRule } from './validation/rules/BaseRule';
6
17
  import { ValidationMode } from './validation';
7
18
  function isRecord(value) {
8
19
  return !!value && typeof value === 'object' && !Array.isArray(value);
@@ -11,11 +22,40 @@ function isErrorMessages(value) {
11
22
  return Array.isArray(value) && (value.length === 0 || typeof value[0] === 'string');
12
23
  }
13
24
  function isErrorArray(value) {
14
- return Array.isArray(value) && value.length > 0 && isRecord(value[0]);
25
+ return Array.isArray(value) && value.some((item) => isRecord(item));
15
26
  }
16
27
  function isErrorObject(value) {
17
28
  return isRecord(value);
18
29
  }
30
+ function isPropertyAwareObject(value) {
31
+ return value instanceof PropertyAwareObject;
32
+ }
33
+ function isSerializedPropertyAwareObject(value) {
34
+ return isRecord(value) && value[PROPERTY_AWARE_OBJECT_MARKER] === true;
35
+ }
36
+ function restoreSerializedPropertyAwareValue(value) {
37
+ if (Array.isArray(value)) {
38
+ return value.map((item) => restoreSerializedPropertyAwareValue(item));
39
+ }
40
+ if (isSerializedPropertyAwareObject(value)) {
41
+ const restored = {};
42
+ for (const [key, child] of Object.entries(value)) {
43
+ if (key === PROPERTY_AWARE_OBJECT_MARKER) {
44
+ continue;
45
+ }
46
+ restored[key] = restoreSerializedPropertyAwareValue(child);
47
+ }
48
+ return new PropertyAwareObject(restored);
49
+ }
50
+ if (isRecord(value)) {
51
+ const restored = {};
52
+ for (const [key, child] of Object.entries(value)) {
53
+ restored[key] = restoreSerializedPropertyAwareValue(child);
54
+ }
55
+ return restored;
56
+ }
57
+ return value;
58
+ }
19
59
  export function propertyAwareToRaw(propertyAwareObject) {
20
60
  var _a;
21
61
  if (Array.isArray(propertyAwareObject)) {
@@ -65,40 +105,41 @@ function deepMergeArrays(target, source) {
65
105
  return s;
66
106
  });
67
107
  }
68
- /**
69
- * Helper: Given the defaults and a state object (or original),
70
- * for every key where the default is a PropertyAwareArray, rewrap the value if needed.
71
- */
72
- function restorePropertyAwareArrays(defaults, state) {
73
- for (const key in defaults) {
74
- const defVal = defaults[key];
75
- if (defVal instanceof PropertyAwareArray) {
76
- if (!(state[key] instanceof PropertyAwareArray)) {
77
- const values = Array.isArray(state[key]) ? Array.from(state[key]) : [];
78
- state[key] = new PropertyAwareArray(values);
108
+ function restorePropertyAwareStructure(defaults, value) {
109
+ if (defaults instanceof PropertyAwareArray) {
110
+ const restored = value instanceof PropertyAwareArray ? value : new PropertyAwareArray(Array.isArray(value) ? Array.from(value) : []);
111
+ const defaultItemTemplate = defaults[0];
112
+ for (let index = 0; index < restored.length; index++) {
113
+ if (index < defaults.length) {
114
+ restored[index] = restorePropertyAwareStructure(defaults[index], restored[index]);
115
+ }
116
+ else if (defaultItemTemplate !== undefined) {
117
+ restored[index] = restorePropertyAwareStructure(defaultItemTemplate, restored[index]);
118
+ }
119
+ else {
120
+ restored[index] = restoreSerializedPropertyAwareValue(restored[index]);
79
121
  }
80
122
  }
123
+ return restored;
81
124
  }
82
- }
83
- /**
84
- * Compare values while treating PropertyAwareArray as its underlying array.
85
- * This avoids false negatives when comparing persisted state to defaults.
86
- */
87
- function propertyAwareDeepEqual(a, b) {
88
- const getInner = (val) => {
89
- if (val instanceof PropertyAwareArray) {
90
- return val;
91
- }
92
- return val;
93
- };
94
- return isEqualWith(a, b, (aValue, bValue) => {
95
- const normA = getInner(aValue);
96
- const normB = getInner(bValue);
97
- if (normA !== aValue || normB !== bValue) {
98
- return isEqual(normA, normB);
99
- }
100
- return undefined; // use default comparison
101
- });
125
+ if (defaults instanceof PropertyAwareObject) {
126
+ const restored = value instanceof PropertyAwareObject
127
+ ? value
128
+ : new PropertyAwareObject(isRecord(value) ? value : {});
129
+ const restoredRecord = restored;
130
+ const defaultRecord = defaults;
131
+ for (const key of Object.keys(defaults)) {
132
+ restoredRecord[key] = restorePropertyAwareStructure(defaultRecord[key], restoredRecord[key]);
133
+ }
134
+ return restored;
135
+ }
136
+ if (isRecord(defaults) && isRecord(value)) {
137
+ for (const key of Object.keys(defaults)) {
138
+ value[key] = restorePropertyAwareStructure(defaults[key], value[key]);
139
+ }
140
+ return value;
141
+ }
142
+ return restoreSerializedPropertyAwareValue(value);
102
143
  }
103
144
  /**
104
145
  * A generic base class for forms.
@@ -118,6 +159,20 @@ export class BaseForm {
118
159
  getPersistenceDriver(_suffix) {
119
160
  return new NonPersistentDriver();
120
161
  }
162
+ getPersistenceRestorePolicy() {
163
+ return new StrictPersistenceRestorePolicy();
164
+ }
165
+ shouldLogPersistenceDebug() {
166
+ return false;
167
+ }
168
+ logPersistenceDebug(event) {
169
+ if (!this.shouldLogPersistenceDebug()) {
170
+ return;
171
+ }
172
+ const suffixLabel = event.persistSuffix ? ` (${event.persistSuffix})` : '';
173
+ const details = event.details ? ` ${JSON.stringify(event.details)}` : '';
174
+ console.debug(`[BaseForm persistence] ${event.formName}${suffixLabel}: ${event.action} (${event.reason})${details}`);
175
+ }
121
176
  /**
122
177
  * Helper: recursively computes the dirty state for a value based on the original.
123
178
  * For plain arrays we compare the entire array (a single flag), not each element.
@@ -130,7 +185,7 @@ export class BaseForm {
130
185
  const dirty = {};
131
186
  for (const key in current) {
132
187
  if (Object.prototype.hasOwnProperty.call(current, key)) {
133
- dirty[key] = !isEqual(current[key], original[key]);
188
+ dirty[key] = this.computeDirtyState(current[key], original[key]);
134
189
  }
135
190
  }
136
191
  return dirty;
@@ -251,44 +306,65 @@ export class BaseForm {
251
306
  }
252
307
  }
253
308
  constructor(defaults, options) {
309
+ var _a;
254
310
  this.options = options;
255
311
  this._errors = reactive({});
312
+ this._asyncErrors = reactive({});
256
313
  this.append = [];
257
314
  this.ignore = [];
258
315
  this.errorMap = {};
259
316
  this.rules = {};
317
+ this.validationGroups = {};
260
318
  this.fieldDependencies = new Map();
319
+ this.arrayWrapperCache = new Map();
320
+ this.arrayItemWrapperCache = new Map();
321
+ this.asyncValidationDebouncers = new Map();
322
+ this.pendingAsyncValidationContexts = new Map();
323
+ this.asyncValidationTokens = reactive({});
261
324
  const persist = (options === null || options === void 0 ? void 0 : options.persist) !== false;
262
325
  let initialData;
263
326
  const driver = this.getPersistenceDriver(options === null || options === void 0 ? void 0 : options.persistSuffix);
264
327
  if (persist) {
265
- const persisted = driver.get(this.constructor.name);
266
- if (persisted && propertyAwareDeepEqual(defaults, persisted.original)) {
267
- initialData = persisted.state;
268
- this.original = cloneDeep(persisted.original);
269
- this.dirty = reactive(persisted.dirty);
270
- this.touched = reactive(persisted.touched || {});
271
- restorePropertyAwareArrays(defaults, initialData);
272
- restorePropertyAwareArrays(defaults, this.original);
328
+ const persisted = (_a = driver.get(this.constructor.name)) !== null && _a !== void 0 ? _a : null;
329
+ const restoreDecision = this.getPersistenceRestorePolicy().resolve({
330
+ formName: this.constructor.name,
331
+ persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
332
+ defaults,
333
+ persisted
334
+ });
335
+ this.logPersistenceDebug({
336
+ formName: this.constructor.name,
337
+ persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
338
+ action: restoreDecision.action,
339
+ reason: restoreDecision.reason,
340
+ details: restoreDecision.details
341
+ });
342
+ if (restoreDecision.action === 'restore' && restoreDecision.persisted) {
343
+ initialData = restorePropertyAwareStructure(defaults, restoreDecision.persisted.state);
344
+ this.original = restorePropertyAwareStructure(defaults, cloneDeep(restoreDecision.persisted.original));
345
+ this.dirty = reactive(restoreDecision.persisted.dirty);
346
+ this.touched = reactive(restoreDecision.persisted.touched || {});
273
347
  }
274
348
  else {
275
- console.log('Discarding persisted data for ' + this.constructor.name + " because it doesn't match the defaults.");
276
349
  initialData = defaults;
277
- this.original = cloneDeep(defaults);
350
+ this.original = restorePropertyAwareStructure(defaults, cloneDeep(defaults));
278
351
  const init = this.initDirtyTouched(defaults);
279
352
  this.dirty = init.dirty;
280
353
  this.touched = init.touched;
281
- driver.remove(this.constructor.name);
354
+ if (restoreDecision.action === 'discard') {
355
+ driver.remove(this.constructor.name);
356
+ }
282
357
  }
283
358
  }
284
359
  else {
285
360
  initialData = defaults;
286
- this.original = cloneDeep(defaults);
361
+ this.original = restorePropertyAwareStructure(defaults, cloneDeep(defaults));
287
362
  const init = this.initDirtyTouched(defaults);
288
363
  this.dirty = init.dirty;
289
364
  this.touched = init.touched;
290
365
  }
291
366
  this.rules = this.defineRules();
367
+ this.validationGroups = this.defineValidationGroups();
292
368
  this.buildFieldDependencies();
293
369
  this.state = reactive(initialData);
294
370
  this._model = {};
@@ -300,7 +376,7 @@ export class BaseForm {
300
376
  set: (newVal) => {
301
377
  const next = Array.isArray(newVal) ? Array.from(newVal) : [];
302
378
  this.replacePropertyAwareArray(key, next);
303
- this.dirty[key] = next.map(() => false);
379
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
304
380
  this.markFieldUpdated(key, driver);
305
381
  }
306
382
  });
@@ -323,29 +399,7 @@ export class BaseForm {
323
399
  }, { deep: true });
324
400
  }
325
401
  }
326
- this._hasErrors = computed(() => {
327
- for (const field in this._errors) {
328
- if (Object.prototype.hasOwnProperty.call(this._errors, field)) {
329
- const fieldErrors = this._errors[field];
330
- if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
331
- return true;
332
- }
333
- if (fieldErrors && typeof fieldErrors === 'object') {
334
- if (Array.isArray(fieldErrors)) {
335
- for (const item of fieldErrors) {
336
- if (item && typeof item === 'object' && Object.keys(item).length > 0) {
337
- return true;
338
- }
339
- }
340
- }
341
- else if (Object.keys(fieldErrors).length > 0) {
342
- return true;
343
- }
344
- }
345
- }
346
- }
347
- return false;
348
- });
402
+ this._hasErrors = computed(() => Object.keys(this.flattenErrors()).length > 0);
349
403
  if (persist) {
350
404
  watch(() => this.state, () => this.persistState(driver), { deep: true, immediate: true });
351
405
  }
@@ -354,18 +408,275 @@ export class BaseForm {
354
408
  defineRules() {
355
409
  return {};
356
410
  }
411
+ defineValidationGroups() {
412
+ return {};
413
+ }
414
+ clearErrorBag(errorBag) {
415
+ for (const key in errorBag) {
416
+ delete errorBag[key];
417
+ }
418
+ }
419
+ clearSyncErrors() {
420
+ this.clearErrorBag(this._errors);
421
+ }
422
+ clearAsyncErrors() {
423
+ this.clearErrorBag(this._asyncErrors);
424
+ }
357
425
  clearErrors() {
358
- for (const key in this._errors) {
359
- delete this._errors[key];
426
+ this.cancelPendingAsyncValidations();
427
+ this.clearSyncErrors();
428
+ this.clearAsyncErrors();
429
+ }
430
+ hasAsyncRules(field) {
431
+ const fieldConfig = this.rules[field];
432
+ if (!(fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.rules) || fieldConfig.rules.length === 0) {
433
+ return false;
434
+ }
435
+ return fieldConfig.rules.some((rule) => rule.validateAsync !== BaseRule.prototype.validateAsync);
436
+ }
437
+ bumpAsyncValidationToken(field) {
438
+ var _a;
439
+ const fieldKey = String(field);
440
+ const nextToken = ((_a = this.asyncValidationTokens[fieldKey]) !== null && _a !== void 0 ? _a : 0) + 1;
441
+ this.asyncValidationTokens[fieldKey] = nextToken;
442
+ return nextToken;
443
+ }
444
+ cancelPendingAsyncValidations() {
445
+ var _a;
446
+ for (const debouncer of this.asyncValidationDebouncers.values()) {
447
+ debouncer.cancel();
448
+ }
449
+ this.pendingAsyncValidationContexts.clear();
450
+ for (const fieldKey of Object.keys(this.asyncValidationTokens)) {
451
+ this.asyncValidationTokens[fieldKey] = ((_a = this.asyncValidationTokens[fieldKey]) !== null && _a !== void 0 ? _a : 0) + 1;
452
+ }
453
+ }
454
+ scheduleAsyncValidation(field, context) {
455
+ var _a, _b, _c;
456
+ if (!this.hasAsyncRules(field)) {
457
+ return;
458
+ }
459
+ const token = this.bumpAsyncValidationToken(field);
460
+ const debounceMs = (_c = (_b = (_a = this.rules[field]) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.asyncDebounceMs) !== null && _c !== void 0 ? _c : 0;
461
+ this.pendingAsyncValidationContexts.set(field, { token, context });
462
+ if (debounceMs <= 0) {
463
+ void this.executeScheduledAsyncValidation(field).catch(() => undefined);
464
+ return;
465
+ }
466
+ let debouncer = this.asyncValidationDebouncers.get(field);
467
+ if (debouncer === undefined) {
468
+ debouncer = debounce(() => {
469
+ void this.executeScheduledAsyncValidation(field).catch(() => undefined);
470
+ }, debounceMs);
471
+ this.asyncValidationDebouncers.set(field, debouncer);
360
472
  }
473
+ debouncer();
361
474
  }
362
- getOrCreateErrorArray(key) {
363
- const existing = this._errors[key];
475
+ executeScheduledAsyncValidation(field) {
476
+ return __awaiter(this, void 0, void 0, function* () {
477
+ const pending = this.pendingAsyncValidationContexts.get(field);
478
+ if (pending === undefined) {
479
+ return;
480
+ }
481
+ yield this.runFieldAsyncValidation(field, Object.assign(Object.assign({}, pending.context), { skipSyncValidation: true, skipAsyncValidation: true }), pending.token);
482
+ });
483
+ }
484
+ flattenErrorValue(value, path, flattened) {
485
+ if (value === undefined) {
486
+ return;
487
+ }
488
+ if (isErrorMessages(value)) {
489
+ if (value.length > 0) {
490
+ flattened[path] = cloneDeep(value);
491
+ }
492
+ return;
493
+ }
494
+ if (isErrorArray(value)) {
495
+ value.forEach((item, index) => {
496
+ const itemPath = `${path}.${index}`;
497
+ if (isErrorMessages(item)) {
498
+ if (item.length > 0) {
499
+ flattened[itemPath] = cloneDeep(item);
500
+ }
501
+ return;
502
+ }
503
+ this.flattenErrorValue(item, itemPath, flattened);
504
+ });
505
+ return;
506
+ }
507
+ if (!isErrorObject(value)) {
508
+ return;
509
+ }
510
+ const rootErrors = value[''];
511
+ if (isErrorMessages(rootErrors) && rootErrors.length > 0) {
512
+ flattened[path] = cloneDeep(rootErrors);
513
+ }
514
+ for (const key of Object.keys(value)) {
515
+ if (key === '') {
516
+ continue;
517
+ }
518
+ const nestedPath = path.length > 0 ? `${path}.${key}` : key;
519
+ this.flattenErrorValue(value[key], nestedPath, flattened);
520
+ }
521
+ }
522
+ flattenErrorsFromBag(errorBag) {
523
+ const flattened = {};
524
+ for (const key of Object.keys(errorBag)) {
525
+ this.flattenErrorValue(errorBag[key], key, flattened);
526
+ }
527
+ return flattened;
528
+ }
529
+ mergeErrorMessages(...messageSets) {
530
+ const merged = [];
531
+ for (const messages of messageSets) {
532
+ for (const message of messages) {
533
+ if (!merged.includes(message)) {
534
+ merged.push(message);
535
+ }
536
+ }
537
+ }
538
+ return merged;
539
+ }
540
+ flattenErrors() {
541
+ var _a;
542
+ const flattened = this.flattenErrorsFromBag(this._errors);
543
+ for (const [key, messages] of Object.entries(this.flattenErrorsFromBag(this._asyncErrors))) {
544
+ flattened[key] = this.mergeErrorMessages((_a = flattened[key]) !== null && _a !== void 0 ? _a : [], messages);
545
+ }
546
+ return flattened;
547
+ }
548
+ applyErrors(errorsData, useErrorMap, targetBag = this._errors) {
549
+ var _a, _b, _c;
550
+ for (const serverKey in errorsData) {
551
+ if (!Object.prototype.hasOwnProperty.call(errorsData, serverKey)) {
552
+ continue;
553
+ }
554
+ const errorMessage = errorsData[serverKey];
555
+ if (errorMessage === undefined) {
556
+ continue;
557
+ }
558
+ let targetKeys = [serverKey];
559
+ if (useErrorMap) {
560
+ const mapping = (_a = this.errorMap) === null || _a === void 0 ? void 0 : _a[serverKey];
561
+ if (mapping) {
562
+ targetKeys = Array.isArray(mapping) ? mapping : [mapping];
563
+ }
564
+ }
565
+ for (const targetKey of targetKeys) {
566
+ const parts = targetKey.split('.');
567
+ if (parts.length > 1) {
568
+ const topKey = (_b = parts[0]) !== null && _b !== void 0 ? _b : '';
569
+ const indexPart = (_c = parts[1]) !== null && _c !== void 0 ? _c : '';
570
+ const index = Number.parseInt(indexPart, 10);
571
+ if (!topKey) {
572
+ targetBag[targetKey] = errorMessage;
573
+ continue;
574
+ }
575
+ if (!Number.isFinite(index)) {
576
+ const current = isErrorObject(targetBag[topKey]) ? targetBag[topKey] : {};
577
+ targetBag[topKey] = current;
578
+ this.setNestedError(current, parts.slice(1), errorMessage);
579
+ continue;
580
+ }
581
+ const errorSubKey = parts.slice(2).join('.');
582
+ const errors = this.getOrCreateErrorArray(topKey, targetBag);
583
+ const errorObject = this.getOrCreateErrorObject(errors, index);
584
+ if (errorSubKey.length === 0) {
585
+ errorObject[''] = errorMessage;
586
+ }
587
+ else {
588
+ this.setNestedError(errorObject, errorSubKey.split('.'), errorMessage);
589
+ }
590
+ }
591
+ else {
592
+ targetBag[targetKey] = errorMessage;
593
+ }
594
+ }
595
+ }
596
+ }
597
+ getValidationGroupPaths(group) {
598
+ const paths = this.validationGroups[group];
599
+ if (!paths || paths.length === 0) {
600
+ return [];
601
+ }
602
+ return [...paths];
603
+ }
604
+ matchesValidationGroupPath(errorKey, groupPath) {
605
+ return errorKey === groupPath || errorKey.startsWith(`${groupPath}.`);
606
+ }
607
+ errorKeyBelongsToGroup(errorKey, group) {
608
+ return this.getValidationGroupPaths(group).some((groupPath) => this.matchesValidationGroupPath(errorKey, groupPath));
609
+ }
610
+ getValidationGroupFields(group) {
611
+ const fields = [];
612
+ for (const path of this.getValidationGroupPaths(group)) {
613
+ const [topLevelField] = path.split('.');
614
+ if (!topLevelField || !(topLevelField in this.state)) {
615
+ continue;
616
+ }
617
+ const typedField = topLevelField;
618
+ if (!fields.includes(typedField)) {
619
+ fields.push(typedField);
620
+ }
621
+ }
622
+ return fields;
623
+ }
624
+ clearGroupErrors(group) {
625
+ const groupPaths = this.getValidationGroupPaths(group);
626
+ if (groupPaths.length === 0) {
627
+ return;
628
+ }
629
+ const preservedErrors = {};
630
+ for (const [errorKey, errorValue] of Object.entries(this.flattenErrors())) {
631
+ if (!this.errorKeyBelongsToGroup(errorKey, group)) {
632
+ preservedErrors[errorKey] = cloneDeep(errorValue);
633
+ }
634
+ }
635
+ this.clearErrors();
636
+ this.applyErrors(preservedErrors, false);
637
+ }
638
+ collectSiblingNestedErrors(field, path) {
639
+ if (path.length === 0) {
640
+ return {};
641
+ }
642
+ const fieldKey = String(field);
643
+ const clearedPath = `${fieldKey}.${path.join('.')}`;
644
+ const preservedErrors = {};
645
+ for (const [errorKey, errorValue] of Object.entries(this.flattenErrors())) {
646
+ if (!this.matchesValidationGroupPath(errorKey, fieldKey)) {
647
+ continue;
648
+ }
649
+ if (errorKey === clearedPath || errorKey.startsWith(`${clearedPath}.`)) {
650
+ continue;
651
+ }
652
+ preservedErrors[errorKey] = cloneDeep(errorValue);
653
+ }
654
+ return preservedErrors;
655
+ }
656
+ validateFieldPreservingNestedErrors(field, path) {
657
+ const preservedErrors = this.collectSiblingNestedErrors(field, path);
658
+ this.validateField(field);
659
+ if (Object.keys(preservedErrors).length > 0) {
660
+ this.applyErrors(preservedErrors, false);
661
+ }
662
+ }
663
+ clearErrorBagPaths(errorBag, paths) {
664
+ const preservedErrors = {};
665
+ for (const [errorKey, errorValue] of Object.entries(this.flattenErrorsFromBag(errorBag))) {
666
+ if (!paths.some((path) => this.matchesValidationGroupPath(errorKey, path))) {
667
+ preservedErrors[errorKey] = cloneDeep(errorValue);
668
+ }
669
+ }
670
+ this.clearErrorBag(errorBag);
671
+ this.applyErrors(preservedErrors, false, errorBag);
672
+ }
673
+ getOrCreateErrorArray(key, errorBag = this._errors) {
674
+ const existing = errorBag[key];
364
675
  if (isErrorArray(existing)) {
365
676
  return existing;
366
677
  }
367
678
  const next = [];
368
- this._errors[key] = next;
679
+ errorBag[key] = next;
369
680
  return next;
370
681
  }
371
682
  getOrCreateErrorObject(errors, index) {
@@ -378,8 +689,9 @@ export class BaseForm {
378
689
  return next;
379
690
  }
380
691
  getFieldErrors(field) {
381
- const errors = this._errors[field];
382
- return isErrorMessages(errors) ? errors : [];
692
+ const syncErrors = this._errors[field];
693
+ const asyncErrors = this._asyncErrors[field];
694
+ return this.mergeErrorMessages(isErrorMessages(syncErrors) ? syncErrors : [], isErrorMessages(asyncErrors) ? asyncErrors : []);
383
695
  }
384
696
  getOrCreateFieldErrors(field) {
385
697
  const existing = this._errors[field];
@@ -399,22 +711,24 @@ export class BaseForm {
399
711
  return isErrorObject(item) ? item : undefined;
400
712
  }
401
713
  getArrayItemFieldErrors(field, index, innerKey) {
402
- const itemErrors = this.getArrayItemErrors(field, index);
403
- if (!itemErrors) {
404
- return [];
405
- }
406
- const fieldErrors = itemErrors[innerKey];
407
- if (isErrorMessages(fieldErrors)) {
408
- return fieldErrors;
409
- }
410
- if (isErrorObject(fieldErrors)) {
411
- const nestedErrors = fieldErrors[''];
412
- return isErrorMessages(nestedErrors) ? nestedErrors : [];
413
- }
414
- return [];
714
+ return this.mergeErrorMessages(this.getNestedErrorMessagesFromValue(this.getArrayItemErrors(field, index), innerKey.split('.')), this.getNestedErrorMessagesFromValue(this.getArrayItemErrorsFromBag(this._asyncErrors, field, index), innerKey.split('.')));
415
715
  }
416
716
  getArrayItemErrorMessages(field, index) {
417
- const errors = this._errors[field];
717
+ return this.mergeErrorMessages(this.getArrayItemErrorMessagesFromBag(this._errors, field, index), this.getArrayItemErrorMessagesFromBag(this._asyncErrors, field, index));
718
+ }
719
+ getObjectFieldErrors(field, path) {
720
+ return this.mergeErrorMessages(this.getNestedErrorMessagesFromValue(this._errors[field], path), this.getNestedErrorMessagesFromValue(this._asyncErrors[field], path));
721
+ }
722
+ getArrayItemErrorsFromBag(errorBag, field, index) {
723
+ const errors = errorBag[field];
724
+ if (!isErrorArray(errors)) {
725
+ return undefined;
726
+ }
727
+ const item = errors[index];
728
+ return isErrorObject(item) ? item : undefined;
729
+ }
730
+ getArrayItemErrorMessagesFromBag(errorBag, field, index) {
731
+ const errors = errorBag[field];
418
732
  if (!Array.isArray(errors)) {
419
733
  return [];
420
734
  }
@@ -457,13 +771,7 @@ export class BaseForm {
457
771
  return false;
458
772
  }
459
773
  const entry = dirtyState[index];
460
- if (typeof entry === 'boolean') {
461
- return entry;
462
- }
463
- if (isRecord(entry)) {
464
- return entry[innerKey] === true;
465
- }
466
- return false;
774
+ return this.getNestedDirtyValue(entry, innerKey.split('.'));
467
775
  }
468
776
  getArrayItemDirtyValue(field, index) {
469
777
  const dirtyState = this.dirty[field];
@@ -473,49 +781,229 @@ export class BaseForm {
473
781
  const entry = dirtyState[index];
474
782
  return typeof entry === 'boolean' ? entry : false;
475
783
  }
784
+ getNestedErrorMessagesFromValue(value, path) {
785
+ if (path.length === 0) {
786
+ if (isErrorMessages(value)) {
787
+ return value;
788
+ }
789
+ if (isErrorObject(value)) {
790
+ const nestedErrors = value[''];
791
+ return isErrorMessages(nestedErrors) ? nestedErrors : [];
792
+ }
793
+ return [];
794
+ }
795
+ if (!isErrorObject(value)) {
796
+ return [];
797
+ }
798
+ const [segment, ...rest] = path;
799
+ if (segment === undefined) {
800
+ return [];
801
+ }
802
+ return this.getNestedErrorMessagesFromValue(value[segment], rest);
803
+ }
804
+ setNestedError(target, path, errorMessage) {
805
+ if (path.length === 0) {
806
+ target[''] = errorMessage;
807
+ return;
808
+ }
809
+ const [segment, ...rest] = path;
810
+ if (segment === undefined) {
811
+ return;
812
+ }
813
+ if (rest.length === 0) {
814
+ target[segment] = errorMessage;
815
+ return;
816
+ }
817
+ const next = isErrorObject(target[segment]) ? target[segment] : {};
818
+ target[segment] = next;
819
+ this.setNestedError(next, rest, errorMessage);
820
+ }
821
+ getNestedDirtyValue(value, path) {
822
+ if (path.length === 0) {
823
+ if (typeof value === 'boolean') {
824
+ return value;
825
+ }
826
+ if (Array.isArray(value)) {
827
+ return value.some((entry) => this.getNestedDirtyValue(entry, []));
828
+ }
829
+ if (isRecord(value)) {
830
+ return Object.values(value).some((item) => this.getNestedDirtyValue(item, []));
831
+ }
832
+ return false;
833
+ }
834
+ if (!isRecord(value)) {
835
+ return false;
836
+ }
837
+ const [segment, ...rest] = path;
838
+ if (segment === undefined) {
839
+ return false;
840
+ }
841
+ return this.getNestedDirtyValue(value[segment], rest);
842
+ }
843
+ createFieldProperty(getValue, setValue, getErrors, getDirty, getTouched) {
844
+ const field = {
845
+ model: computed({
846
+ get: getValue,
847
+ set: setValue
848
+ })
849
+ };
850
+ Object.defineProperties(field, {
851
+ errors: {
852
+ enumerable: true,
853
+ get: getErrors
854
+ },
855
+ dirty: {
856
+ enumerable: true,
857
+ get: getDirty
858
+ },
859
+ touched: {
860
+ enumerable: true,
861
+ get: getTouched
862
+ }
863
+ });
864
+ return field;
865
+ }
866
+ resolveArrayItemIndex(field, item) {
867
+ const value = this.state[field];
868
+ if (!(value instanceof PropertyAwareArray)) {
869
+ return -1;
870
+ }
871
+ return value.indexOf(item);
872
+ }
873
+ getArrayItemValueByPath(field, item, path) {
874
+ const index = this.resolveArrayItemIndex(field, item);
875
+ if (index < 0) {
876
+ return undefined;
877
+ }
878
+ let current = this.state[field][index];
879
+ for (const segment of path) {
880
+ if (!isRecord(current)) {
881
+ return undefined;
882
+ }
883
+ current = current[segment];
884
+ }
885
+ return current;
886
+ }
887
+ setArrayItemValueByPath(field, item, path, value) {
888
+ const index = this.resolveArrayItemIndex(field, item);
889
+ if (index < 0) {
890
+ return;
891
+ }
892
+ if (path.length === 0) {
893
+ ;
894
+ this.state[field][index] = value;
895
+ const updatedElement = this.state[field][index];
896
+ const originalElement = this.original[field][index];
897
+ this.setArrayDirty(field, index, this.computeDirtyState(updatedElement, originalElement));
898
+ this.touched[field] = true;
899
+ this.validateField(field);
900
+ this.validateDependentFields(field);
901
+ return;
902
+ }
903
+ const currentItem = this.state[field][index];
904
+ if (!isRecord(currentItem)) {
905
+ return;
906
+ }
907
+ let current = currentItem;
908
+ const segments = [...path];
909
+ const last = segments.pop();
910
+ if (last === undefined) {
911
+ return;
912
+ }
913
+ for (const segment of segments) {
914
+ const next = current[segment];
915
+ if (!isRecord(next)) {
916
+ return;
917
+ }
918
+ current = next;
919
+ }
920
+ current[last] = value;
921
+ const updatedElement = this.state[field][index];
922
+ const originalElement = this.original[field][index];
923
+ this.setArrayDirty(field, index, this.computeDirtyState(updatedElement, originalElement));
924
+ this.touched[field] = true;
925
+ this.validateFieldPreservingNestedErrors(field, [String(index), ...path]);
926
+ this.validateDependentFields(field);
927
+ }
928
+ createObjectWrapperFromShape(field, shape, getValueByPath, setValueByPath, getErrorsByPath, getDirtyByPath, getTouched) {
929
+ const wrapper = {};
930
+ const shapeRecord = shape;
931
+ for (const innerKey of Object.keys(shape)) {
932
+ const child = shapeRecord[innerKey];
933
+ if (isPropertyAwareObject(child)) {
934
+ wrapper[innerKey] = this.createObjectWrapperFromShape(field, child, (path) => getValueByPath([innerKey, ...path]), (path, value) => setValueByPath([innerKey, ...path], value), (path) => getErrorsByPath([innerKey, ...path]), (path) => getDirtyByPath([innerKey, ...path]), getTouched);
935
+ continue;
936
+ }
937
+ wrapper[innerKey] = this.createFieldProperty(() => getValueByPath([innerKey]), (newValue) => setValueByPath([innerKey], newValue), () => getErrorsByPath([innerKey]), () => getDirtyByPath([innerKey]), getTouched);
938
+ }
939
+ return wrapper;
940
+ }
941
+ getOrCreateArrayItemWrapper(field, item) {
942
+ let fieldCache = this.arrayItemWrapperCache.get(field);
943
+ if (!fieldCache) {
944
+ fieldCache = new WeakMap();
945
+ this.arrayItemWrapperCache.set(field, fieldCache);
946
+ }
947
+ const existing = fieldCache.get(item);
948
+ if (existing) {
949
+ return existing;
950
+ }
951
+ const wrapper = {};
952
+ if (isRecord(item)) {
953
+ for (const innerKey of Object.keys(item)) {
954
+ const child = item[innerKey];
955
+ if (isPropertyAwareObject(child)) {
956
+ wrapper[innerKey] = this.createObjectWrapperFromShape(field, child, (path) => this.getArrayItemValueByPath(field, item, [innerKey, ...path]), (path, value) => this.setArrayItemValueByPath(field, item, [innerKey, ...path], value), (path) => {
957
+ const index = this.resolveArrayItemIndex(field, item);
958
+ return index < 0 ? [] : this.getArrayItemFieldErrors(String(field), index, [innerKey, ...path].join('.'));
959
+ }, (path) => {
960
+ var _a;
961
+ const index = this.resolveArrayItemIndex(field, item);
962
+ return index < 0
963
+ ? false
964
+ : this.getNestedDirtyValue((_a = this.dirty[field]) === null || _a === void 0 ? void 0 : _a[index], [innerKey, ...path]);
965
+ }, () => this.touched[field] || false);
966
+ continue;
967
+ }
968
+ wrapper[innerKey] = this.createFieldProperty(() => this.getArrayItemValueByPath(field, item, [innerKey]), (newValue) => this.setArrayItemValueByPath(field, item, [innerKey], newValue), () => {
969
+ const index = this.resolveArrayItemIndex(field, item);
970
+ return index < 0 ? [] : this.getArrayItemFieldErrors(String(field), index, innerKey);
971
+ }, () => {
972
+ const index = this.resolveArrayItemIndex(field, item);
973
+ return index < 0 ? false : this.getArrayItemDirty(field, index, innerKey);
974
+ }, () => this.touched[field] || false);
975
+ }
976
+ }
977
+ else {
978
+ wrapper['value'] = this.createFieldProperty(() => this.getArrayItemValueByPath(field, item, []), (newValue) => this.setArrayItemValueByPath(field, item, [], newValue), () => {
979
+ const index = this.resolveArrayItemIndex(field, item);
980
+ return index < 0 ? [] : this.getArrayItemErrorMessages(String(field), index);
981
+ }, () => {
982
+ const index = this.resolveArrayItemIndex(field, item);
983
+ return index < 0 ? false : this.getArrayItemDirtyValue(field, index);
984
+ }, () => this.touched[field] || false);
985
+ }
986
+ fieldCache.set(item, wrapper);
987
+ return wrapper;
988
+ }
989
+ getOrCreateArrayWrappers(field, value) {
990
+ let wrappers = this.arrayWrapperCache.get(field);
991
+ if (!wrappers) {
992
+ wrappers = [];
993
+ this.arrayWrapperCache.set(field, wrappers);
994
+ }
995
+ wrappers.length = 0;
996
+ value.forEach((item) => {
997
+ wrappers.push(this.getOrCreateArrayItemWrapper(field, item));
998
+ });
999
+ return wrappers;
1000
+ }
476
1001
  /**
477
1002
  * Map server-side errors (including dot-notation paths) into the form error bag.
478
1003
  */
479
1004
  fillErrors(errorsData) {
480
- var _a, _b, _c;
481
1005
  this.clearErrors();
482
- for (const serverKey in errorsData) {
483
- if (Object.prototype.hasOwnProperty.call(errorsData, serverKey)) {
484
- const errorMessage = errorsData[serverKey];
485
- if (errorMessage === undefined) {
486
- continue;
487
- }
488
- let targetKeys = [serverKey];
489
- const mapping = (_a = this.errorMap) === null || _a === void 0 ? void 0 : _a[serverKey];
490
- if (mapping) {
491
- targetKeys = Array.isArray(mapping) ? mapping : [mapping];
492
- }
493
- for (const targetKey of targetKeys) {
494
- const parts = targetKey.split('.');
495
- if (parts.length > 1) {
496
- const topKey = (_b = parts[0]) !== null && _b !== void 0 ? _b : '';
497
- const indexPart = (_c = parts[1]) !== null && _c !== void 0 ? _c : '';
498
- const index = Number.parseInt(indexPart, 10);
499
- if (!topKey || !Number.isFinite(index)) {
500
- this._errors[targetKey] = errorMessage;
501
- continue;
502
- }
503
- const errorSubKey = parts.slice(2).join('.');
504
- const errors = this.getOrCreateErrorArray(topKey);
505
- const errorObject = this.getOrCreateErrorObject(errors, index);
506
- if (errorSubKey.length === 0) {
507
- errorObject[''] = errorMessage;
508
- }
509
- else {
510
- errorObject[errorSubKey] = errorMessage;
511
- }
512
- }
513
- else {
514
- this._errors[targetKey] = errorMessage;
515
- }
516
- }
517
- }
518
- }
1006
+ this.applyErrors(errorsData, true);
519
1007
  }
520
1008
  /**
521
1009
  * Mark a field as touched, which indicates user interaction
@@ -572,19 +1060,22 @@ export class BaseForm {
572
1060
  this.getOrCreateFieldErrors(errorKey).push(rule.getMessage());
573
1061
  }
574
1062
  }
1063
+ if (!context.skipAsyncValidation) {
1064
+ this.scheduleAsyncValidation(field, context);
1065
+ }
575
1066
  }
576
1067
  }
577
- validate(isSubmitting = false) {
1068
+ validate(isSubmitting = false, options = {}) {
1069
+ var _a;
578
1070
  let isValid = true;
579
- for (const key in this._errors) {
580
- delete this._errors[key];
581
- }
1071
+ this.clearSyncErrors();
582
1072
  for (const field in this.rules) {
583
1073
  if (Object.prototype.hasOwnProperty.call(this.rules, field)) {
584
1074
  this.validateField(field, {
585
1075
  isSubmitting,
586
1076
  isDependentChange: false,
587
- isTouched: this.isTouched(field)
1077
+ isTouched: this.isTouched(field),
1078
+ skipAsyncValidation: (_a = options.skipAsyncValidation) !== null && _a !== void 0 ? _a : false
588
1079
  });
589
1080
  const fieldErrors = this._errors[String(field)];
590
1081
  if (isErrorMessages(fieldErrors) && fieldErrors.length > 0) {
@@ -594,6 +1085,99 @@ export class BaseForm {
594
1085
  }
595
1086
  return isValid;
596
1087
  }
1088
+ validateGroup(group, isSubmitting = false, options = {}) {
1089
+ var _a;
1090
+ const fields = this.getValidationGroupFields(group);
1091
+ if (fields.length === 0) {
1092
+ return true;
1093
+ }
1094
+ this.clearGroupErrors(group);
1095
+ for (const field of fields) {
1096
+ this.validateField(field, {
1097
+ isSubmitting,
1098
+ isDependentChange: false,
1099
+ isTouched: this.isTouched(field),
1100
+ skipAsyncValidation: (_a = options.skipAsyncValidation) !== null && _a !== void 0 ? _a : false
1101
+ });
1102
+ }
1103
+ return !this.hasErrorsInGroup(group);
1104
+ }
1105
+ runFieldAsyncValidation(field_1) {
1106
+ return __awaiter(this, arguments, void 0, function* (field, context = {}, expectedToken) {
1107
+ var _a;
1108
+ if (!context.skipSyncValidation) {
1109
+ this.validateField(field, Object.assign(Object.assign({}, context), { skipAsyncValidation: true }));
1110
+ }
1111
+ const fieldConfig = this.rules[field];
1112
+ if (!(fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.rules) || fieldConfig.rules.length === 0) {
1113
+ return !Object.keys(this.flattenErrors()).some((errorKey) => this.matchesValidationGroupPath(errorKey, String(field)));
1114
+ }
1115
+ const asyncPaths = fieldConfig.rules.flatMap((rule) => rule.getAsyncValidationPaths(field, this.state));
1116
+ const payload = this.buildPayload();
1117
+ const nextAsyncErrors = {};
1118
+ for (const rule of fieldConfig.rules) {
1119
+ const errors = yield rule.validateAsync(this.state[field], this.state, {
1120
+ field: String(field),
1121
+ payload,
1122
+ isSubmitting: (_a = context.isSubmitting) !== null && _a !== void 0 ? _a : false
1123
+ });
1124
+ if (errors && Object.keys(errors).length > 0) {
1125
+ this.applyErrors(errors, false, nextAsyncErrors);
1126
+ }
1127
+ }
1128
+ if (expectedToken !== undefined && this.asyncValidationTokens[String(field)] !== expectedToken) {
1129
+ return !Object.keys(this.flattenErrors()).some((errorKey) => this.matchesValidationGroupPath(errorKey, String(field)));
1130
+ }
1131
+ this.clearErrorBagPaths(this._asyncErrors, asyncPaths);
1132
+ for (const [key, messages] of Object.entries(this.flattenErrorsFromBag(nextAsyncErrors))) {
1133
+ this.applyErrors({ [key]: messages }, false, this._asyncErrors);
1134
+ }
1135
+ return !Object.keys(this.flattenErrors()).some((errorKey) => this.matchesValidationGroupPath(errorKey, String(field)));
1136
+ });
1137
+ }
1138
+ validateFieldAsync(field_1) {
1139
+ return __awaiter(this, arguments, void 0, function* (field, context = {}) {
1140
+ const token = this.bumpAsyncValidationToken(field);
1141
+ return yield this.runFieldAsyncValidation(field, Object.assign(Object.assign({}, context), { skipAsyncValidation: true }), token);
1142
+ });
1143
+ }
1144
+ validateAsync() {
1145
+ return __awaiter(this, arguments, void 0, function* (isSubmitting = false) {
1146
+ const isSyncValid = this.validate(isSubmitting, { skipAsyncValidation: true });
1147
+ for (const field in this.rules) {
1148
+ if (Object.prototype.hasOwnProperty.call(this.rules, field)) {
1149
+ yield this.validateFieldAsync(field, {
1150
+ isSubmitting,
1151
+ isTouched: this.isTouched(field),
1152
+ skipSyncValidation: true
1153
+ });
1154
+ }
1155
+ }
1156
+ return isSyncValid && !this.hasErrors();
1157
+ });
1158
+ }
1159
+ validateGroupAsync(group_1) {
1160
+ return __awaiter(this, arguments, void 0, function* (group, isSubmitting = false) {
1161
+ const fields = this.getValidationGroupFields(group);
1162
+ if (fields.length === 0) {
1163
+ return true;
1164
+ }
1165
+ const isSyncValid = this.validateGroup(group, isSubmitting, { skipAsyncValidation: true });
1166
+ for (const field of fields) {
1167
+ yield this.validateFieldAsync(field, {
1168
+ isSubmitting,
1169
+ isTouched: this.isTouched(field),
1170
+ skipSyncValidation: true
1171
+ });
1172
+ }
1173
+ return isSyncValid && !this.hasErrorsInGroup(group);
1174
+ });
1175
+ }
1176
+ touchGroup(group) {
1177
+ for (const field of this.getValidationGroupFields(group)) {
1178
+ this.touch(field);
1179
+ }
1180
+ }
597
1181
  fillState(data) {
598
1182
  var _a;
599
1183
  const driver = this.getPersistenceDriver((_a = this.options) === null || _a === void 0 ? void 0 : _a.persistSuffix);
@@ -606,7 +1190,13 @@ export class BaseForm {
606
1190
  if (currentVal instanceof PropertyAwareArray) {
607
1191
  const values = newVal instanceof PropertyAwareArray || Array.isArray(newVal) ? Array.from(newVal) : [];
608
1192
  this.replacePropertyAwareArray(key, values);
609
- this.dirty[key] = values.map(() => false);
1193
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
1194
+ this.touched[key] = true;
1195
+ continue;
1196
+ }
1197
+ if (isPropertyAwareObject(currentVal)) {
1198
+ this.state[key] = restorePropertyAwareStructure(currentVal, newVal);
1199
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
610
1200
  this.touched[key] = true;
611
1201
  continue;
612
1202
  }
@@ -659,6 +1249,17 @@ export class BaseForm {
659
1249
  if (value instanceof PropertyAwareArray) {
660
1250
  return [...value].map((item) => this.transformValue(item, parentKey));
661
1251
  }
1252
+ if (isPropertyAwareObject(value)) {
1253
+ const result = {};
1254
+ const valueRecord = value;
1255
+ for (const prop in valueRecord) {
1256
+ const transformed = this.transformValue(valueRecord[prop], parentKey);
1257
+ if (transformed !== undefined) {
1258
+ result[prop] = transformed;
1259
+ }
1260
+ }
1261
+ return result;
1262
+ }
662
1263
  if (Array.isArray(value)) {
663
1264
  return value.map((item) => this.transformValue(item, parentKey));
664
1265
  }
@@ -742,6 +1343,11 @@ export class BaseForm {
742
1343
  this.dirty[typedKey] = values.map(() => false);
743
1344
  this.touched[typedKey] = false;
744
1345
  }
1346
+ else if (isPropertyAwareObject(this.state[key])) {
1347
+ this.state[key] = restorePropertyAwareStructure(this.original[key], cloneDeep(this.original[key]));
1348
+ this.dirty[key] = false;
1349
+ this.touched[key] = false;
1350
+ }
745
1351
  else if (Array.isArray(this.original[key])) {
746
1352
  this.state[key] = cloneDeep(this.original[key]);
747
1353
  this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
@@ -753,9 +1359,7 @@ export class BaseForm {
753
1359
  this.touched[key] = false;
754
1360
  }
755
1361
  }
756
- for (const key in this._errors) {
757
- delete this._errors[key];
758
- }
1362
+ this.clearErrors();
759
1363
  this.persistState(driver);
760
1364
  this.validate();
761
1365
  }
@@ -820,65 +1424,47 @@ export class BaseForm {
820
1424
  for (const key of Object.keys(this.state)) {
821
1425
  const value = this.state[key];
822
1426
  if (value instanceof PropertyAwareArray) {
823
- const arrayProps = [...value].map((item, index) => {
824
- if (isRecord(item)) {
825
- const elementProps = {};
826
- for (const innerKey of Object.keys(item)) {
827
- elementProps[innerKey] = {
828
- model: computed({
829
- get: () => {
830
- const current = value[index];
831
- return isRecord(current) ? current[innerKey] : undefined;
832
- },
833
- set: (newVal) => {
834
- const current = value[index];
835
- if (isRecord(current)) {
836
- current[innerKey] = newVal;
837
- }
838
- const updatedElement = value[index];
839
- const originalElement = this.original[key][index];
840
- this.setArrayDirty(key, index, this.computeDirtyState(updatedElement, originalElement));
841
- this.touched[key] = true;
842
- this.validateField(key);
843
- this.validateDependentFields(key);
844
- }
845
- }),
846
- errors: this.getArrayItemFieldErrors(key, index, innerKey),
847
- dirty: this.getArrayItemDirty(key, index, innerKey),
848
- touched: this.touched[key] || false
849
- };
1427
+ props[key] = this.getOrCreateArrayWrappers(key, value);
1428
+ continue;
1429
+ }
1430
+ if (isPropertyAwareObject(value)) {
1431
+ props[key] = this.createObjectWrapperFromShape(key, value, (path) => {
1432
+ let current = this.state[key];
1433
+ for (const segment of path) {
1434
+ if (!isRecord(current)) {
1435
+ return undefined;
850
1436
  }
851
- return elementProps;
1437
+ current = current[segment];
1438
+ }
1439
+ return current;
1440
+ }, (path, newValue) => {
1441
+ const current = this.state[key];
1442
+ if (!isRecord(current)) {
1443
+ return;
1444
+ }
1445
+ const segments = [...path];
1446
+ const last = segments.pop();
1447
+ if (last === undefined) {
1448
+ return;
852
1449
  }
853
- return {
854
- value: {
855
- model: computed({
856
- get: () => value[index],
857
- set: (newVal) => {
858
- value[index] = newVal;
859
- const updatedValue = value[index];
860
- const originalValue = this.original[key][index];
861
- this.setArrayDirty(key, index, this.computeDirtyState(updatedValue, originalValue));
862
- this.touched[key] = true;
863
- this.validateField(key);
864
- this.validateDependentFields(key);
865
- }
866
- }),
867
- errors: this.getArrayItemErrorMessages(key, index),
868
- dirty: this.getArrayItemDirtyValue(key, index),
869
- touched: this.touched[key] || false
1450
+ let record = current;
1451
+ for (const segment of segments) {
1452
+ if (!isRecord(record[segment])) {
1453
+ return;
870
1454
  }
871
- };
872
- });
873
- props[key] = arrayProps;
1455
+ record = record[segment];
1456
+ }
1457
+ record[last] = newValue;
1458
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
1459
+ this.touched[key] = true;
1460
+ this.validateFieldPreservingNestedErrors(key, path);
1461
+ this.validateDependentFields(key);
1462
+ }, (path) => this.getObjectFieldErrors(String(key), path), (path) => this.getNestedDirtyValue(this.dirty[key], path), () => this.touched[key] || false);
874
1463
  continue;
875
1464
  }
876
- props[key] = {
877
- model: this._model[key],
878
- errors: this.getFieldErrors(key),
879
- dirty: this.dirty[key] || false,
880
- touched: this.touched[key] || false
881
- };
1465
+ props[key] = this.createFieldProperty(() => this._model[key].value, (newValue) => {
1466
+ this._model[key].value = newValue;
1467
+ }, () => this.getFieldErrors(key), () => this.dirty[key] || false, () => this.touched[key] || false);
882
1468
  }
883
1469
  return props;
884
1470
  }
@@ -889,42 +1475,10 @@ export class BaseForm {
889
1475
  */
890
1476
  isDirty(field) {
891
1477
  if (field !== undefined) {
892
- const dirtyState = this.dirty[field];
893
- if (typeof dirtyState === 'boolean') {
894
- return dirtyState;
895
- }
896
- if (Array.isArray(dirtyState)) {
897
- return dirtyState.some((item) => {
898
- if (typeof item === 'boolean') {
899
- return item;
900
- }
901
- if (item && typeof item === 'object') {
902
- return Object.values(item).some((v) => v === true);
903
- }
904
- return false;
905
- });
906
- }
907
- if (dirtyState && typeof dirtyState === 'object') {
908
- return Object.values(dirtyState).some((v) => v === true);
909
- }
910
- return false;
1478
+ return this.getNestedDirtyValue(this.dirty[field], []);
911
1479
  }
912
1480
  for (const key in this.dirty) {
913
- const dirtyState = this.dirty[key];
914
- if (typeof dirtyState === 'boolean' && dirtyState) {
915
- return true;
916
- }
917
- if (Array.isArray(dirtyState)) {
918
- for (const item of dirtyState) {
919
- if (typeof item === 'boolean' && item) {
920
- return true;
921
- }
922
- if (item && typeof item === 'object' && Object.values(item).some((v) => v === true)) {
923
- return true;
924
- }
925
- }
926
- }
927
- if (dirtyState && typeof dirtyState === 'object' && Object.values(dirtyState).some((v) => v === true)) {
1481
+ if (this.getNestedDirtyValue(this.dirty[key], [])) {
928
1482
  return true;
929
1483
  }
930
1484
  }
@@ -937,6 +1491,21 @@ export class BaseForm {
937
1491
  hasErrors() {
938
1492
  return this._hasErrors.value;
939
1493
  }
1494
+ getErrors() {
1495
+ return this.flattenErrors();
1496
+ }
1497
+ hasErrorsInGroup(group) {
1498
+ return Object.keys(this.flattenErrors()).some((errorKey) => this.errorKeyBelongsToGroup(errorKey, group));
1499
+ }
1500
+ getErrorsInGroup(group) {
1501
+ const groupErrors = {};
1502
+ for (const [errorKey, errorMessages] of Object.entries(this.flattenErrors())) {
1503
+ if (this.errorKeyBelongsToGroup(errorKey, group)) {
1504
+ groupErrors[errorKey] = cloneDeep(errorMessages);
1505
+ }
1506
+ }
1507
+ return groupErrors;
1508
+ }
940
1509
  /**
941
1510
  * Updates both the state and original value for a given property,
942
1511
  * keeping the field in a clean (not dirty) state.