@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.
- package/.editorconfig +508 -0
- package/.eslintrc.cjs +15 -0
- package/.prettierrc.json +8 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/docker-compose.yaml +8 -0
- package/docs/.vitepress/config.ts +68 -0
- package/docs/.vitepress/theme/Layout.vue +14 -0
- package/docs/.vitepress/theme/components/VersionSelector.vue +64 -0
- package/docs/.vitepress/theme/index.js +13 -0
- package/docs/index.md +70 -0
- package/docs/services/laravel/pagination.md +54 -0
- package/docs/services/laravel/requests.md +62 -0
- package/docs/services/requests/index.md +74 -0
- package/docs/vue/forms.md +326 -0
- package/docs/vue/requests/route-model-binding.md +66 -0
- package/docs/vue/state.md +293 -0
- package/env.d.ts +1 -0
- package/eslint.config.js +15 -0
- package/examples/files/7z2404-x64.exe +0 -0
- package/examples/index.html +14 -0
- package/examples/js/app.js +8 -0
- package/examples/js/router.js +22 -0
- package/examples/js/view/App.vue +49 -0
- package/examples/js/view/layout/DemoPage.vue +28 -0
- package/examples/js/view/pagination/Pagination.vue +28 -0
- package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +71 -0
- package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +54 -0
- package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +50 -0
- package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +57 -0
- package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +50 -0
- package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +63 -0
- package/examples/js/view/requests/Requests.vue +34 -0
- package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +36 -0
- package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +25 -0
- package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +15 -0
- package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +44 -0
- package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +34 -0
- package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +59 -0
- package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +21 -0
- package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +53 -0
- package/package.json +81 -0
- package/release-tool.json +7 -0
- package/src/helpers.ts +78 -0
- package/src/service/bulkRequests/BulkRequestEvent.enum.ts +4 -0
- package/src/service/bulkRequests/BulkRequestSender.ts +184 -0
- package/src/service/bulkRequests/BulkRequestWrapper.ts +49 -0
- package/src/service/bulkRequests/index.ts +6 -0
- package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +4 -0
- package/src/service/laravel/pagination/contracts/PaginationResponseBodyContract.ts +6 -0
- package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +32 -0
- package/src/service/laravel/pagination/index.ts +7 -0
- package/src/service/laravel/requests/JsonBaseRequest.ts +35 -0
- package/src/service/laravel/requests/PaginationJsonBaseRequest.ts +29 -0
- package/src/service/laravel/requests/index.ts +9 -0
- package/src/service/laravel/requests/responses/JsonResponse.ts +8 -0
- package/src/service/laravel/requests/responses/PaginationResponse.ts +16 -0
- package/src/service/pagination/InfiniteScroller.ts +21 -0
- package/src/service/pagination/Paginator.ts +149 -0
- package/src/service/pagination/contracts/PaginateableRequestContract.ts +13 -0
- package/src/service/pagination/contracts/PaginationDataDriverContract.ts +5 -0
- package/src/service/pagination/contracts/PaginationResponseContract.ts +7 -0
- package/src/service/pagination/contracts/PaginatorLoadDataOptions.ts +4 -0
- package/src/service/pagination/contracts/ViewDriverContract.ts +12 -0
- package/src/service/pagination/contracts/ViewDriverFactoryContract.ts +5 -0
- package/src/service/pagination/dataDrivers/ArrayDriver.ts +28 -0
- package/src/service/pagination/dtos/PaginationDataDto.ts +14 -0
- package/src/service/pagination/factories/VuePaginationDriverFactory.ts +9 -0
- package/src/service/pagination/frontendDrivers/VuePaginationDriver.ts +61 -0
- package/src/service/pagination/index.ts +16 -0
- package/src/service/persistenceDrivers/LocalStorageDriver.ts +22 -0
- package/src/service/persistenceDrivers/NonPersistentDriver.ts +12 -0
- package/src/service/persistenceDrivers/SessionStorageDriver.ts +22 -0
- package/src/service/persistenceDrivers/index.ts +8 -0
- package/src/service/persistenceDrivers/types/PersistenceDriver.ts +5 -0
- package/src/service/requests/BaseRequest.ts +197 -0
- package/src/service/requests/ErrorHandler.ts +64 -0
- package/src/service/requests/RequestEvents.enum.ts +3 -0
- package/src/service/requests/RequestMethod.enum.ts +8 -0
- package/src/service/requests/bodies/FormDataBody.ts +41 -0
- package/src/service/requests/bodies/JsonBody.ts +16 -0
- package/src/service/requests/contracts/AbortableRequestContract.ts +3 -0
- package/src/service/requests/contracts/BaseRequestContract.ts +36 -0
- package/src/service/requests/contracts/BodyContract.ts +7 -0
- package/src/service/requests/contracts/BodyFactoryContract.ts +5 -0
- package/src/service/requests/contracts/DriverConfigContract.ts +7 -0
- package/src/service/requests/contracts/HeadersContract.ts +5 -0
- package/src/service/requests/contracts/RequestDriverContract.ts +15 -0
- package/src/service/requests/contracts/RequestLoaderContract.ts +5 -0
- package/src/service/requests/contracts/RequestLoaderFactoryContract.ts +5 -0
- package/src/service/requests/contracts/ResponseContract.ts +7 -0
- package/src/service/requests/drivers/contracts/ResponseHandlerContract.ts +10 -0
- package/src/service/requests/drivers/fetch/FetchDriver.ts +115 -0
- package/src/service/requests/drivers/fetch/FetchResponse.ts +30 -0
- package/src/service/requests/exceptions/NoResponseReceivedException.ts +3 -0
- package/src/service/requests/exceptions/NotFoundException.ts +3 -0
- package/src/service/requests/exceptions/PageExpiredException.ts +3 -0
- package/src/service/requests/exceptions/ResponseBodyException.ts +15 -0
- package/src/service/requests/exceptions/ResponseException.ts +11 -0
- package/src/service/requests/exceptions/ServerErrorException.ts +3 -0
- package/src/service/requests/exceptions/UnauthorizedException.ts +3 -0
- package/src/service/requests/exceptions/ValidationException.ts +3 -0
- package/src/service/requests/exceptions/index.ts +19 -0
- package/src/service/requests/factories/FormDataFactory.ts +9 -0
- package/src/service/requests/factories/JsonBodyFactory.ts +9 -0
- package/src/service/requests/index.ts +50 -0
- package/src/service/requests/responses/BaseResponse.ts +41 -0
- package/src/service/requests/responses/BlobResponse.ts +19 -0
- package/src/service/requests/responses/JsonResponse.ts +15 -0
- package/src/service/requests/responses/PlainTextResponse.ts +15 -0
- package/src/service/support/DeferredPromise.ts +67 -0
- package/src/service/support/index.ts +3 -0
- package/src/vue/composables/useConfirmDialog.ts +59 -0
- package/src/vue/composables/useGlobalCheckbox.ts +145 -0
- package/src/vue/composables/useIsEmpty.ts +34 -0
- package/src/vue/composables/useIsOpen.ts +37 -0
- package/src/vue/composables/useIsOpenFromVar.ts +61 -0
- package/src/vue/composables/useModelWrapper.ts +24 -0
- package/src/vue/composables/useOnOpen.ts +34 -0
- package/src/vue/contracts/ModelValueOptions.ts +3 -0
- package/src/vue/contracts/ModelValueProps.ts +3 -0
- package/src/vue/forms/BaseForm.ts +1074 -0
- package/src/vue/forms/PropertyAwareArray.ts +78 -0
- package/src/vue/forms/index.ts +11 -0
- package/src/vue/forms/types/PersistedForm.ts +6 -0
- package/src/vue/forms/validation/ValidationMode.enum.ts +14 -0
- package/src/vue/forms/validation/index.ts +12 -0
- package/src/vue/forms/validation/rules/BaseRule.ts +7 -0
- package/src/vue/forms/validation/rules/ConfirmedRule.ts +39 -0
- package/src/vue/forms/validation/rules/MinRule.ts +61 -0
- package/src/vue/forms/validation/rules/RequiredRule.ts +19 -0
- package/src/vue/forms/validation/rules/UrlRule.ts +24 -0
- package/src/vue/forms/validation/types/BidirectionalRule.ts +11 -0
- package/src/vue/index.ts +14 -0
- package/src/vue/requests/factories/VueRequestLoaderFactory.ts +9 -0
- package/src/vue/requests/index.ts +5 -0
- package/src/vue/requests/loaders/VueRequestBatchLoader.ts +30 -0
- package/src/vue/requests/loaders/VueRequestLoader.ts +18 -0
- package/src/vue/router/routeModelBinding/RouteModelRequestResolver.ts +11 -0
- package/src/vue/router/routeModelBinding/defineRoute.ts +31 -0
- package/src/vue/router/routeModelBinding/index.ts +8 -0
- package/src/vue/router/routeModelBinding/installRouteInjection.ts +73 -0
- package/src/vue/router/routeModelBinding/types.ts +46 -0
- package/src/vue/state/State.ts +391 -0
- package/src/vue/state/index.ts +3 -0
- package/tests/service/helpers/mergeDeep.test.ts +53 -0
- package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +84 -0
- package/tests/service/laravel/requests/JsonBaseRequest.test.ts +43 -0
- package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +58 -0
- package/tests/service/laravel/requests/responses/JsonResponse.test.ts +59 -0
- package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +127 -0
- package/tests/service/pagination/dtos/PaginationDataDto.test.ts +35 -0
- package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +32 -0
- package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +66 -0
- package/tests/service/requests/ErrorHandler.test.ts +141 -0
- package/tsconfig.json +114 -0
- package/vite.config.ts +34 -0
- 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
|
+
}
|