@blueprint-ts/core 1.0.0

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