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