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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +25 -1
  3. package/docs/.vitepress/config.ts +81 -23
  4. package/docs/index.md +6 -63
  5. package/docs/{services/laravel → laravel}/pagination.md +19 -6
  6. package/docs/{services/laravel → laravel}/requests.md +2 -2
  7. package/docs/services/pagination/index.md +46 -0
  8. package/docs/services/pagination/infinite-scroller.md +20 -0
  9. package/docs/services/pagination/page-aware.md +51 -0
  10. package/docs/services/pagination/state-pagination.md +77 -0
  11. package/docs/services/pagination/updating-rows.md +52 -0
  12. package/docs/services/persistence/index.md +46 -0
  13. package/docs/services/requests/abort-requests.md +29 -0
  14. package/docs/services/requests/bulk-requests.md +70 -0
  15. package/docs/services/requests/concurrency.md +58 -0
  16. package/docs/services/requests/drivers.md +50 -0
  17. package/docs/services/requests/error-handling.md +153 -0
  18. package/docs/services/requests/events.md +31 -0
  19. package/docs/services/requests/getting-started.md +201 -0
  20. package/docs/services/requests/headers.md +40 -0
  21. package/docs/services/requests/loading.md +63 -0
  22. package/docs/services/requests/request-bodies.md +59 -0
  23. package/docs/services/requests/responses.md +34 -0
  24. package/docs/services/support/deferred-promise.md +63 -0
  25. package/docs/services/support/helpers.md +77 -0
  26. package/docs/services/support/index.md +6 -0
  27. package/docs/upgrading/v1-to-v2.md +64 -0
  28. package/docs/upgrading/v2-to-v3.md +52 -0
  29. package/docs/upgrading/v3-to-v4.md +214 -0
  30. package/docs/upgrading.md +0 -0
  31. package/docs/vue/composables/use-confirm-dialog.md +96 -0
  32. package/docs/vue/composables/use-global-checkbox.md +73 -0
  33. package/docs/vue/composables/use-is-empty.md +26 -0
  34. package/docs/vue/composables/use-is-open-from-var.md +32 -0
  35. package/docs/vue/composables/use-is-open.md +28 -0
  36. package/docs/vue/composables/use-model-wrapper.md +29 -0
  37. package/docs/vue/composables/use-on-open.md +26 -0
  38. package/docs/vue/forms/arrays.md +102 -0
  39. package/docs/vue/forms/errors.md +52 -0
  40. package/docs/vue/forms/index.md +99 -0
  41. package/docs/vue/forms/payloads.md +99 -0
  42. package/docs/vue/forms/persistence.md +19 -0
  43. package/docs/vue/forms/state-and-properties.md +26 -0
  44. package/docs/vue/forms/utilities.md +27 -0
  45. package/docs/vue/forms/validation.md +189 -0
  46. package/docs/vue/requests/loading.md +51 -0
  47. package/docs/vue/{requests → router}/route-resource-binding.md +33 -27
  48. package/docs/vue/state.md +27 -11
  49. package/package.json +12 -13
  50. package/release-tool.json +22 -3
  51. package/src/{service/bulkRequests → bulkRequests}/BulkRequestSender.ts +29 -17
  52. package/src/{service/bulkRequests → bulkRequests}/BulkRequestWrapper.ts +5 -5
  53. package/src/laravel/pagination/dataDrivers/RequestDriver.ts +30 -0
  54. package/src/laravel/pagination/index.ts +6 -0
  55. package/src/pagination/BasePaginator.ts +94 -0
  56. package/src/{service/pagination → pagination}/InfiniteScroller.ts +1 -0
  57. package/src/{service/pagination → pagination}/PageAwarePaginator.ts +34 -26
  58. package/src/{service/pagination → pagination}/StatePaginator.ts +2 -8
  59. package/src/{service/pagination → pagination}/index.ts +1 -3
  60. package/src/{service/requests → requests}/BaseRequest.ts +89 -4
  61. package/src/requests/ErrorHandler.ts +144 -0
  62. package/src/requests/RequestConcurrencyMode.enum.ts +6 -0
  63. package/src/requests/RequestErrorRouter.ts +89 -0
  64. package/src/{service/requests → requests}/bodies/FormDataBody.ts +10 -6
  65. package/src/{service/requests → requests}/contracts/BaseRequestContract.ts +3 -0
  66. package/src/requests/exceptions/BadGatewayException.ts +3 -0
  67. package/src/requests/exceptions/BadRequestException.ts +3 -0
  68. package/src/requests/exceptions/ConflictException.ts +3 -0
  69. package/src/requests/exceptions/ForbiddenException.ts +3 -0
  70. package/src/requests/exceptions/GatewayTimeoutException.ts +3 -0
  71. package/src/requests/exceptions/GoneException.ts +3 -0
  72. package/src/requests/exceptions/InvalidJsonException.ts +15 -0
  73. package/src/requests/exceptions/LockedException.ts +3 -0
  74. package/src/requests/exceptions/MethodNotAllowedException.ts +3 -0
  75. package/src/requests/exceptions/NotImplementedException.ts +3 -0
  76. package/src/requests/exceptions/PayloadTooLargeException.ts +3 -0
  77. package/src/requests/exceptions/PreconditionFailedException.ts +3 -0
  78. package/src/requests/exceptions/RequestTimeoutException.ts +3 -0
  79. package/src/requests/exceptions/ServiceUnavailableException.ts +3 -0
  80. package/src/requests/exceptions/StaleResponseException.ts +13 -0
  81. package/src/requests/exceptions/TooManyRequestsException.ts +3 -0
  82. package/src/requests/exceptions/UnsupportedMediaTypeException.ts +3 -0
  83. package/src/requests/exceptions/index.ts +51 -0
  84. package/src/requests/factories/FormDataFactory.ts +17 -0
  85. package/src/{service/requests → requests}/index.ts +9 -3
  86. package/src/requests/types/RequestConcurrencyOptions.ts +6 -0
  87. package/src/{service/support → support}/DeferredPromise.ts +1 -1
  88. package/src/support/index.ts +4 -0
  89. package/src/vue/composables/useConfirmDialog.ts +5 -1
  90. package/src/vue/composables/useModelWrapper.ts +3 -0
  91. package/src/vue/forms/BaseForm.ts +512 -399
  92. package/src/vue/forms/PropertyAwareArray.ts +6 -2
  93. package/src/vue/forms/index.ts +4 -4
  94. package/src/vue/forms/validation/index.ts +5 -2
  95. package/src/vue/forms/validation/rules/ConfirmedRule.ts +3 -3
  96. package/src/vue/forms/validation/rules/EmailRule.ts +23 -0
  97. package/src/vue/forms/validation/rules/JsonRule.ts +28 -0
  98. package/src/vue/forms/validation/types/BidirectionalRule.ts +2 -2
  99. package/src/vue/forms/validation/types/ValidationRules.ts +15 -0
  100. package/src/vue/index.ts +3 -3
  101. package/src/vue/requests/factories/VueRequestLoaderFactory.ts +3 -2
  102. package/src/vue/requests/loaders/VueRequestBatchLoader.ts +6 -1
  103. package/src/vue/requests/loaders/VueRequestLoader.ts +1 -1
  104. package/src/vue/router/routeResourceBinding/types.ts +3 -3
  105. package/src/vue/state/State.ts +38 -50
  106. package/tests/service/helpers/mergeDeep.test.ts +1 -1
  107. package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +3 -3
  108. package/tests/service/laravel/requests/JsonBaseRequest.test.ts +4 -4
  109. package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +3 -3
  110. package/tests/service/laravel/requests/responses/JsonResponse.test.ts +2 -2
  111. package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +2 -2
  112. package/tests/service/pagination/dtos/PaginationDataDto.test.ts +1 -1
  113. package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +2 -2
  114. package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +1 -1
  115. package/tests/service/requests/ErrorHandler.test.ts +61 -58
  116. package/tests/service/requests/FormDataBody.test.ts +1 -1
  117. package/tests/vue/forms/BaseForm.behavior.test.ts +98 -0
  118. package/docs/.vitepress/theme/Layout.vue +0 -14
  119. package/docs/.vitepress/theme/components/VersionSelector.vue +0 -64
  120. package/docs/.vitepress/theme/index.js +0 -13
  121. package/docs/services/requests/index.md +0 -74
  122. package/docs/vue/forms.md +0 -477
  123. package/examples/files/7z2404-x64.exe +0 -0
  124. package/examples/index.html +0 -14
  125. package/examples/js/app.js +0 -8
  126. package/examples/js/router.js +0 -22
  127. package/examples/js/view/App.vue +0 -49
  128. package/examples/js/view/layout/DemoPage.vue +0 -28
  129. package/examples/js/view/pagination/Pagination.vue +0 -28
  130. package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +0 -71
  131. package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +0 -54
  132. package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +0 -50
  133. package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +0 -57
  134. package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +0 -50
  135. package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +0 -63
  136. package/examples/js/view/requests/Requests.vue +0 -34
  137. package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +0 -36
  138. package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +0 -25
  139. package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +0 -15
  140. package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +0 -44
  141. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +0 -34
  142. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +0 -59
  143. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +0 -21
  144. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +0 -53
  145. package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +0 -4
  146. package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +0 -32
  147. package/src/service/laravel/pagination/index.ts +0 -7
  148. package/src/service/pagination/BasePaginator.ts +0 -36
  149. package/src/service/pagination/Paginator.ts +0 -11
  150. package/src/service/requests/ErrorHandler.ts +0 -64
  151. package/src/service/requests/exceptions/index.ts +0 -19
  152. package/src/service/requests/factories/FormDataFactory.ts +0 -9
  153. package/src/service/support/index.ts +0 -3
  154. /package/src/{service/bulkRequests → bulkRequests}/BulkRequestEvent.enum.ts +0 -0
  155. /package/src/{service/bulkRequests → bulkRequests}/index.ts +0 -0
  156. /package/src/{service/laravel → laravel}/pagination/contracts/PaginationResponseBodyContract.ts +0 -0
  157. /package/src/{service/laravel → laravel}/requests/JsonBaseRequest.ts +0 -0
  158. /package/src/{service/laravel → laravel}/requests/PaginationJsonBaseRequest.ts +0 -0
  159. /package/src/{service/laravel → laravel}/requests/index.ts +0 -0
  160. /package/src/{service/laravel → laravel}/requests/responses/JsonResponse.ts +0 -0
  161. /package/src/{service/laravel → laravel}/requests/responses/PaginationResponse.ts +0 -0
  162. /package/src/{service/pagination → pagination}/contracts/BaseViewDriverContract.ts +0 -0
  163. /package/src/{service/pagination → pagination}/contracts/BaseViewDriverFactoryContract.ts +0 -0
  164. /package/src/{service/pagination → pagination}/contracts/PaginateableRequestContract.ts +0 -0
  165. /package/src/{service/pagination → pagination}/contracts/PaginationDataDriverContract.ts +0 -0
  166. /package/src/{service/pagination → pagination}/contracts/PaginationResponseContract.ts +0 -0
  167. /package/src/{service/pagination → pagination}/contracts/PaginatorLoadDataOptions.ts +0 -0
  168. /package/src/{service/pagination → pagination}/contracts/StatePaginationDataDriverContract.ts +0 -0
  169. /package/src/{service/pagination → pagination}/contracts/ViewDriverContract.ts +0 -0
  170. /package/src/{service/pagination → pagination}/contracts/ViewDriverFactoryContract.ts +0 -0
  171. /package/src/{service/pagination → pagination}/dataDrivers/ArrayDriver.ts +0 -0
  172. /package/src/{service/pagination → pagination}/dtos/PaginationDataDto.ts +0 -0
  173. /package/src/{service/pagination → pagination}/dtos/StatePaginationDataDto.ts +0 -0
  174. /package/src/{service/pagination → pagination}/factories/VueBaseViewDriverFactory.ts +0 -0
  175. /package/src/{service/pagination → pagination}/factories/VuePaginationDriverFactory.ts +0 -0
  176. /package/src/{service/pagination → pagination}/frontendDrivers/VueBaseViewDriver.ts +0 -0
  177. /package/src/{service/pagination → pagination}/frontendDrivers/VuePaginationDriver.ts +0 -0
  178. /package/src/{service/persistenceDrivers → persistenceDrivers}/LocalStorageDriver.ts +0 -0
  179. /package/src/{service/persistenceDrivers → persistenceDrivers}/NonPersistentDriver.ts +0 -0
  180. /package/src/{service/persistenceDrivers → persistenceDrivers}/SessionStorageDriver.ts +0 -0
  181. /package/src/{service/persistenceDrivers → persistenceDrivers}/index.ts +0 -0
  182. /package/src/{service/persistenceDrivers → persistenceDrivers}/types/PersistenceDriver.ts +0 -0
  183. /package/src/{service/requests → requests}/RequestEvents.enum.ts +0 -0
  184. /package/src/{service/requests → requests}/RequestMethod.enum.ts +0 -0
  185. /package/src/{service/requests → requests}/bodies/JsonBody.ts +0 -0
  186. /package/src/{service/requests → requests}/contracts/AbortableRequestContract.ts +0 -0
  187. /package/src/{service/requests → requests}/contracts/BodyContract.ts +0 -0
  188. /package/src/{service/requests → requests}/contracts/BodyFactoryContract.ts +0 -0
  189. /package/src/{service/requests → requests}/contracts/DriverConfigContract.ts +0 -0
  190. /package/src/{service/requests → requests}/contracts/HeadersContract.ts +0 -0
  191. /package/src/{service/requests → requests}/contracts/RequestDriverContract.ts +0 -0
  192. /package/src/{service/requests → requests}/contracts/RequestLoaderContract.ts +0 -0
  193. /package/src/{service/requests → requests}/contracts/RequestLoaderFactoryContract.ts +0 -0
  194. /package/src/{service/requests → requests}/contracts/ResponseContract.ts +0 -0
  195. /package/src/{service/requests → requests}/drivers/contracts/ResponseHandlerContract.ts +0 -0
  196. /package/src/{service/requests → requests}/drivers/fetch/FetchDriver.ts +0 -0
  197. /package/src/{service/requests → requests}/drivers/fetch/FetchResponse.ts +0 -0
  198. /package/src/{service/requests → requests}/exceptions/NoResponseReceivedException.ts +0 -0
  199. /package/src/{service/requests → requests}/exceptions/NotFoundException.ts +0 -0
  200. /package/src/{service/requests → requests}/exceptions/PageExpiredException.ts +0 -0
  201. /package/src/{service/requests → requests}/exceptions/ResponseBodyException.ts +0 -0
  202. /package/src/{service/requests → requests}/exceptions/ResponseException.ts +0 -0
  203. /package/src/{service/requests → requests}/exceptions/ServerErrorException.ts +0 -0
  204. /package/src/{service/requests → requests}/exceptions/UnauthorizedException.ts +0 -0
  205. /package/src/{service/requests → requests}/exceptions/ValidationException.ts +0 -0
  206. /package/src/{service/requests → requests}/factories/JsonBodyFactory.ts +0 -0
  207. /package/src/{service/requests → requests}/responses/BaseResponse.ts +0 -0
  208. /package/src/{service/requests → requests}/responses/BlobResponse.ts +0 -0
  209. /package/src/{service/requests → requests}/responses/JsonResponse.ts +0 -0
  210. /package/src/{service/requests → requests}/responses/PlainTextResponse.ts +0 -0
  211. /package/src/{helpers.ts → support/helpers.ts} +0 -0
package/docs/vue/forms.md DELETED
@@ -1,477 +0,0 @@
1
- # BaseForm Documentation
2
-
3
- ## Overview
4
-
5
- `BaseForm` is a powerful and flexible TypeScript class for handling form state, validation, and submission in Vue
6
- applications. It provides a comprehensive solution for managing form data with features like:
7
-
8
- - Type-safe form state management
9
- - Dirty + touched state tracking for fields
10
- - Error handling and field validation
11
- - Form persistence between page reloads
12
- - Support for complex nested objects and arrays
13
- - Automatic transformation of form values to API payloads
14
- - Supports `File`/`Blob` fields for multipart requests (see “Files / Uploads”)
15
-
16
- ## Key Features
17
-
18
- ### 1. Type-Safe Form Management
19
-
20
- The `BaseForm` is a generic class that takes two type parameters:
21
-
22
- - `RequestBody`: The shape of the data that will be sent to the server
23
- - `FormBody`: The shape of the form's internal state, which can differ from the request payload
24
-
25
- ````typescript
26
- class MyForm extends BaseForm<MyRequestPayload, MyFormState> {
27
- // ...
28
- }
29
- ````
30
-
31
- ### 2. Form State Persistence
32
-
33
- Forms can automatically save their state to browser storage (session, local, etc.), allowing users to navigate away and
34
- return without losing their input.
35
-
36
- ````typescript
37
- protected override getPersistenceDriver(suffix?: string): PersistenceDriver {
38
- return new SessionStorageDriver(suffix) // Or LocalStorageDriver(suffix), etc.
39
- }
40
- ````
41
-
42
- Notes:
43
- - Persistence is enabled by default. Disable it via `super(defaults, { persist: false })`.
44
- - `persistSuffix` is passed into `getPersistenceDriver(suffix)` and is typically used to namespace the storage key.
45
- - Persisted state is only reused if the stored `original` matches your current `defaults`; otherwise it is discarded.
46
- - `persist: false` disables the automatic rehydration + background persistence, but some explicit mutation helpers (e.g. `fillState()`, `reset()`, `addToArrayProperty()`) still call the driver.
47
- - `File`/`Blob` values are not JSON-serializable, so persistence is not supported for file inputs. Use `{ persist: false }` for file upload forms.
48
-
49
- ### 3. Transformations and Getters
50
-
51
- `buildPayload()` supports three “getter” patterns to transform values before sending them to your API.
52
-
53
- Special value types:
54
- - `Date` values are treated as scalars and preserved (not “object-walked”). When sent as JSON they serialize to ISO strings via `JSON.stringify()`, and when sent as multipart `FormData` they are appended as `toISOString()`.
55
- - `File`/`Blob` values are also treated as scalars and preserved for multipart uploads.
56
-
57
- #### A) Field Getter (common)
58
- If your form `state` contains a field, you can define a getter for that same field name. During `buildPayload()`,
59
- `BaseForm` will call it with the field’s current value and use the return value in the payload.
60
-
61
- Getter name format: `get${upperFirst(camelCase(fieldName))}(value)`
62
-
63
- Omitting fields:
64
- - If a field getter returns `undefined`, the field is **not added** to the payload object.
65
- - Other values (`null`, `false`, `0`, `''`, empty arrays/objects) are included as-is.
66
-
67
- Examples:
68
-
69
- ````typescript
70
- // state.name -> payload.name (trimmed)
71
- protected getName(value: string): string {
72
- return value.trim()
73
- }
74
- ````
75
-
76
- This works for arrays too (plain arrays and `PropertyAwareArray`):
77
-
78
- ````typescript
79
- // state.positions -> payload.positions (mapped)
80
- protected getPositions(positions: PositionItem[]): Array<{ id: number }> {
81
- return positions.map((p) => ({ id: p.id }))
82
- }
83
- ````
84
-
85
- #### B) Composite Getter for Nested Props (automatic fallback)
86
- If you do *not* provide a field getter for a given top-level field, `BaseForm` will recursively walk objects/arrays and
87
- allow transforming nested properties via composite getter names.
88
-
89
- Composite getter format: `get${upperFirst(parentFieldKey)}${upperFirst(camelCase(propName))}(value)`
90
-
91
- Example (field key `businessAssociate`, prop `id`):
92
-
93
- ````typescript
94
- // state.businessAssociate.id -> payload.businessAssociate.id (replaced with the resource id)
95
- protected getBusinessAssociateId(value: BusinessAssociateResource | null): string | null {
96
- return value?.id ?? null
97
- }
98
- ````
99
-
100
- Notes:
101
- - This applies to arrays of objects too, because arrays are mapped recursively.
102
- - The “parent field” part uses the original field key with only the first character uppercased (not camel-cased).
103
- - Returning `undefined` from a composite getter omits that nested property from the payload object.
104
-
105
- #### C) Appended / Computed Payload Fields (advanced)
106
- If you need payload fields that do not exist in `state`, add their names to `append`. `buildPayload()` will then call a
107
- zero-argument getter for each appended field.
108
-
109
- Example (append key `started_at` → getter `getStartedAt()`):
110
-
111
- ````typescript
112
- protected override append: string[] = ['started_at']
113
-
114
- protected getStartedAt(): string {
115
- return DateTime.fromFormat(`${this.state.start_date} ${this.state.start_time}`, 'dd.MM.yyyy HH:mm').toISO()
116
- }
117
- ````
118
-
119
- ### 4. Error Handling and Validation
120
-
121
- Map server-side validation errors to specific form fields, with support for nested fields:
122
-
123
- ````typescript
124
- protected override errorMap: { [serverKey: string]: string | string[] } = {
125
- started_at: ['start_date', 'start_time'],
126
- ended_at: ['end_date', 'end_time']
127
- }
128
- ````
129
-
130
- Validation is configured by overriding `defineRules()` and returning per-field rules and an optional validation mode:
131
-
132
- - `ValidationMode.DEFAULT` (default): validates on dirty, touch, and submit
133
- - `ValidationMode.PASSIVE`: only validates on submit
134
- - `ValidationMode.AGGRESSIVE`: validates immediately and on all triggers
135
- - `ValidationMode.ON_DEPENDENT_CHANGE`: revalidates when a dependency changes (see below)
136
-
137
- Rules can declare dependencies via `rule.dependsOn = ['otherField']`. Some rules (e.g. `ConfirmedRule`) implement
138
- bidirectional dependencies, so changing either field revalidates the other.
139
-
140
- ### 5. Array Management
141
-
142
- Special support for arrays with the class `PropertyAwareArray`, enabling reactive updates to array items:
143
-
144
- ````typescript
145
- public addPosition(): void {
146
- this.addToArrayProperty('positions', {
147
- index: this.properties.positions.length + 1,
148
- gross_amount: null,
149
- vat_rate: VatRateEnum.VAT_RATE_19,
150
- booking_account_category_id: null
151
- })
152
- }
153
- ````
154
-
155
- ## Core Concepts
156
-
157
- ### State and Dirty Tracking
158
-
159
- `BaseForm` tracks the original state and current state of each form field, automatically computing "dirty" status for
160
- fields that have been changed.
161
-
162
- ````typescript
163
- // Check if any field in the form has been modified
164
- form.isDirty()
165
-
166
- // Check if a specific field has been modified
167
- form.isDirty('email')
168
- ````
169
-
170
- ### Touched Tracking
171
-
172
- Touched indicates user interaction (or programmatic updates via setters/fill methods).
173
-
174
- ````typescript
175
- form.touch('email')
176
- form.isTouched('email')
177
- ````
178
-
179
- ### The Properties Object
180
-
181
- The `properties` getter provides access to each form field with its model, errors, and dirty status:
182
-
183
- ````html
184
-
185
- <template>
186
- <input v-model="form.properties.email.model.value" />
187
- <div v-if="form.properties.email.dirty">This field has been changed</div>
188
- <div v-if="form.properties.email.errors.length">{{ form.properties.email.errors[0] }}</div>
189
- </template>
190
- ````
191
-
192
- `properties.<field>` exposes:
193
- - `model` (a `ComputedRef` compatible with `v-model`)
194
- - `errors` (array; empty until validated/filled)
195
- - `dirty` and `touched`
196
-
197
- ### Form Submission
198
-
199
- Build a payload for API submission with:
200
-
201
- ````typescript
202
- const payload = form.buildPayload()
203
- ````
204
-
205
- For validation on submit, call:
206
-
207
- ````typescript
208
- const ok = form.validate(true)
209
- if (!ok) return
210
- await api.submitForm(form.buildPayload())
211
- ````
212
-
213
- ## How to Use
214
-
215
- ### 1. Create a Form Class
216
-
217
- ````typescript
218
- import { BaseForm, type PersistenceDriver, SessionStorageDriver } from '@hank-it/ui/vue/forms'
219
- import { RequiredRule, ValidationMode } from '@hank-it/ui/vue/forms/validation'
220
-
221
- interface MyFormState {
222
- name: string
223
- email: string
224
- }
225
-
226
- interface MyRequestPayload {
227
- name: string
228
- email: string
229
- timestamp: string // Added field not in the form
230
- }
231
-
232
- class MyForm extends BaseForm<MyRequestPayload, MyFormState> {
233
- // Fields to add to the final payload that aren't in the form state
234
- protected override append: string[] = ['timestamp']
235
-
236
- // Fields to exclude from the final payload
237
- protected override ignore: string[] = []
238
-
239
- // Map server error keys to form field names
240
- protected override errorMap: { [serverKey: string]: string | string[] } = {}
241
-
242
- public constructor() {
243
- super({
244
- name: '',
245
- email: ''
246
- }, { persist: true, persistSuffix: 'optional-suffix' })
247
- }
248
-
249
- // Use session storage for persistence
250
- protected override getPersistenceDriver(suffix?: string): PersistenceDriver {
251
- return new SessionStorageDriver(suffix)
252
- }
253
-
254
- protected override defineRules() {
255
- return {
256
- name: { rules: [new RequiredRule<MyFormState>('Name is required')] },
257
- email: {
258
- rules: [new RequiredRule<MyFormState>('Email is required')],
259
- options: { mode: ValidationMode.DEFAULT }
260
- }
261
- }
262
- }
263
-
264
- // Generate a timestamp for the request
265
- protected getTimestamp(): string {
266
- return new Date().toISOString()
267
- }
268
- }
269
- ````
270
-
271
- ### 2. Use in Components
272
-
273
- ````vue
274
- <template>
275
- <form @submit.prevent="submitForm">
276
- <div>
277
- <label>Name</label>
278
- <input v-model="form.properties.name.model.value" />
279
- <div v-if="form.properties.name.errors.length" class="error">
280
- {{ form.properties.name.errors[0] }}
281
- </div>
282
- </div>
283
-
284
- <div>
285
- <label>Email</label>
286
- <input v-model="form.properties.email.model.value" />
287
- <div v-if="form.properties.email.errors.length" class="error">
288
- {{ form.properties.email.errors[0] }}
289
- </div>
290
- </div>
291
-
292
- <button type="submit" :disabled="!form.isDirty()">Submit</button>
293
- <button type="button" @click="form.reset()">Reset</button>
294
- </form>
295
- </template>
296
-
297
- <script setup>
298
- import { MyForm } from './MyForm'
299
-
300
- const form = new MyForm()
301
-
302
- async function submitForm() {
303
- if (!form.validate(true)) return
304
- try {
305
- const payload = form.buildPayload()
306
- await api.submitForm(payload)
307
- // Success handling
308
- } catch (error) {
309
- if (error.response?.data?.errors) {
310
- form.fillErrors(error.response.data.errors)
311
- }
312
- }
313
- }
314
- </script>
315
- ````
316
-
317
- ## Working with Arrays
318
- The `PropertyAwareArray` class enables special handling for array items. Each value of objects in the PropertyAwareArray will receive a v-model, errors, etc.:
319
-
320
- ````typescript
321
- import { BaseForm, PropertyAwareArray } from '@hank-it/ui/vue/forms'
322
-
323
- export interface FormWithPositions {
324
- // ...other fields
325
- positions: PositionItem[]
326
- }
327
-
328
- export class MyComplexForm extends BaseForm<RequestType, FormWithPositions> {
329
- constructor() {
330
- super({
331
- // ...other defaults
332
- positions: new PropertyAwareArray([
333
- { id: 1, value: '' }
334
- ])
335
- })
336
- }
337
-
338
- // Add a new position to the array
339
- public addPosition(): void {
340
- this.addToArrayProperty('positions', {
341
- id: this.properties.positions.length + 1,
342
- value: ''
343
- })
344
- }
345
-
346
- // Remove a position by id
347
- public removePosition(id: number): void {
348
- this.removeArrayItem('positions', (position) => position.id !== id)
349
- this.resetArrayCounter('positions', 'id')
350
- }
351
- }
352
-
353
- const form = new MyComplexForm()
354
- const id = form.properties.positions[0].id.model.value
355
- ````
356
-
357
- ## Advanced Features
358
- ### 1. Form Reset
359
- Revert all changes to the original state:
360
-
361
- ````typescript
362
- form.reset()
363
- ````
364
-
365
- ### 2. Error Handling
366
- Fill form with validation errors from a server response:
367
-
368
- ````typescript
369
- try {
370
- await submitForm(form.buildPayload())
371
- } catch (error) {
372
- form.fillErrors(error.response.data.errors)
373
- }
374
- ````
375
-
376
- `fillErrors` supports:
377
- - direct field keys (e.g. `email`)
378
- - array dot notation where the 2nd segment is a numeric index (e.g. `positions.0.value`)
379
- - remapping via `errorMap` (including mapping one server key to multiple fields)
380
-
381
- ### 3. Filling Form State
382
- Update multiple form fields at once and recompute dirty/touched accordingly:
383
-
384
- ````typescript
385
- form.fillState({
386
- name: 'John Doe',
387
- email: 'john@example.com'
388
- })
389
- ````
390
-
391
- ### 4. Synchronizing Values Without Marking Dirty
392
- Update both the current and original state, keeping the field "clean":
393
-
394
- ````typescript
395
- form.syncValue('email', 'new@example.com')
396
- ````
397
-
398
- ### 5. Converting `properties` Back To Data
399
- If you ever need a plain object from the `properties` tree (e.g. for debugging or integrating with non-`BaseForm` code),
400
- use `propertyAwareToRaw`:
401
-
402
- ````typescript
403
- import { propertyAwareToRaw } from '@hank-it/ui/vue/forms'
404
-
405
- const raw = propertyAwareToRaw<MyFormState>(form.properties)
406
- ````
407
-
408
- ### 6. Checking For Errors
409
-
410
- ````typescript
411
- form.hasErrors()
412
- ````
413
-
414
- ## Files / Uploads (Multipart)
415
- If your form includes a file, keep it in state as `File | null` and disable persistence:
416
-
417
- ````typescript
418
- interface UploadFormBody {
419
- name: string
420
- file: File | null
421
- }
422
-
423
- class UploadForm extends BaseForm<RequestBody, UploadFormBody> {
424
- constructor() {
425
- super({ name: '', file: null }, { persist: false })
426
- }
427
- }
428
- ````
429
-
430
- `buildPayload()` keeps `File`/`Blob` values intact, so you can send the payload using a multipart `FormData` request body
431
- (e.g. the request layer’s `FormDataFactory` / `FormDataBody`).
432
-
433
- If `file` is `null`, `FormDataBody` encodes it as an empty string (the key stays present). Many backends (e.g. Laravel with
434
- `ConvertEmptyStringsToNull`) will treat that as `null` again.
435
-
436
- ## Real-World Examples
437
- ### 1. Date/Time Handling
438
-
439
- This example shows the “appended/computed fields” pattern: `started_at` and `ended_at` are not part of the form state,
440
- so they are listed in `append`, and `buildPayload()` calls `getStartedAt()` / `getEndedAt()` (no arguments).
441
-
442
- ````typescript
443
- export class TimeTrackingEntryCreateUpdateForm extends BaseForm<RequestPayload, FormState> {
444
- protected override append: string[] = ['started_at', 'ended_at']
445
- protected override ignore: string[] = ['start_date', 'start_time', 'end_date', 'end_time']
446
-
447
- protected getStartedAt(): string {
448
- return DateTime.fromFormat(`${this.state.start_date} ${this.state.start_time}`, 'dd.MM.yyyy HH:mm').toISO()
449
- }
450
-
451
- protected getEndedAt(): string {
452
- return DateTime.fromFormat(`${this.state.end_date} ${this.state.end_time}`, 'dd.MM.yyyy HH:mm').toISO()
453
- }
454
- }
455
- ````
456
-
457
- ### 2. Complex Object Handling
458
-
459
- If your form state contains nested objects, `buildPayload()` can transform individual nested properties via
460
- composite getter names of the form `get<ParentField><NestedProp>()`, where:
461
- - `ParentField` is based on the form field key (first character uppercased, not camel-cased)
462
- - `NestedProp` is `upperFirst(camelCase(prop))`
463
-
464
- Example (field key `businessAssociate`, prop `id`): `getBusinessAssociateId(...)`.
465
-
466
- ````typescript
467
- export class IncomingVoucherCreateUpdateForm extends BaseForm<RequestPayload, FormState> {
468
- // Extract IDs from related objects
469
- protected getBusinessAssociateId(value: BusinessAssociateResource): string | null {
470
- return value?.id
471
- }
472
-
473
- protected getFileId(value: FileResource): string | null {
474
- return value?.id
475
- }
476
- }
477
- ````
Binary file
@@ -1,14 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" class="h-full">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Hank-IT UI</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- </head>
8
- <body class="h-full">
9
-
10
- <div id="app" class="w-full h-full"></div>
11
-
12
- <script type="module" src="/js/app.js"></script>
13
- </body>
14
- </html>
@@ -1,8 +0,0 @@
1
- import { createApp } from 'vue'
2
- import App from './view/App.vue'
3
- import Router from './router'
4
-
5
- const app = createApp(App)
6
- .use(Router)
7
-
8
- app.mount('#app')
@@ -1,22 +0,0 @@
1
- import { createWebHistory, createRouter } from 'vue-router'
2
-
3
- const routes = [
4
- {
5
- path: '/pagination/:component?',
6
- component: () => import('@view/pagination/Pagination.vue'),
7
- name: 'pagination',
8
- props: true,
9
- },
10
- {
11
- path: '/requests/:component?',
12
- component: () => import('@view/requests/Requests.vue'),
13
- name: 'requests',
14
- props: true,
15
- }
16
- ]
17
-
18
-
19
- export default createRouter({
20
- history: createWebHistory(),
21
- routes, // short for `routes: routes`,
22
- })
@@ -1,49 +0,0 @@
1
- <template>
2
- <div class="container">
3
- <div class="sidebar">
4
- <ul>
5
- <li><RouterLink :to="{ name: 'requests' }">Requests</RouterLink></li>
6
- <li><RouterLink :to="{ name: 'pagination' }">Pagination</RouterLink></li>
7
- </ul>
8
- </div>
9
- <div class="content">
10
- <RouterView />
11
- </div>
12
- </div>
13
- </template>
14
-
15
- <script setup>
16
-
17
- </script>
18
-
19
- <style>
20
- body {
21
- margin: 0;
22
- padding:0;
23
- }
24
-
25
- .container {
26
- display: flex;
27
- flex-direction: row;
28
- }
29
-
30
- .sidebar,
31
- .content {
32
- display: flex;
33
- flex-direction: column;
34
- color: black;
35
- min-height: 500px;
36
- border-radius: 4px;
37
- margin: 10px;
38
- }
39
-
40
- .sidebar {
41
- flex-grow: 1;
42
- min-width: 300px;
43
- }
44
-
45
- .content {
46
- flex-grow: 5;
47
- min-width: 630px;
48
- }
49
- </style>
@@ -1,28 +0,0 @@
1
- <template>
2
- <select v-model="selectedComponentKey">
3
- <RouterLink
4
- v-for="(component, key) in components"
5
- :key="key"
6
- :to="component.route"
7
- custom
8
- v-slot="{ isActive, href, navigate }"
9
- >
10
- <option @click="navigate" :value="key">
11
- {{ component.name }}
12
- </option>
13
- </RouterLink>
14
- </select>
15
-
16
- <component v-if="selectedComponentKey" :is="components[selectedComponentKey].component" />
17
- </template>
18
-
19
- <script setup lang="ts">
20
- import {ref} from 'vue'
21
-
22
- const props = defineProps({
23
- components: {},
24
- component: String,
25
- })
26
-
27
- const selectedComponentKey = ref(props.component)
28
- </script>
@@ -1,28 +0,0 @@
1
- <template>
2
- <DemoPage :components="components" />
3
- </template>
4
-
5
- <script setup lang="ts">
6
- import TablePagination from './components/tablePagination/TablePagination.vue'
7
- import ErrorPagination from './components/errorPagination/ErrorPagination.vue'
8
- import InfiniteScrolling from './components/infiniteScrolling/InfiniteScrolling.vue'
9
- import DemoPage from '../layout/DemoPage.vue'
10
-
11
- const components = {
12
- 'table-pagination': {
13
- name: 'Paginated table',
14
- component: TablePagination,
15
- route: { name: 'pagination', params: { component: 'table-pagination' } },
16
- },
17
- 'error-pagination': {
18
- name: 'Pagination with error',
19
- component: ErrorPagination,
20
- route: { name: 'pagination', params: { component: 'error-pagination' } },
21
- },
22
- 'infinite-scrolling': {
23
- name: 'Infinite scrolling',
24
- component: InfiniteScrolling,
25
- route: { name: 'pagination', params: { component: 'infinite-scrolling' } },
26
- },
27
- }
28
- </script>
@@ -1,71 +0,0 @@
1
- <template>
2
- <table>
3
- <tr>
4
- <th>ID</th>
5
- <th>Title</th>
6
- <th>Description</th>
7
- </tr>
8
- <tr v-for="row in paginator.getPageData()">
9
- <td>{{ row.id }}</td>
10
- <td>{{ row.title }}</td>
11
- <td>{{ row.description }}</td>
12
- </tr>
13
- </table>
14
-
15
- Current page: {{ paginator.getCurrentPage() }}
16
- <br>
17
- Pages: {{ paginator.getPages() }}
18
- <br>
19
- Page Size: <input v-model="pageSize" />
20
- <br>
21
- Back Page <button @click="paginator.toPreviousPage()">Back</button>
22
- <br>
23
- Next Page <button @click="paginator.toNextPage()">Next</button>
24
- <br>
25
- Showing {{ paginator.getFromItemNumber() }} to {{ paginator.getToItemNumber() }} of {{ paginator.getTotal() }} items.
26
-
27
- {{ displayablePages }}
28
- </template>
29
-
30
- <script setup lang="ts">
31
- import {BaseRequest, FetchDriver, VueLoaderDriverFactory} from "@hank-it/ui/service/requests"
32
- import {Paginator, RequestDriver, VuePaginationDriverFactory} from "@hank-it/ui/service/pagination";
33
- import {getDisplayablePages} from '@hank-it/ui/service/helpers'
34
- import {GetProductsRequest} from "./GetProductsRequest";
35
- import {computed} from 'vue'
36
-
37
- /* Booting */
38
- BaseRequest.setRequestDriver(new FetchDriver)
39
- BaseRequest.setLoaderStateFactory(new VueLoaderDriverFactory)
40
- Paginator.setViewDriverFactory(new VuePaginationDriverFactory())
41
-
42
- /* component */
43
- const getProductsRequest = new GetProductsRequest
44
-
45
- const paginator = new Paginator(new RequestDriver(getProductsRequest))
46
-
47
- paginator.init(1, 10).catch(paginationErrorHandler)
48
-
49
- function paginationErrorHandler(response) {
50
- console.log(response.getError())
51
- }
52
-
53
- const pageSize = computed({
54
- set(value) {
55
- paginator.setPageSize(value)
56
- },
57
- get() {
58
- return paginator.getPageSize()
59
- }
60
- })
61
-
62
- const displayablePages = computed(() => {
63
- console.log(paginator.getPages())
64
-
65
- return getDisplayablePages(paginator.getPages().length, paginator.getCurrentPage())
66
- })
67
- </script>
68
-
69
- <style scoped>
70
-
71
- </style>