@blueprint-ts/core 3.0.0 → 4.0.0-beta.1

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.
Files changed (206) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +25 -1
  3. package/docs/.vitepress/config.ts +80 -23
  4. package/docs/index.md +6 -63
  5. package/docs/{services/laravel → laravel}/pagination.md +19 -6
  6. package/docs/{services/laravel → laravel}/requests.md +2 -2
  7. package/docs/services/pagination/index.md +46 -0
  8. package/docs/services/pagination/infinite-scroller.md +19 -0
  9. package/docs/services/pagination/page-aware.md +46 -0
  10. package/docs/services/pagination/state-pagination.md +77 -0
  11. package/docs/services/pagination/updating-rows.md +36 -0
  12. package/docs/services/persistence/index.md +46 -0
  13. package/docs/services/requests/abort-requests.md +25 -0
  14. package/docs/services/requests/bulk-requests.md +70 -0
  15. package/docs/services/requests/drivers.md +50 -0
  16. package/docs/services/requests/error-handling.md +137 -0
  17. package/docs/services/requests/events.md +31 -0
  18. package/docs/services/requests/getting-started.md +201 -0
  19. package/docs/services/requests/headers.md +40 -0
  20. package/docs/services/requests/loading.md +63 -0
  21. package/docs/services/requests/request-bodies.md +59 -0
  22. package/docs/services/requests/responses.md +34 -0
  23. package/docs/services/support/deferred-promise.md +63 -0
  24. package/docs/services/support/helpers.md +77 -0
  25. package/docs/services/support/index.md +6 -0
  26. package/docs/upgrading/v1-to-v2.md +64 -0
  27. package/docs/upgrading/v2-to-v3.md +52 -0
  28. package/docs/upgrading/v3-to-v4.md +171 -0
  29. package/docs/upgrading.md +0 -0
  30. package/docs/vue/composables/use-confirm-dialog.md +96 -0
  31. package/docs/vue/composables/use-global-checkbox.md +73 -0
  32. package/docs/vue/composables/use-is-empty.md +26 -0
  33. package/docs/vue/composables/use-is-open-from-var.md +32 -0
  34. package/docs/vue/composables/use-is-open.md +28 -0
  35. package/docs/vue/composables/use-model-wrapper.md +29 -0
  36. package/docs/vue/composables/use-on-open.md +26 -0
  37. package/docs/vue/forms/arrays.md +45 -0
  38. package/docs/vue/forms/errors.md +52 -0
  39. package/docs/vue/forms/index.md +99 -0
  40. package/docs/vue/forms/payloads.md +99 -0
  41. package/docs/vue/forms/persistence.md +19 -0
  42. package/docs/vue/forms/state-and-properties.md +26 -0
  43. package/docs/vue/forms/utilities.md +27 -0
  44. package/docs/vue/forms/validation.md +189 -0
  45. package/docs/vue/requests/loading.md +51 -0
  46. package/docs/vue/{requests → router}/route-resource-binding.md +33 -27
  47. package/docs/vue/state.md +27 -11
  48. package/package.json +9 -10
  49. package/release-tool.json +22 -3
  50. package/src/{service/bulkRequests → bulkRequests}/BulkRequestSender.ts +29 -17
  51. package/src/{service/bulkRequests → bulkRequests}/BulkRequestWrapper.ts +5 -5
  52. package/src/laravel/pagination/dataDrivers/RequestDriver.ts +30 -0
  53. package/src/laravel/pagination/index.ts +6 -0
  54. package/src/{service/pagination → pagination}/BasePaginator.ts +35 -0
  55. package/src/{service/pagination → pagination}/InfiniteScroller.ts +1 -0
  56. package/src/{service/pagination → pagination}/PageAwarePaginator.ts +19 -21
  57. package/src/{service/pagination → pagination}/StatePaginator.ts +2 -8
  58. package/src/{service/pagination → pagination}/index.ts +1 -1
  59. package/src/{service/requests → requests}/BaseRequest.ts +2 -2
  60. package/src/requests/ErrorHandler.ts +144 -0
  61. package/src/requests/RequestErrorRouter.ts +89 -0
  62. package/src/{service/requests → requests}/bodies/FormDataBody.ts +10 -6
  63. package/src/requests/exceptions/BadGatewayException.ts +3 -0
  64. package/src/requests/exceptions/BadRequestException.ts +3 -0
  65. package/src/requests/exceptions/ConflictException.ts +3 -0
  66. package/src/requests/exceptions/ForbiddenException.ts +3 -0
  67. package/src/requests/exceptions/GatewayTimeoutException.ts +3 -0
  68. package/src/requests/exceptions/GoneException.ts +3 -0
  69. package/src/requests/exceptions/InvalidJsonException.ts +15 -0
  70. package/src/requests/exceptions/LockedException.ts +3 -0
  71. package/src/requests/exceptions/MethodNotAllowedException.ts +3 -0
  72. package/src/requests/exceptions/NotImplementedException.ts +3 -0
  73. package/src/requests/exceptions/PayloadTooLargeException.ts +3 -0
  74. package/src/requests/exceptions/PreconditionFailedException.ts +3 -0
  75. package/src/requests/exceptions/RequestTimeoutException.ts +3 -0
  76. package/src/requests/exceptions/ServiceUnavailableException.ts +3 -0
  77. package/src/requests/exceptions/TooManyRequestsException.ts +3 -0
  78. package/src/requests/exceptions/UnsupportedMediaTypeException.ts +3 -0
  79. package/src/requests/exceptions/index.ts +51 -0
  80. package/src/requests/factories/FormDataFactory.ts +14 -0
  81. package/src/{service/requests → requests}/index.ts +2 -2
  82. package/src/{service/support → support}/DeferredPromise.ts +1 -1
  83. package/src/support/index.ts +4 -0
  84. package/src/vue/composables/useConfirmDialog.ts +5 -1
  85. package/src/vue/composables/useModelWrapper.ts +3 -0
  86. package/src/vue/forms/BaseForm.ts +491 -393
  87. package/src/vue/forms/PropertyAwareArray.ts +2 -2
  88. package/src/vue/forms/index.ts +4 -4
  89. package/src/vue/forms/validation/index.ts +5 -2
  90. package/src/vue/forms/validation/rules/ConfirmedRule.ts +3 -3
  91. package/src/vue/forms/validation/rules/EmailRule.ts +23 -0
  92. package/src/vue/forms/validation/rules/JsonRule.ts +28 -0
  93. package/src/vue/forms/validation/types/BidirectionalRule.ts +2 -2
  94. package/src/vue/forms/validation/types/ValidationRules.ts +15 -0
  95. package/src/vue/index.ts +3 -3
  96. package/src/vue/requests/factories/VueRequestLoaderFactory.ts +3 -2
  97. package/src/vue/requests/loaders/VueRequestBatchLoader.ts +6 -1
  98. package/src/vue/requests/loaders/VueRequestLoader.ts +1 -1
  99. package/src/vue/router/routeResourceBinding/types.ts +3 -3
  100. package/src/vue/state/State.ts +38 -50
  101. package/tests/service/helpers/mergeDeep.test.ts +1 -1
  102. package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +3 -3
  103. package/tests/service/laravel/requests/JsonBaseRequest.test.ts +4 -4
  104. package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +3 -3
  105. package/tests/service/laravel/requests/responses/JsonResponse.test.ts +2 -2
  106. package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +2 -2
  107. package/tests/service/pagination/dtos/PaginationDataDto.test.ts +1 -1
  108. package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +2 -2
  109. package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +1 -1
  110. package/tests/service/requests/ErrorHandler.test.ts +61 -58
  111. package/tests/service/requests/FormDataBody.test.ts +1 -1
  112. package/tests/vue/forms/BaseForm.behavior.test.ts +98 -0
  113. package/docs/.vitepress/theme/Layout.vue +0 -14
  114. package/docs/.vitepress/theme/components/VersionSelector.vue +0 -64
  115. package/docs/.vitepress/theme/index.js +0 -13
  116. package/docs/services/requests/index.md +0 -74
  117. package/docs/vue/forms.md +0 -477
  118. package/examples/files/7z2404-x64.exe +0 -0
  119. package/examples/index.html +0 -14
  120. package/examples/js/app.js +0 -8
  121. package/examples/js/router.js +0 -22
  122. package/examples/js/view/App.vue +0 -49
  123. package/examples/js/view/layout/DemoPage.vue +0 -28
  124. package/examples/js/view/pagination/Pagination.vue +0 -28
  125. package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +0 -71
  126. package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +0 -54
  127. package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +0 -50
  128. package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +0 -57
  129. package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +0 -50
  130. package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +0 -63
  131. package/examples/js/view/requests/Requests.vue +0 -34
  132. package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +0 -36
  133. package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +0 -25
  134. package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +0 -15
  135. package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +0 -44
  136. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +0 -34
  137. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +0 -59
  138. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +0 -21
  139. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +0 -53
  140. package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +0 -4
  141. package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +0 -32
  142. package/src/service/laravel/pagination/index.ts +0 -7
  143. package/src/service/requests/ErrorHandler.ts +0 -64
  144. package/src/service/requests/exceptions/index.ts +0 -19
  145. package/src/service/requests/factories/FormDataFactory.ts +0 -9
  146. package/src/service/support/index.ts +0 -3
  147. /package/src/{service/bulkRequests → bulkRequests}/BulkRequestEvent.enum.ts +0 -0
  148. /package/src/{service/bulkRequests → bulkRequests}/index.ts +0 -0
  149. /package/src/{service/laravel → laravel}/pagination/contracts/PaginationResponseBodyContract.ts +0 -0
  150. /package/src/{service/laravel → laravel}/requests/JsonBaseRequest.ts +0 -0
  151. /package/src/{service/laravel → laravel}/requests/PaginationJsonBaseRequest.ts +0 -0
  152. /package/src/{service/laravel → laravel}/requests/index.ts +0 -0
  153. /package/src/{service/laravel → laravel}/requests/responses/JsonResponse.ts +0 -0
  154. /package/src/{service/laravel → laravel}/requests/responses/PaginationResponse.ts +0 -0
  155. /package/src/{service/pagination → pagination}/Paginator.ts +0 -0
  156. /package/src/{service/pagination → pagination}/contracts/BaseViewDriverContract.ts +0 -0
  157. /package/src/{service/pagination → pagination}/contracts/BaseViewDriverFactoryContract.ts +0 -0
  158. /package/src/{service/pagination → pagination}/contracts/PaginateableRequestContract.ts +0 -0
  159. /package/src/{service/pagination → pagination}/contracts/PaginationDataDriverContract.ts +0 -0
  160. /package/src/{service/pagination → pagination}/contracts/PaginationResponseContract.ts +0 -0
  161. /package/src/{service/pagination → pagination}/contracts/PaginatorLoadDataOptions.ts +0 -0
  162. /package/src/{service/pagination → pagination}/contracts/StatePaginationDataDriverContract.ts +0 -0
  163. /package/src/{service/pagination → pagination}/contracts/ViewDriverContract.ts +0 -0
  164. /package/src/{service/pagination → pagination}/contracts/ViewDriverFactoryContract.ts +0 -0
  165. /package/src/{service/pagination → pagination}/dataDrivers/ArrayDriver.ts +0 -0
  166. /package/src/{service/pagination → pagination}/dtos/PaginationDataDto.ts +0 -0
  167. /package/src/{service/pagination → pagination}/dtos/StatePaginationDataDto.ts +0 -0
  168. /package/src/{service/pagination → pagination}/factories/VueBaseViewDriverFactory.ts +0 -0
  169. /package/src/{service/pagination → pagination}/factories/VuePaginationDriverFactory.ts +0 -0
  170. /package/src/{service/pagination → pagination}/frontendDrivers/VueBaseViewDriver.ts +0 -0
  171. /package/src/{service/pagination → pagination}/frontendDrivers/VuePaginationDriver.ts +0 -0
  172. /package/src/{service/persistenceDrivers → persistenceDrivers}/LocalStorageDriver.ts +0 -0
  173. /package/src/{service/persistenceDrivers → persistenceDrivers}/NonPersistentDriver.ts +0 -0
  174. /package/src/{service/persistenceDrivers → persistenceDrivers}/SessionStorageDriver.ts +0 -0
  175. /package/src/{service/persistenceDrivers → persistenceDrivers}/index.ts +0 -0
  176. /package/src/{service/persistenceDrivers → persistenceDrivers}/types/PersistenceDriver.ts +0 -0
  177. /package/src/{service/requests → requests}/RequestEvents.enum.ts +0 -0
  178. /package/src/{service/requests → requests}/RequestMethod.enum.ts +0 -0
  179. /package/src/{service/requests → requests}/bodies/JsonBody.ts +0 -0
  180. /package/src/{service/requests → requests}/contracts/AbortableRequestContract.ts +0 -0
  181. /package/src/{service/requests → requests}/contracts/BaseRequestContract.ts +0 -0
  182. /package/src/{service/requests → requests}/contracts/BodyContract.ts +0 -0
  183. /package/src/{service/requests → requests}/contracts/BodyFactoryContract.ts +0 -0
  184. /package/src/{service/requests → requests}/contracts/DriverConfigContract.ts +0 -0
  185. /package/src/{service/requests → requests}/contracts/HeadersContract.ts +0 -0
  186. /package/src/{service/requests → requests}/contracts/RequestDriverContract.ts +0 -0
  187. /package/src/{service/requests → requests}/contracts/RequestLoaderContract.ts +0 -0
  188. /package/src/{service/requests → requests}/contracts/RequestLoaderFactoryContract.ts +0 -0
  189. /package/src/{service/requests → requests}/contracts/ResponseContract.ts +0 -0
  190. /package/src/{service/requests → requests}/drivers/contracts/ResponseHandlerContract.ts +0 -0
  191. /package/src/{service/requests → requests}/drivers/fetch/FetchDriver.ts +0 -0
  192. /package/src/{service/requests → requests}/drivers/fetch/FetchResponse.ts +0 -0
  193. /package/src/{service/requests → requests}/exceptions/NoResponseReceivedException.ts +0 -0
  194. /package/src/{service/requests → requests}/exceptions/NotFoundException.ts +0 -0
  195. /package/src/{service/requests → requests}/exceptions/PageExpiredException.ts +0 -0
  196. /package/src/{service/requests → requests}/exceptions/ResponseBodyException.ts +0 -0
  197. /package/src/{service/requests → requests}/exceptions/ResponseException.ts +0 -0
  198. /package/src/{service/requests → requests}/exceptions/ServerErrorException.ts +0 -0
  199. /package/src/{service/requests → requests}/exceptions/UnauthorizedException.ts +0 -0
  200. /package/src/{service/requests → requests}/exceptions/ValidationException.ts +0 -0
  201. /package/src/{service/requests → requests}/factories/JsonBodyFactory.ts +0 -0
  202. /package/src/{service/requests → requests}/responses/BaseResponse.ts +0 -0
  203. /package/src/{service/requests → requests}/responses/BlobResponse.ts +0 -0
  204. /package/src/{service/requests → requests}/responses/JsonResponse.ts +0 -0
  205. /package/src/{service/requests → requests}/responses/PlainTextResponse.ts +0 -0
  206. /package/src/{helpers.ts → support/helpers.ts} +0 -0
@@ -2,60 +2,110 @@ import { reactive, computed, toRaw, type ComputedRef, watch } from 'vue'
2
2
  import { camelCase, upperFirst, cloneDeep, isEqual } from 'lodash-es'
3
3
  import isEqualWith from 'lodash-es/isEqualWith'
4
4
  import { type PersistedForm } from './types/PersistedForm'
5
- import { NonPersistentDriver } from '../../service/persistenceDrivers/NonPersistentDriver'
6
- import { type PersistenceDriver } from '../../service/persistenceDrivers/types/PersistenceDriver'
5
+ import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver'
6
+ import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver'
7
7
  import { PropertyAwareArray } from './PropertyAwareArray'
8
- import { BaseRule } from './validation/rules/BaseRule'
9
- import { type BidirectionalRule } from './validation/types/BidirectionalRule'
10
- import { ValidationMode } from './validation'
8
+ import { ValidationMode, type ValidationRules } from './validation'
11
9
 
12
- export function propertyAwareToRaw<T>(propertyAwareObject: any): T {
13
- // Prüfe, ob es sich um ein Array handelt
10
+ type DirtyObject = Record<string, boolean>
11
+ type DirtyArray = Array<boolean | DirtyObject>
12
+ type DirtyState = boolean | DirtyObject | DirtyArray
13
+ type DirtyMap<FormBody extends object> = Record<keyof FormBody, DirtyState>
14
+
15
+ type ErrorMessages = string[]
16
+ interface ErrorObject {
17
+ [key: string]: FieldErrors
18
+ }
19
+ type ErrorArray = Array<ErrorObject>
20
+ type FieldErrors = ErrorMessages | ErrorObject | ErrorArray
21
+ type ErrorBag = Record<string, FieldErrors>
22
+
23
+ type FieldProperty<T> = {
24
+ model: ComputedRef<T>
25
+ errors: ErrorMessages
26
+ dirty: boolean
27
+ touched: boolean
28
+ }
29
+
30
+ type FormKey<FormBody extends object> = Extract<keyof FormBody, string>
31
+ type RequestKey<RequestBody extends object> = Extract<keyof RequestBody, string>
32
+ type ArrayItem<T> = T extends Array<infer Item> ? Item : never
33
+ type PropertyAwareInput<T> = T extends PropertyAwareArray<infer Item> ? Array<Item> | PropertyAwareArray<Item> : T
34
+
35
+ type FormProperties<FormBody extends object> = {
36
+ [K in keyof FormBody]: FormBody[K] extends PropertyAwareArray<infer Item>
37
+ ? Array<Item extends object ? { [P in keyof Item]: FieldProperty<Item[P]> } : { value: FieldProperty<Item> }>
38
+ : FieldProperty<FormBody[K]>
39
+ }
40
+
41
+ function isRecord(value: unknown): value is Record<string, unknown> {
42
+ return !!value && typeof value === 'object' && !Array.isArray(value)
43
+ }
44
+
45
+ function isErrorMessages(value: unknown): value is ErrorMessages {
46
+ return Array.isArray(value) && (value.length === 0 || typeof value[0] === 'string')
47
+ }
48
+
49
+ function isErrorArray(value: unknown): value is ErrorArray {
50
+ return Array.isArray(value) && value.length > 0 && isRecord(value[0])
51
+ }
52
+
53
+ function isErrorObject(value: unknown): value is ErrorObject {
54
+ return isRecord(value)
55
+ }
56
+
57
+ export function propertyAwareToRaw<T>(propertyAwareObject: T): T {
14
58
  if (Array.isArray(propertyAwareObject)) {
15
59
  return propertyAwareObject.map((item) => propertyAwareToRaw(item)) as T
16
60
  }
17
61
 
18
- // Wenn es kein Objekt ist oder null/undefined, direkt zurückgeben
19
- if (!propertyAwareObject || typeof propertyAwareObject !== 'object') {
20
- return propertyAwareObject as T
62
+ if (!isRecord(propertyAwareObject)) {
63
+ return propertyAwareObject
21
64
  }
22
65
 
23
- const result: any = {}
66
+ const result: Record<string, unknown> = {}
67
+ const record = propertyAwareObject
24
68
 
25
- for (const key in propertyAwareObject) {
26
- // Überspringen von prototyp-eigenschaften oder speziellen Eigenschaften
27
- if (!Object.prototype.hasOwnProperty.call(propertyAwareObject, key) || key.startsWith('_')) {
69
+ for (const key in record) {
70
+ if (!Object.prototype.hasOwnProperty.call(record, key) || key.startsWith('_')) {
28
71
  continue
29
72
  }
30
73
 
31
- // Prüfen, ob die Eigenschaft ein model.value-Objekt hat
32
- if (propertyAwareObject[key]?.model?.value !== undefined) {
33
- result[key] = propertyAwareObject[key].model.value
34
- } else if (propertyAwareObject[key] && typeof propertyAwareObject[key] === 'object') {
35
- // Rekursiv für verschachtelte Objekte oder Arrays
36
- result[key] = propertyAwareToRaw(propertyAwareObject[key])
37
- } else {
38
- // Fallback für den Fall, dass es sich nicht um ein property-aware Feld handelt
39
- result[key] = propertyAwareObject[key]
74
+ const value = record[key]
75
+ const modelValue = isRecord(value) ? (value as { model?: { value?: unknown } }).model?.value : undefined
76
+
77
+ if (modelValue !== undefined) {
78
+ result[key] = modelValue
79
+ continue
80
+ }
81
+
82
+ if (value && typeof value === 'object') {
83
+ result[key] = propertyAwareToRaw(value)
84
+ continue
40
85
  }
86
+
87
+ result[key] = value
41
88
  }
42
89
 
43
90
  return result as T
44
91
  }
45
92
 
46
93
  /** Helper: shallow-merge source object into target. */
47
- function shallowMerge(target: any, source: any): any {
94
+ function shallowMerge<T extends object, U extends object>(target: T, source: U): T & U {
48
95
  Object.assign(target, source)
49
- return target
96
+ return target as T & U
50
97
  }
51
98
 
52
99
  /** Helper: if both values are arrays, update each element if possible, otherwise replace. */
53
- function deepMergeArrays(target: any[], source: any[]): any[] {
100
+ function deepMergeArrays<T>(target: T[], source: T[]): T[] {
54
101
  if (target.length !== source.length) {
55
102
  return source
56
103
  }
57
104
  return target.map((t, i) => {
58
105
  const s = source[i]
106
+ if (s === undefined) {
107
+ return t
108
+ }
59
109
  if (t && typeof t === 'object' && s && typeof s === 'object') {
60
110
  return shallowMerge({ ...t }, s)
61
111
  }
@@ -71,18 +121,20 @@ function restorePropertyAwareArrays<FormBody>(defaults: FormBody, state: FormBod
71
121
  for (const key in defaults) {
72
122
  const defVal = defaults[key]
73
123
  if (defVal instanceof PropertyAwareArray) {
74
- // If state[key] is not an instance, assume it's a plain array and rewrap it.
75
124
  if (!(state[key] instanceof PropertyAwareArray)) {
76
- state[key] = new PropertyAwareArray(Array.isArray(state[key]) ? (state[key] as any) : []) as any
125
+ const values = Array.isArray(state[key]) ? Array.from(state[key]) : []
126
+ state[key] = new PropertyAwareArray(values) as FormBody[typeof key]
77
127
  }
78
128
  }
79
129
  }
80
130
  }
81
131
 
82
- function propertyAwareDeepEqual(a: any, b: any): boolean {
83
- // A helper that returns the "inner" value if it looks like a PropertyAwareArray.
84
- const getInner = (val: any) => {
85
- // If val is an instance of PropertyAwareArray, it's already an array
132
+ /**
133
+ * Compare values while treating PropertyAwareArray as its underlying array.
134
+ * This avoids false negatives when comparing persisted state to defaults.
135
+ */
136
+ function propertyAwareDeepEqual<T>(a: T, b: T): boolean {
137
+ const getInner = (val: unknown) => {
86
138
  if (val instanceof PropertyAwareArray) {
87
139
  return val
88
140
  }
@@ -92,7 +144,6 @@ function propertyAwareDeepEqual(a: any, b: any): boolean {
92
144
  return isEqualWith(a, b, (aValue, bValue) => {
93
145
  const normA = getInner(aValue)
94
146
  const normB = getInner(bValue)
95
- // If either normalization changed the value, compare the normalized ones.
96
147
  if (normA !== aValue || normB !== bValue) {
97
148
  return isEqual(normA, normB)
98
149
  }
@@ -104,31 +155,23 @@ function propertyAwareDeepEqual(a: any, b: any): boolean {
104
155
  * A generic base class for forms.
105
156
  *
106
157
  * @template RequestBody - The final payload shape (what is sent to the server).
107
- * @template FormBody - The raw form data shape (before any mutators are applied).
158
+ * @template FormBody - The raw form data shape (before mutators are applied).
108
159
  *
109
160
  * (We assume that for every key in RequestBody there is a corresponding key in FormBody.)
110
161
  */
111
162
  export abstract class BaseForm<RequestBody extends object, FormBody extends object> {
112
163
  public readonly state: FormBody
113
- private readonly dirty: Record<keyof FormBody, boolean | any[]>
164
+ private readonly dirty: DirtyMap<FormBody>
114
165
  private readonly touched: Record<keyof FormBody, boolean>
115
166
  private readonly original: FormBody
116
167
  private readonly _model: { [K in keyof FormBody]: ComputedRef<FormBody[K]> }
117
- private _errors: any = reactive({})
168
+ private _errors: ErrorBag = reactive<ErrorBag>({})
118
169
  private _hasErrors: ComputedRef<boolean>
119
170
  protected append: string[] = []
120
171
  protected ignore: string[] = []
121
172
  protected errorMap: { [serverKey: string]: string | string[] } = {}
122
173
 
123
- protected rules: Partial<
124
- Record<
125
- keyof FormBody,
126
- {
127
- rules: BaseRule<any>[]
128
- options?: { mode?: ValidationMode }
129
- }
130
- >
131
- > = {}
174
+ protected rules: ValidationRules<FormBody> = {}
132
175
 
133
176
  private fieldDependencies: Map<keyof FormBody, Set<keyof FormBody>> = new Map()
134
177
 
@@ -137,6 +180,7 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
137
180
  * The default is a NonPersistentDriver.
138
181
  * Child classes can override this method to return a different driver.
139
182
  */
183
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
140
184
  protected getPersistenceDriver(_suffix: string | undefined): PersistenceDriver {
141
185
  return new NonPersistentDriver()
142
186
  }
@@ -145,7 +189,7 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
145
189
  * Helper: recursively computes the dirty state for a value based on the original.
146
190
  * For plain arrays we compare the entire array (a single flag), not each element.
147
191
  */
148
- private computeDirtyState(current: any, original: any): any {
192
+ private computeDirtyState<T>(current: T, original: T): DirtyState {
149
193
  if (Array.isArray(current) && Array.isArray(original)) {
150
194
  return current.length !== original.length || !isEqual(current, original)
151
195
  } else if (current && typeof current === 'object' && original && typeof original === 'object') {
@@ -160,6 +204,79 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
160
204
  return !isEqual(current, original)
161
205
  }
162
206
 
207
+ private initDirtyTouched(defaults: FormBody): {
208
+ dirty: DirtyMap<FormBody>
209
+ touched: Record<keyof FormBody, boolean>
210
+ } {
211
+ const initDirty: Partial<DirtyMap<FormBody>> = {}
212
+ const initTouched: Partial<Record<keyof FormBody, boolean>> = {}
213
+
214
+ for (const key in defaults) {
215
+ const value = defaults[key]
216
+ if (value instanceof PropertyAwareArray) {
217
+ initDirty[key as keyof FormBody] = Array.from(value).map((item) => {
218
+ if (item && typeof item === 'object') {
219
+ const obj: Record<string, boolean> = {}
220
+ for (const k in item) {
221
+ if (Object.prototype.hasOwnProperty.call(item, k)) {
222
+ obj[k] = false
223
+ }
224
+ }
225
+ return obj
226
+ }
227
+ return false
228
+ })
229
+ } else {
230
+ initDirty[key as keyof FormBody] = false
231
+ }
232
+
233
+ initTouched[key as keyof FormBody] = false
234
+ }
235
+
236
+ return {
237
+ dirty: reactive(initDirty) as DirtyMap<FormBody>,
238
+ touched: reactive(initTouched) as Record<keyof FormBody, boolean>
239
+ }
240
+ }
241
+
242
+ private replacePropertyAwareArray<K extends keyof FormBody>(key: K, values: Array<ArrayItem<FormBody[K]>>): void {
243
+ const current = this.state[key]
244
+ if (current instanceof PropertyAwareArray) {
245
+ current.length = 0
246
+ values.forEach((item) => current.push(item))
247
+ return
248
+ }
249
+
250
+ this.state[key] = new PropertyAwareArray(values) as FormBody[K]
251
+ }
252
+
253
+ private persistState(driver?: PersistenceDriver): void {
254
+ if (this.options?.persist === false) {
255
+ return
256
+ }
257
+
258
+ const persistDriver = driver ?? this.getPersistenceDriver(this.options?.persistSuffix)
259
+ persistDriver.set(this.constructor.name, {
260
+ state: toRaw(this.state),
261
+ original: toRaw(this.original),
262
+ dirty: toRaw(this.dirty),
263
+ touched: toRaw(this.touched)
264
+ } as PersistedForm<FormBody>)
265
+ }
266
+
267
+ private markFieldUpdated(key: keyof FormBody, driver?: PersistenceDriver): void {
268
+ this.touched[key] = true
269
+ this.validateField(key)
270
+ this.validateDependentFields(key)
271
+ this.persistState(driver)
272
+ }
273
+
274
+ private updateField<K extends keyof FormBody>(key: K, value: FormBody[K], driver?: PersistenceDriver): void {
275
+ this.state[key] = value
276
+ this.dirty[key] = this.computeDirtyState(value, this.original[key])
277
+ this.markFieldUpdated(key, driver)
278
+ }
279
+
163
280
  /**
164
281
  * Build a map of field dependencies based on the rules
165
282
  * This identifies which fields need to be revalidated when another field changes
@@ -170,9 +287,7 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
170
287
  const fieldRules = this.rules[field as keyof FormBody]?.rules || []
171
288
 
172
289
  for (const rule of fieldRules) {
173
- // Process normal dependencies from dependsOn array
174
290
  for (const dependencyField of rule.dependsOn) {
175
- // Add this field as dependent on dependencyField
176
291
  if (!this.fieldDependencies.has(dependencyField as keyof FormBody)) {
177
292
  this.fieldDependencies.set(dependencyField as keyof FormBody, new Set())
178
293
  }
@@ -180,18 +295,14 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
180
295
  this.fieldDependencies.get(dependencyField as keyof FormBody)?.add(field as keyof FormBody)
181
296
  }
182
297
 
183
- // Process bidirectional dependencies if rule implements BidirectionalRule
184
- if ('getBidirectionalFields' in rule && typeof (rule as any).getBidirectionalFields === 'function') {
185
- const bidirectionalRule = rule as unknown as BidirectionalRule
186
- const bidirectionalFields = bidirectionalRule.getBidirectionalFields()
187
-
298
+ const bidirectionalFields = rule.getBidirectionalFields?.()
299
+ if (bidirectionalFields && bidirectionalFields.length > 0) {
188
300
  for (const bidirectionalField of bidirectionalFields) {
189
- // Add bidirectional dependency from this field to the other field
190
301
  if (!this.fieldDependencies.has(field as keyof FormBody)) {
191
302
  this.fieldDependencies.set(field as keyof FormBody, new Set())
192
303
  }
193
304
 
194
- this.fieldDependencies.get(field as keyof FormBody)?.add(bidirectionalField as unknown as keyof FormBody)
305
+ this.fieldDependencies.get(field as keyof FormBody)?.add(bidirectionalField)
195
306
  }
196
307
  }
197
308
  }
@@ -208,7 +319,6 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
208
319
  if (dependentFields) {
209
320
  const fieldsToValidate = new Set<keyof FormBody>(dependentFields)
210
321
 
211
- // For bidirectional dependencies
212
322
  for (const field of dependentFields) {
213
323
  const fieldDeps = this.fieldDependencies.get(field)
214
324
  if (fieldDeps && fieldDeps.has(changedField)) {
@@ -217,7 +327,6 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
217
327
  }
218
328
  }
219
329
 
220
- // Validate dependent fields
221
330
  for (const field of fieldsToValidate) {
222
331
  this.validateField(field, {
223
332
  isDependentChange: true,
@@ -240,144 +349,56 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
240
349
  if (persisted && propertyAwareDeepEqual(defaults, persisted.original)) {
241
350
  initialData = persisted.state
242
351
  this.original = cloneDeep(persisted.original)
243
- this.dirty = reactive(persisted.dirty) as Record<keyof FormBody, boolean | any[]>
352
+ this.dirty = reactive(persisted.dirty) as DirtyMap<FormBody>
244
353
  this.touched = reactive(persisted.touched || {}) as Record<keyof FormBody, boolean>
245
- // Rewrap persisted values that were originally PropertyAwareArrays:
246
354
  restorePropertyAwareArrays(defaults, initialData)
247
355
  restorePropertyAwareArrays(defaults, this.original)
248
356
  } else {
249
357
  console.log('Discarding persisted data for ' + this.constructor.name + " because it doesn't match the defaults.")
250
358
  initialData = defaults
251
359
  this.original = cloneDeep(defaults)
252
- const initDirty: Partial<Record<keyof FormBody, boolean | any[]>> = {}
253
- const initTouched: Partial<Record<keyof FormBody, boolean>> = {}
254
- for (const key in defaults) {
255
- const value = defaults[key]
256
- // Initialize dirty state
257
- if (value instanceof PropertyAwareArray) {
258
- initDirty[key as keyof FormBody] = ([...value] as any[]).map((item) => {
259
- if (item && typeof item === 'object') {
260
- const obj: Record<string, boolean> = {}
261
- for (const k in item) {
262
- if (Object.prototype.hasOwnProperty.call(item, k)) {
263
- obj[k] = false
264
- }
265
- }
266
- return obj
267
- }
268
- return false
269
- })
270
- } else {
271
- initDirty[key as keyof FormBody] = false
272
- }
273
-
274
- // Initialize touched state
275
- initTouched[key as keyof FormBody] = false
276
- }
277
- this.dirty = reactive(initDirty) as Record<keyof FormBody, boolean | any[]>
278
- this.touched = reactive(initTouched) as Record<keyof FormBody, boolean>
360
+ const init = this.initDirtyTouched(defaults)
361
+ this.dirty = init.dirty
362
+ this.touched = init.touched
279
363
  driver.remove(this.constructor.name)
280
364
  }
281
365
  } else {
282
366
  initialData = defaults
283
367
  this.original = cloneDeep(defaults)
284
- const initDirty: Partial<Record<keyof FormBody, boolean | any[]>> = {}
285
- const initTouched: Partial<Record<keyof FormBody, boolean>> = {}
286
- for (const key in defaults) {
287
- const value = defaults[key]
288
- // Initialize dirty state
289
- if (value instanceof PropertyAwareArray) {
290
- initDirty[key as keyof FormBody] = ([...value] as any[]).map((item) => {
291
- if (item && typeof item === 'object') {
292
- const obj: Record<string, boolean> = {}
293
- for (const k in item) {
294
- if (Object.prototype.hasOwnProperty.call(item, k)) {
295
- obj[k] = false
296
- }
297
- }
298
- return obj
299
- }
300
- return false
301
- })
302
- } else {
303
- initDirty[key as keyof FormBody] = false
304
- }
305
-
306
- // Initialize touched state
307
- initTouched[key as keyof FormBody] = false
308
- }
309
- this.dirty = reactive(initDirty) as Record<keyof FormBody, boolean | any[]>
310
- this.touched = reactive(initTouched) as Record<keyof FormBody, boolean>
368
+ const init = this.initDirtyTouched(defaults)
369
+ this.dirty = init.dirty
370
+ this.touched = init.touched
311
371
  }
312
372
 
313
373
  this.rules = this.defineRules()
314
374
 
315
- // Build the field dependencies map before creating computed properties
316
375
  this.buildFieldDependencies()
317
376
 
318
377
  this.state = reactive(initialData) as FormBody
319
378
  this._model = {} as { [K in keyof FormBody]: ComputedRef<FormBody[K]> }
320
379
 
321
- // Create computed models.
322
380
  for (const key in this.state) {
323
381
  const value = this.state[key]
324
382
  if (value instanceof PropertyAwareArray) {
325
383
  this._model[key as keyof FormBody] = computed({
326
384
  get: () => this.state[key],
327
- set: (newVal: any) => {
328
- const arr = this.state[key] as PropertyAwareArray
329
- // Leere das Array und fülle es neu
330
- arr.length = 0
331
- if (Array.isArray(newVal)) {
332
- newVal.forEach((item) => arr.push(item))
333
- }
334
- this.dirty[key as keyof FormBody] = (newVal as any[]).map(() => false)
335
- this.touched[key as keyof FormBody] = true
336
-
337
- // Validate this field
338
- this.validateField(key as keyof FormBody)
339
-
340
- // Also validate any fields that depend on this field
341
- this.validateDependentFields(key as keyof FormBody)
342
-
343
- if (persist) {
344
- driver.set(this.constructor.name, {
345
- state: toRaw(this.state),
346
- original: toRaw(this.original),
347
- dirty: toRaw(this.dirty),
348
- touched: toRaw(this.touched)
349
- } as PersistedForm<FormBody>)
350
- }
385
+ set: (newVal: PropertyAwareInput<FormBody[typeof key]>) => {
386
+ const next = Array.isArray(newVal) ? Array.from(newVal) : []
387
+ this.replacePropertyAwareArray(key as keyof FormBody, next)
388
+ this.dirty[key as keyof FormBody] = next.map(() => false)
389
+ this.markFieldUpdated(key as keyof FormBody, driver)
351
390
  }
352
391
  })
353
392
  } else {
354
393
  this._model[key as keyof FormBody] = computed({
355
394
  get: () => this.state[key],
356
395
  set: (value: FormBody[typeof key]) => {
357
- this.state[key] = value
358
- this.dirty[key as keyof FormBody] = this.computeDirtyState(value, this.original[key])
359
- this.touched[key as keyof FormBody] = true
360
-
361
- // Validate this field
362
- this.validateField(key as keyof FormBody)
363
-
364
- // Also validate any fields that depend on this field
365
- this.validateDependentFields(key as keyof FormBody)
366
-
367
- if (persist) {
368
- driver.set(this.constructor.name, {
369
- state: toRaw(this.state),
370
- original: toRaw(this.original),
371
- dirty: toRaw(this.dirty),
372
- touched: toRaw(this.touched)
373
- } as PersistedForm<FormBody>)
374
- }
396
+ this.updateField(key as keyof FormBody, value, driver)
375
397
  }
376
398
  })
377
399
  }
378
400
  }
379
401
 
380
- // Add a deep watch for plain arrays to update the dirty state if in-place changes occur.
381
402
  for (const key in this.state) {
382
403
  const value = this.state[key]
383
404
  if (Array.isArray(value) && !(value instanceof PropertyAwareArray)) {
@@ -392,30 +413,23 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
392
413
  }
393
414
  }
394
415
 
395
- // Create computed property for checking errors
396
416
  this._hasErrors = computed(() => {
397
- // Check if any field has errors
398
417
  for (const field in this._errors) {
399
418
  if (Object.prototype.hasOwnProperty.call(this._errors, field)) {
400
419
  const fieldErrors = this._errors[field]
401
420
 
402
- // Handle string array errors
403
421
  if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
404
422
  return true
405
423
  }
406
424
 
407
- // Handle nested array errors
408
425
  if (fieldErrors && typeof fieldErrors === 'object') {
409
- // For arrays of objects with errors
410
426
  if (Array.isArray(fieldErrors)) {
411
427
  for (const item of fieldErrors) {
412
428
  if (item && typeof item === 'object' && Object.keys(item).length > 0) {
413
429
  return true
414
430
  }
415
431
  }
416
- }
417
- // For plain objects with errors
418
- else if (Object.keys(fieldErrors).length > 0) {
432
+ } else if (Object.keys(fieldErrors).length > 0) {
419
433
  return true
420
434
  }
421
435
  }
@@ -428,14 +442,7 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
428
442
  if (persist) {
429
443
  watch(
430
444
  () => this.state,
431
- () => {
432
- driver.set(this.constructor.name, {
433
- state: toRaw(this.state),
434
- original: toRaw(this.original),
435
- dirty: toRaw(this.dirty),
436
- touched: toRaw(this.touched)
437
- } as PersistedForm<FormBody>)
438
- },
445
+ () => this.persistState(driver),
439
446
  { deep: true, immediate: true }
440
447
  )
441
448
  }
@@ -443,18 +450,164 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
443
450
  this.validate()
444
451
  }
445
452
 
446
- protected defineRules(): { [K in keyof FormBody]?: { rules: BaseRule<any>[]; options?: { mode?: ValidationMode } } } {
453
+ protected defineRules(): ValidationRules<FormBody> {
447
454
  return {}
448
455
  }
449
456
 
450
- public fillErrors<ErrorInterface>(errorsData: ErrorInterface): void {
457
+ private clearErrors(): void {
451
458
  for (const key in this._errors) {
452
459
  delete this._errors[key]
453
460
  }
461
+ }
462
+
463
+ private getOrCreateErrorArray(key: string): ErrorArray {
464
+ const existing = this._errors[key]
465
+ if (isErrorArray(existing)) {
466
+ return existing
467
+ }
468
+
469
+ const next: ErrorArray = []
470
+ this._errors[key] = next
471
+ return next
472
+ }
473
+
474
+ private getOrCreateErrorObject(errors: ErrorArray, index: number): ErrorObject {
475
+ const existing = errors[index]
476
+ if (isErrorObject(existing)) {
477
+ return existing
478
+ }
479
+
480
+ const next: ErrorObject = {}
481
+ errors[index] = next
482
+ return next
483
+ }
484
+
485
+ private getFieldErrors(field: string): ErrorMessages {
486
+ const errors = this._errors[field]
487
+ return isErrorMessages(errors) ? errors : []
488
+ }
489
+
490
+ private getOrCreateFieldErrors(field: string): ErrorMessages {
491
+ const existing = this._errors[field]
492
+ if (isErrorMessages(existing)) {
493
+ return existing
494
+ }
495
+
496
+ const next: ErrorMessages = []
497
+ this._errors[field] = next
498
+ return next
499
+ }
500
+
501
+ private getArrayItemErrors(field: string, index: number): ErrorObject | undefined {
502
+ const errors = this._errors[field]
503
+ if (!isErrorArray(errors)) {
504
+ return undefined
505
+ }
506
+ const item = errors[index]
507
+ return isErrorObject(item) ? item : undefined
508
+ }
509
+
510
+ private getArrayItemFieldErrors(field: string, index: number, innerKey: string): ErrorMessages {
511
+ const itemErrors = this.getArrayItemErrors(field, index)
512
+ if (!itemErrors) {
513
+ return []
514
+ }
515
+
516
+ const fieldErrors = itemErrors[innerKey]
517
+ if (isErrorMessages(fieldErrors)) {
518
+ return fieldErrors
519
+ }
520
+
521
+ if (isErrorObject(fieldErrors)) {
522
+ const nestedErrors = fieldErrors['']
523
+ return isErrorMessages(nestedErrors) ? nestedErrors : []
524
+ }
525
+
526
+ return []
527
+ }
528
+
529
+ private getArrayItemErrorMessages(field: string, index: number): ErrorMessages {
530
+ const errors = this._errors[field]
531
+ if (!Array.isArray(errors)) {
532
+ return []
533
+ }
534
+ const item = errors[index]
535
+ if (isErrorMessages(item)) {
536
+ return item
537
+ }
538
+ if (isErrorObject(item)) {
539
+ const nestedErrors = item['']
540
+ return isErrorMessages(nestedErrors) ? nestedErrors : []
541
+ }
542
+ return []
543
+ }
544
+
545
+ private setArrayDirty(field: keyof FormBody, index: number, value: DirtyState): void {
546
+ const dirtyState = this.dirty[field]
547
+ if (Array.isArray(dirtyState)) {
548
+ dirtyState[index] = this.normalizeItemDirtyState(value)
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Collapse nested array dirty states into a single boolean/object for array entries.
554
+ */
555
+ private normalizeItemDirtyState(value: DirtyState): boolean | DirtyObject {
556
+ if (!Array.isArray(value)) {
557
+ return value
558
+ }
559
+
560
+ return value.some((entry) => {
561
+ if (typeof entry === 'boolean') {
562
+ return entry
563
+ }
564
+ if (isRecord(entry)) {
565
+ return Object.values(entry).some((item) => item === true)
566
+ }
567
+ return false
568
+ })
569
+ }
570
+
571
+ private getArrayItemDirty(field: keyof FormBody, index: number, innerKey: string): boolean {
572
+ const dirtyState = this.dirty[field]
573
+ if (!Array.isArray(dirtyState)) {
574
+ return false
575
+ }
576
+
577
+ const entry = dirtyState[index]
578
+ if (typeof entry === 'boolean') {
579
+ return entry
580
+ }
581
+
582
+ if (isRecord(entry)) {
583
+ return entry[innerKey] === true
584
+ }
585
+
586
+ return false
587
+ }
588
+
589
+ private getArrayItemDirtyValue(field: keyof FormBody, index: number): boolean {
590
+ const dirtyState = this.dirty[field]
591
+ if (!Array.isArray(dirtyState)) {
592
+ return false
593
+ }
594
+
595
+ const entry = dirtyState[index]
596
+ return typeof entry === 'boolean' ? entry : false
597
+ }
598
+
599
+ /**
600
+ * Map server-side errors (including dot-notation paths) into the form error bag.
601
+ */
602
+ public fillErrors<ErrorInterface extends Record<string, FieldErrors>>(errorsData: ErrorInterface): void {
603
+ this.clearErrors()
454
604
 
455
605
  for (const serverKey in errorsData) {
456
606
  if (Object.prototype.hasOwnProperty.call(errorsData, serverKey)) {
457
607
  const errorMessage = errorsData[serverKey]
608
+ if (errorMessage === undefined) {
609
+ continue
610
+ }
458
611
 
459
612
  let targetKeys: string[] = [serverKey]
460
613
 
@@ -466,24 +619,23 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
466
619
  for (const targetKey of targetKeys) {
467
620
  const parts = targetKey.split('.')
468
621
  if (parts.length > 1) {
469
- const topKey = parts[0]
470
- // @ts-ignore index could be NaN if part is not a number
471
- const index = parseInt(parts[1], 10)
622
+ const topKey = parts[0] ?? ''
623
+ const indexPart = parts[1] ?? ''
624
+ const index = Number.parseInt(indexPart, 10)
625
+ if (!topKey || !Number.isFinite(index)) {
626
+ this._errors[targetKey] = errorMessage
627
+ continue
628
+ }
472
629
  const errorSubKey = parts.slice(2).join('.')
473
630
 
474
- // @ts-ignore Dynamic property access
475
- if (!this._errors[topKey]) {
476
- // @ts-ignore Dynamic property access
477
- this._errors[topKey] = []
478
- }
479
- // @ts-ignore Dynamic property access, index could be NaN
480
- if (!this._errors[topKey][index]) {
481
- // @ts-ignore Dynamic property access, index could be NaN
482
- this._errors[topKey][index] = {}
483
- }
631
+ const errors = this.getOrCreateErrorArray(topKey)
632
+ const errorObject = this.getOrCreateErrorObject(errors, index)
484
633
 
485
- // @ts-ignore Dynamic property access, index could be NaN
486
- this._errors[topKey][index][errorSubKey] = errorMessage
634
+ if (errorSubKey.length === 0) {
635
+ errorObject[''] = errorMessage
636
+ } else {
637
+ errorObject[errorSubKey] = errorMessage
638
+ }
487
639
  } else {
488
640
  this._errors[targetKey] = errorMessage
489
641
  }
@@ -500,12 +652,10 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
500
652
  public touch(field: keyof FormBody): void {
501
653
  this.touched[field] = true
502
654
 
503
- // Get field config to check if we should validate on touch
504
655
  const fieldConfig = this.rules[field]
505
656
  if (fieldConfig) {
506
657
  const mode = fieldConfig.options?.mode ?? ValidationMode.DEFAULT
507
658
 
508
- // Validate if ON_TOUCH flag is set
509
659
  if (mode & ValidationMode.ON_TOUCH) {
510
660
  this.validateField(field, {
511
661
  isSubmitting: false,
@@ -514,15 +664,8 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
514
664
  }
515
665
  }
516
666
 
517
- // Persist the touched state if persistence is enabled
518
667
  if (this.options?.persist !== false) {
519
- const driver = this.getPersistenceDriver(this.options?.persistSuffix)
520
- driver.set(this.constructor.name, {
521
- state: toRaw(this.state),
522
- original: toRaw(this.original),
523
- dirty: toRaw(this.dirty),
524
- touched: toRaw(this.touched)
525
- } as PersistedForm<FormBody>)
668
+ this.persistState()
526
669
  }
527
670
  }
528
671
 
@@ -544,47 +687,34 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
544
687
  isTouched?: boolean
545
688
  } = {}
546
689
  ): void {
547
- // Clear existing errors for this field
548
- this._errors[field] = []
690
+ const emptyErrors: ErrorMessages = []
691
+ const errorKey = String(field)
692
+ this._errors[errorKey] = emptyErrors
549
693
 
550
694
  const value = this.state[field]
551
695
 
552
- // Get field rules and options
553
696
  const fieldConfig = this.rules[field]
554
697
  if (!fieldConfig?.rules || fieldConfig.rules.length === 0) {
555
698
  return // No rules to validate
556
699
  }
557
700
 
558
- // Default to ON_DIRTY | ON_SUBMIT if no mode is specified
559
701
  const mode = fieldConfig.options?.mode ?? ValidationMode.DEFAULT
560
702
 
561
- // Use the field's actual states if not provided in context
562
703
  const isDirty = context.isDirty !== undefined ? context.isDirty : this.isDirty(field)
563
704
  const isTouched = context.isTouched !== undefined ? context.isTouched : this.isTouched(field)
564
705
 
565
- // Determine if we should validate based on the context and mode
566
706
  const shouldValidate =
567
- // Force validation on submit if ON_SUBMIT flag is set
568
707
  (context.isSubmitting && mode & ValidationMode.ON_SUBMIT) ||
569
- // Validate if field is dirty and ON_DIRTY flag is set
570
708
  (isDirty && mode & ValidationMode.ON_DIRTY) ||
571
- // Validate if field is touched and ON_TOUCH flag is set
572
709
  (isTouched && mode & ValidationMode.ON_TOUCH) ||
573
- // Validate instantly if INSTANTLY flag is set
574
710
  mode & ValidationMode.INSTANTLY ||
575
- // Validate if a dependent field changed and ON_DEPENDENT_CHANGE flag is set
576
711
  (context.isDependentChange && mode & ValidationMode.ON_DEPENDENT_CHANGE)
577
712
 
578
713
  if (shouldValidate) {
579
- // Run each validation rule, passing the entire form state
580
714
  for (const rule of fieldConfig.rules) {
581
715
  const isValid = rule.validate(value, this.state)
582
716
  if (!isValid) {
583
- // If validation fails, add the error message
584
- if (!this._errors[field]) {
585
- this._errors[field] = []
586
- }
587
- this._errors[field].push(rule.getMessage())
717
+ this.getOrCreateFieldErrors(errorKey).push(rule.getMessage())
588
718
  }
589
719
  }
590
720
  }
@@ -593,23 +723,20 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
593
723
  public validate(isSubmitting: boolean = false): boolean {
594
724
  let isValid = true
595
725
 
596
- // Clear all errors
597
726
  for (const key in this._errors) {
598
727
  delete this._errors[key]
599
728
  }
600
729
 
601
- // Validate each field with rules
602
730
  for (const field in this.rules) {
603
731
  if (Object.prototype.hasOwnProperty.call(this.rules, field)) {
604
- // Validate with context, using field-specific states
605
732
  this.validateField(field as keyof FormBody, {
606
733
  isSubmitting,
607
734
  isDependentChange: false,
608
735
  isTouched: this.isTouched(field as keyof FormBody)
609
736
  })
610
737
 
611
- // If there are errors, the form is invalid
612
- if (this._errors[field] && this._errors[field].length > 0) {
738
+ const fieldErrors = this._errors[String(field)]
739
+ if (isErrorMessages(fieldErrors) && fieldErrors.length > 0) {
613
740
  isValid = false
614
741
  }
615
742
  }
@@ -620,50 +747,44 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
620
747
 
621
748
  public fillState(data: Partial<FormBody>): void {
622
749
  const driver = this.getPersistenceDriver(this.options?.persistSuffix)
623
- for (const key in data) {
624
- if (Object.prototype.hasOwnProperty.call(data, key) && key in this.state) {
625
- const currentVal = this.state[key]
626
- const newVal = data[key]
627
- if (currentVal instanceof PropertyAwareArray) {
628
- const arr = this.state[key] as PropertyAwareArray
629
- // Leere das Array und fülle es neu
630
- arr.length = 0
631
-
632
- if (Array.isArray(newVal)) {
633
- newVal.forEach((item) => arr.push(item))
634
- } else if (newVal instanceof PropertyAwareArray) {
635
- ;[...newVal].forEach((item) => arr.push(item))
636
- }
750
+ for (const key of Object.keys(data) as Array<keyof FormBody>) {
751
+ if (!Object.prototype.hasOwnProperty.call(data, key) || !(key in this.state)) {
752
+ continue
753
+ }
637
754
 
638
- this.dirty[key as keyof FormBody] = ([...arr] as any[]).map(() => false)
639
- this.touched[key as keyof FormBody] = true
640
- } else if (Array.isArray(newVal) && Array.isArray(currentVal)) {
641
- if (newVal.length === currentVal.length) {
642
- this.state[key] = deepMergeArrays(currentVal, newVal) as any
643
- } else {
644
- this.state[key] = newVal as any
645
- }
646
- this.dirty[key as keyof FormBody] = this.computeDirtyState(this.state[key], this.original[key])
647
- this.touched[key as keyof FormBody] = true
648
- } else if (newVal && typeof newVal === 'object' && currentVal && typeof currentVal === 'object') {
649
- this.state[key] = shallowMerge({ ...currentVal }, newVal)
650
- this.dirty[key as keyof FormBody] = this.computeDirtyState(this.state[key], this.original[key])
651
- this.touched[key as keyof FormBody] = true
652
- } else {
653
- this.state[key] = newVal as any
654
- this.dirty[key as keyof FormBody] = this.computeDirtyState(this.state[key], this.original[key])
655
- this.touched[key as keyof FormBody] = true
656
- }
755
+ const currentVal = this.state[key]
756
+ const newVal = data[key] as FormBody[typeof key] | undefined
757
+
758
+ if (currentVal instanceof PropertyAwareArray) {
759
+ const values = newVal instanceof PropertyAwareArray || Array.isArray(newVal) ? Array.from(newVal) : []
760
+ this.replacePropertyAwareArray(key, values)
761
+
762
+ this.dirty[key] = values.map(() => false)
763
+ this.touched[key] = true
764
+ continue
765
+ }
766
+
767
+ if (Array.isArray(newVal) && Array.isArray(currentVal)) {
768
+ const merged = newVal.length === currentVal.length ? deepMergeArrays(currentVal, newVal) : newVal
769
+ this.state[key] = merged as FormBody[typeof key]
770
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key])
771
+ this.touched[key] = true
772
+ continue
773
+ }
774
+
775
+ if (isRecord(newVal) && isRecord(currentVal)) {
776
+ this.state[key] = shallowMerge({ ...currentVal }, newVal) as FormBody[typeof key]
777
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key])
778
+ this.touched[key] = true
779
+ continue
657
780
  }
781
+
782
+ this.state[key] = newVal as FormBody[typeof key]
783
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key])
784
+ this.touched[key] = true
658
785
  }
659
- driver.set(this.constructor.name, {
660
- state: toRaw(this.state),
661
- original: toRaw(this.original),
662
- dirty: toRaw(this.dirty),
663
- touched: toRaw(this.touched)
664
- } as PersistedForm<FormBody>)
786
+ this.persistState(driver)
665
787
 
666
- // Validate affected fields and their dependencies
667
788
  for (const key in data) {
668
789
  if (Object.prototype.hasOwnProperty.call(data, key) && key in this.state) {
669
790
  this.validateField(key as keyof FormBody)
@@ -672,7 +793,17 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
672
793
  }
673
794
  }
674
795
 
675
- private transformValue(value: any, parentKey?: string): any {
796
+ private getValueGetter(name: string): ((value: unknown) => unknown) | undefined {
797
+ const candidate = (this as Record<string, unknown>)[name]
798
+ return typeof candidate === 'function' ? (candidate as (value: unknown) => unknown) : undefined
799
+ }
800
+
801
+ private getNoArgGetter(name: string): (() => unknown) | undefined {
802
+ const candidate = (this as Record<string, unknown>)[name]
803
+ return typeof candidate === 'function' ? (candidate as () => unknown) : undefined
804
+ }
805
+
806
+ private transformValue(value: unknown, parentKey?: string): unknown {
676
807
  if (value instanceof Date) {
677
808
  return value
678
809
  }
@@ -683,22 +814,21 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
683
814
  return [...value].map((item) => this.transformValue(item, parentKey))
684
815
  }
685
816
  if (Array.isArray(value)) {
686
- // For plain arrays, you might want to map them too:
687
817
  return value.map((item) => this.transformValue(item, parentKey))
688
- } else if (value && typeof value === 'object') {
689
- const result: any = {}
818
+ } else if (isRecord(value)) {
819
+ const result: Record<string, unknown> = {}
690
820
  for (const prop in value) {
691
821
  if (parentKey) {
692
822
  const compositeMethod = 'get' + upperFirst(parentKey) + upperFirst(camelCase(prop))
693
- if (typeof (this as any)[compositeMethod] === 'function') {
694
- const transformed = (this as any)[compositeMethod](value[prop])
823
+ const getter = this.getValueGetter(compositeMethod)
824
+ if (getter) {
825
+ const transformed = getter(value[prop])
695
826
  if (transformed !== undefined) {
696
827
  result[prop] = transformed
697
828
  }
698
829
  continue
699
830
  }
700
831
  }
701
- // Pass the parentKey along so that nested objects still use it.
702
832
  const transformed = this.transformValue(value[prop], parentKey)
703
833
  if (transformed !== undefined) {
704
834
  result[prop] = transformed
@@ -709,9 +839,12 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
709
839
  return value
710
840
  }
711
841
 
842
+ /**
843
+ * Build the request payload, applying field/composite/appended getters when present.
844
+ */
712
845
  public buildPayload(): RequestBody {
713
846
  const payload = {} as RequestBody
714
- for (const key in this.state) {
847
+ for (const key of Object.keys(this.state) as Array<FormKey<FormBody> & RequestKey<RequestBody>>) {
715
848
  if (this.ignore.includes(key)) {
716
849
  continue
717
850
  }
@@ -719,16 +852,17 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
719
852
  const value = this.state[key]
720
853
 
721
854
  const getterName = 'get' + upperFirst(camelCase(key))
722
- const typedKey = key as unknown as keyof RequestBody
723
- if (typeof (this as any)[getterName] === 'function') {
724
- const transformed = (this as any)[getterName](value)
855
+ const typedKey = key
856
+ const getter = this.getValueGetter(getterName)
857
+ if (getter) {
858
+ const transformed = getter(value)
725
859
  if (transformed !== undefined) {
726
- payload[typedKey] = transformed
860
+ payload[typedKey] = transformed as RequestBody[typeof typedKey]
727
861
  }
728
862
  } else {
729
863
  const transformed = this.transformValue(value, key)
730
864
  if (transformed !== undefined) {
731
- payload[typedKey] = transformed
865
+ payload[typedKey] = transformed as RequestBody[typeof typedKey]
732
866
  }
733
867
  }
734
868
  }
@@ -740,10 +874,11 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
740
874
  }
741
875
 
742
876
  const getterName = 'get' + upperFirst(camelCase(fieldName))
743
- if (typeof (this as any)[getterName] === 'function') {
744
- const transformed = (this as any)[getterName]()
877
+ const getter = this.getNoArgGetter(getterName)
878
+ if (getter) {
879
+ const transformed = getter()
745
880
  if (transformed !== undefined) {
746
- payload[fieldName as keyof RequestBody] = transformed
881
+ payload[fieldName as keyof RequestBody] = transformed as RequestBody[keyof RequestBody]
747
882
  }
748
883
  } else {
749
884
  console.warn(`Getter method '${getterName}' not found for appended field '${fieldName}' in ${this.constructor.name}.`)
@@ -757,14 +892,12 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
757
892
  const driver = this.getPersistenceDriver(this.options?.persistSuffix)
758
893
  for (const key in this.state) {
759
894
  if (this.state[key] instanceof PropertyAwareArray) {
760
- const stateArr = this.state[key] as PropertyAwareArray
761
895
  const originalValue = this.original[key] as PropertyAwareArray
762
-
763
- stateArr.length = 0
764
- ;[...originalValue].forEach((item) => stateArr.push(cloneDeep(item)))
765
-
766
- this.dirty[key as keyof FormBody] = ([...stateArr] as any[]).map(() => false)
767
- this.touched[key as keyof FormBody] = false
896
+ const values = [...originalValue].map((item) => cloneDeep(item))
897
+ const typedKey = key as keyof FormBody
898
+ this.replacePropertyAwareArray(typedKey, values as Array<ArrayItem<FormBody[typeof typedKey]>>)
899
+ this.dirty[typedKey] = values.map(() => false)
900
+ this.touched[typedKey] = false
768
901
  } else if (Array.isArray(this.original[key])) {
769
902
  this.state[key] = cloneDeep(this.original[key])
770
903
  this.dirty[key as keyof FormBody] = this.computeDirtyState(this.state[key], this.original[key])
@@ -778,29 +911,18 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
778
911
  for (const key in this._errors) {
779
912
  delete this._errors[key]
780
913
  }
781
- driver.set(this.constructor.name, {
782
- state: toRaw(this.state),
783
- original: toRaw(this.original),
784
- dirty: toRaw(this.dirty),
785
- touched: toRaw(this.touched)
786
- } as PersistedForm<FormBody>)
914
+ this.persistState(driver)
787
915
 
788
- // Revalidate the form after reset
789
916
  this.validate()
790
917
  }
791
918
 
792
- protected addToArrayProperty(property: keyof FormBody, newElement: any): void {
919
+ protected addToArrayProperty<K extends keyof FormBody>(property: K, newElement: ArrayItem<FormBody[K]>): void {
793
920
  const driver = this.getPersistenceDriver(this.options?.persistSuffix)
794
921
  const arr = this.state[property]
795
922
  if (arr instanceof PropertyAwareArray) {
796
923
  arr.push(newElement)
797
924
  this.touched[property] = true
798
- driver.set(this.constructor.name, {
799
- state: toRaw(this.state),
800
- original: toRaw(this.original),
801
- dirty: toRaw(this.dirty),
802
- touched: toRaw(this.touched)
803
- } as PersistedForm<FormBody>)
925
+ this.persistState(driver)
804
926
 
805
927
  return
806
928
  }
@@ -812,124 +934,121 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
812
934
  arr.push(newElement)
813
935
  this.dirty[property] = this.computeDirtyState(arr, this.original[property])
814
936
  this.touched[property] = true
815
- driver.set(this.constructor.name, {
816
- state: toRaw(this.state),
817
- original: toRaw(this.original),
818
- dirty: toRaw(this.dirty),
819
- touched: toRaw(this.touched)
820
- } as PersistedForm<FormBody>)
937
+ this.persistState(driver)
821
938
 
822
- // Validate the array field after modification
823
939
  this.validateField(property)
824
940
  this.validateDependentFields(property)
825
941
  }
826
942
 
827
- protected removeArrayItem(arrayIndex: string, filter: (item: any) => boolean): void {
828
- // @ts-expect-error
943
+ protected removeArrayItem<K extends keyof FormBody>(arrayIndex: K, filter: (item: ArrayItem<FormBody[K]>) => boolean): void {
829
944
  const current = this.state[arrayIndex]
830
945
  if (current instanceof PropertyAwareArray) {
831
- // Filter-Funktion auf PropertyAwareArray anwenden und Ergebnis zurückschreiben
832
946
  const filtered = [...current].filter(filter)
833
947
  current.length = 0
834
948
  filtered.forEach((item) => current.push(item))
835
949
  } else if (Array.isArray(current)) {
836
- // @ts-expect-error
837
- this.state[arrayIndex] = current.filter(filter)
950
+ this.state[arrayIndex] = current.filter(filter) as FormBody[K]
838
951
  }
839
952
 
840
- // Mark the array as touched
841
- this.touched[arrayIndex as keyof FormBody] = true
953
+ this.touched[arrayIndex] = true
842
954
 
843
- // Validate the array field after modification
844
- this.validateField(arrayIndex as keyof FormBody)
845
- this.validateDependentFields(arrayIndex as keyof FormBody)
955
+ this.validateField(arrayIndex)
956
+ this.validateDependentFields(arrayIndex)
846
957
  }
847
958
 
848
- protected resetArrayCounter(arrayIndex: string, counterIndex: string): void {
959
+ protected resetArrayCounter(arrayIndex: keyof FormBody, counterIndex: string): void {
849
960
  let count = 1
850
- // @ts-expect-error
851
961
  const current = this.state[arrayIndex]
852
962
  if (current instanceof PropertyAwareArray) {
853
- ;[...current].forEach((item: any): void => {
854
- item[counterIndex] = count
855
- count++
963
+ ;[...current].forEach((item): void => {
964
+ if (isRecord(item)) {
965
+ item[counterIndex] = count
966
+ count++
967
+ }
856
968
  })
857
969
  } else if (Array.isArray(current)) {
858
- current.forEach((item: any): void => {
859
- item[counterIndex] = count
860
- count++
970
+ current.forEach((item): void => {
971
+ if (isRecord(item)) {
972
+ item[counterIndex] = count
973
+ count++
974
+ }
861
975
  })
862
976
  }
863
977
 
864
- // Mark the array as touched
865
978
  this.touched[arrayIndex as keyof FormBody] = true
866
979
  }
867
980
 
868
- public get properties(): { [K in keyof FormBody]: any } {
869
- const props: any = {}
870
- for (const key in this.state) {
981
+ public get properties(): FormProperties<FormBody> {
982
+ const props: Partial<FormProperties<FormBody>> = {}
983
+ for (const key of Object.keys(this.state) as Array<FormKey<FormBody>>) {
871
984
  const value = this.state[key]
872
985
  if (value instanceof PropertyAwareArray) {
873
- props[key] = [...value].map((item, index) => {
874
- const elementProps: any = {}
875
- if (item && typeof item === 'object') {
876
- for (const innerKey in item) {
986
+ const arrayProps = [...value].map((item, index) => {
987
+ if (isRecord(item)) {
988
+ const elementProps: Record<string, FieldProperty<unknown>> = {}
989
+ for (const innerKey of Object.keys(item)) {
877
990
  elementProps[innerKey] = {
878
991
  model: computed({
879
- get: () => (this.state[key] as PropertyAwareArray)[index][innerKey],
992
+ get: () => {
993
+ const current = value[index]
994
+ return isRecord(current) ? current[innerKey] : undefined
995
+ },
880
996
  set: (newVal) => {
881
- ;(this.state[key] as PropertyAwareArray)[index][innerKey] = newVal
882
- const updatedElement = (this.state[key] as PropertyAwareArray)[index]
997
+ const current = value[index]
998
+ if (isRecord(current)) {
999
+ current[innerKey] = newVal
1000
+ }
1001
+ const updatedElement = value[index]
883
1002
  const originalElement = (this.original[key] as PropertyAwareArray)[index]
884
- ;(this.dirty[key] as any[])[index] = this.computeDirtyState(updatedElement, originalElement)
1003
+ this.setArrayDirty(key, index, this.computeDirtyState(updatedElement, originalElement))
885
1004
  this.touched[key] = true
886
1005
 
887
- // Validate after changing array items
888
- this.validateField(key as keyof FormBody)
889
- this.validateDependentFields(key as keyof FormBody)
1006
+ this.validateField(key)
1007
+ this.validateDependentFields(key)
890
1008
  }
891
1009
  }),
892
- errors: (this._errors[key] && this._errors[key][index] && this._errors[key][index][innerKey]) || [],
893
- dirty:
894
- Array.isArray(this.dirty[key]) && this.dirty[key][index] && typeof (this.dirty[key] as any[])[index] === 'object'
895
- ? (this.dirty[key] as any[])[index][innerKey]
896
- : false,
1010
+ errors: this.getArrayItemFieldErrors(key, index, innerKey),
1011
+ dirty: this.getArrayItemDirty(key, index, innerKey),
897
1012
  touched: this.touched[key] || false
898
1013
  }
899
1014
  }
900
- } else {
901
- elementProps.value = {
1015
+ return elementProps
1016
+ }
1017
+
1018
+ return {
1019
+ value: {
902
1020
  model: computed({
903
- get: () => (this.state[key] as PropertyAwareArray)[index],
1021
+ get: () => value[index],
904
1022
  set: (newVal) => {
905
- ;(this.state[key] as PropertyAwareArray)[index] = newVal
906
- const updatedValue = (this.state[key] as PropertyAwareArray)[index]
1023
+ value[index] = newVal as ArrayItem<FormBody[typeof key]>
1024
+ const updatedValue = value[index]
907
1025
  const originalValue = (this.original[key] as PropertyAwareArray)[index]
908
- ;(this.dirty[key] as boolean[])[index] = !isEqual(updatedValue, originalValue)
1026
+ this.setArrayDirty(key, index, this.computeDirtyState(updatedValue, originalValue))
909
1027
  this.touched[key] = true
910
1028
 
911
- // Validate after changing array items
912
- this.validateField(key as keyof FormBody)
913
- this.validateDependentFields(key as keyof FormBody)
1029
+ this.validateField(key)
1030
+ this.validateDependentFields(key)
914
1031
  }
915
1032
  }),
916
- errors: (this._errors[key] && this._errors[key][index]) || [],
917
- dirty: Array.isArray(this.dirty[key]) ? (this.dirty[key] as boolean[])[index] : false,
1033
+ errors: this.getArrayItemErrorMessages(key, index),
1034
+ dirty: this.getArrayItemDirtyValue(key, index),
918
1035
  touched: this.touched[key] || false
919
1036
  }
920
1037
  }
921
- return elementProps
922
1038
  })
923
- } else {
924
- props[key] = {
925
- model: this._model[key],
926
- errors: this._errors[key] || [],
927
- dirty: this.dirty[key] || false,
928
- touched: this.touched[key] || false
929
- }
1039
+
1040
+ props[key] = arrayProps as FormProperties<FormBody>[typeof key]
1041
+ continue
930
1042
  }
1043
+
1044
+ props[key] = {
1045
+ model: this._model[key],
1046
+ errors: this.getFieldErrors(key),
1047
+ dirty: this.dirty[key] || false,
1048
+ touched: this.touched[key] || false
1049
+ } as FormProperties<FormBody>[typeof key]
931
1050
  }
932
- return props
1051
+ return props as FormProperties<FormBody>
933
1052
  }
934
1053
 
935
1054
  /**
@@ -938,11 +1057,9 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
938
1057
  * @returns boolean indicating if the form or specified field is dirty
939
1058
  */
940
1059
  public isDirty(field?: keyof FormBody): boolean {
941
- // If a specific field is provided, check only that field
942
1060
  if (field !== undefined) {
943
1061
  const dirtyState = this.dirty[field]
944
1062
 
945
- // Handle different types of dirty state
946
1063
  if (typeof dirtyState === 'boolean') {
947
1064
  return dirtyState
948
1065
  }
@@ -966,7 +1083,6 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
966
1083
  return false
967
1084
  }
968
1085
 
969
- // Check all fields (original behavior)
970
1086
  for (const key in this.dirty) {
971
1087
  const dirtyState = this.dirty[key as keyof FormBody]
972
1088
 
@@ -994,8 +1110,8 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
994
1110
  }
995
1111
 
996
1112
  /**
997
- * Returns whether the form has any validation errors
998
- * @returns boolean indicating if the form has any errors
1113
+ * Returns whether the form has validation errors
1114
+ * @returns boolean indicating if the form has errors
999
1115
  */
1000
1116
  public hasErrors(): boolean {
1001
1117
  return this._hasErrors.value
@@ -1013,16 +1129,13 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
1013
1129
  const driver = this.getPersistenceDriver(this.options?.persistSuffix)
1014
1130
  const currentVal = this.state[key]
1015
1131
 
1016
- // Handle PropertyAwareArray
1017
1132
  if (currentVal instanceof PropertyAwareArray) {
1018
1133
  const arr = this.state[key] as PropertyAwareArray
1019
1134
  const originalArr = this.original[key] as PropertyAwareArray
1020
1135
 
1021
- // Clear arrays
1022
1136
  arr.length = 0
1023
1137
  originalArr.length = 0
1024
1138
 
1025
- // Fill both arrays with the new value
1026
1139
  if (Array.isArray(value)) {
1027
1140
  value.forEach((item) => {
1028
1141
  arr.push(cloneDeep(item))
@@ -1035,44 +1148,29 @@ export abstract class BaseForm<RequestBody extends object, FormBody extends obje
1035
1148
  })
1036
1149
  }
1037
1150
 
1038
- // Reset dirty state for this array
1039
- this.dirty[key] = ([...arr] as any[]).map(() => false)
1040
- // Mark as touched
1151
+ this.dirty[key] = Array.from(arr).map(() => false)
1041
1152
  this.touched[key] = true
1042
- }
1043
- // Handle regular arrays
1044
- else if (Array.isArray(currentVal)) {
1153
+ } else if (Array.isArray(currentVal)) {
1045
1154
  this.state[key] = cloneDeep(value)
1046
1155
  this.original[key] = cloneDeep(value)
1047
1156
  this.dirty[key] = false
1048
1157
  this.touched[key] = true
1049
- }
1050
- // Handle objects
1051
- else if (typeof currentVal === 'object' && currentVal !== null) {
1158
+ } else if (typeof currentVal === 'object' && currentVal !== null) {
1052
1159
  this.state[key] = cloneDeep(value)
1053
1160
  this.original[key] = cloneDeep(value)
1054
1161
  this.dirty[key] = false
1055
1162
  this.touched[key] = true
1056
- }
1057
- // Handle primitive values
1058
- else {
1163
+ } else {
1059
1164
  this.state[key] = value
1060
1165
  this.original[key] = value
1061
1166
  this.dirty[key] = false
1062
1167
  this.touched[key] = true
1063
1168
  }
1064
1169
 
1065
- // Update persistence if enabled
1066
1170
  if (this.options?.persist !== false) {
1067
- driver.set(this.constructor.name, {
1068
- state: toRaw(this.state),
1069
- original: toRaw(this.original),
1070
- dirty: toRaw(this.dirty),
1071
- touched: toRaw(this.touched)
1072
- } as PersistedForm<FormBody>)
1171
+ this.persistState(driver)
1073
1172
  }
1074
1173
 
1075
- // Validate the field and any dependent fields
1076
1174
  this.validateField(key)
1077
1175
  this.validateDependentFields(key)
1078
1176
  }