@blueprint-ts/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.editorconfig +508 -0
  2. package/.eslintrc.cjs +15 -0
  3. package/.prettierrc.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.md +1 -0
  6. package/docker-compose.yaml +8 -0
  7. package/docs/.vitepress/config.ts +68 -0
  8. package/docs/.vitepress/theme/Layout.vue +14 -0
  9. package/docs/.vitepress/theme/components/VersionSelector.vue +64 -0
  10. package/docs/.vitepress/theme/index.js +13 -0
  11. package/docs/index.md +70 -0
  12. package/docs/services/laravel/pagination.md +54 -0
  13. package/docs/services/laravel/requests.md +62 -0
  14. package/docs/services/requests/index.md +74 -0
  15. package/docs/vue/forms.md +326 -0
  16. package/docs/vue/requests/route-model-binding.md +66 -0
  17. package/docs/vue/state.md +293 -0
  18. package/env.d.ts +1 -0
  19. package/eslint.config.js +15 -0
  20. package/examples/files/7z2404-x64.exe +0 -0
  21. package/examples/index.html +14 -0
  22. package/examples/js/app.js +8 -0
  23. package/examples/js/router.js +22 -0
  24. package/examples/js/view/App.vue +49 -0
  25. package/examples/js/view/layout/DemoPage.vue +28 -0
  26. package/examples/js/view/pagination/Pagination.vue +28 -0
  27. package/examples/js/view/pagination/components/errorPagination/ErrorPagination.vue +71 -0
  28. package/examples/js/view/pagination/components/errorPagination/GetProductsRequest.ts +54 -0
  29. package/examples/js/view/pagination/components/infiniteScrolling/GetProductsRequest.ts +50 -0
  30. package/examples/js/view/pagination/components/infiniteScrolling/InfiniteScrolling.vue +57 -0
  31. package/examples/js/view/pagination/components/tablePagination/GetProductsRequest.ts +50 -0
  32. package/examples/js/view/pagination/components/tablePagination/TablePagination.vue +63 -0
  33. package/examples/js/view/requests/Requests.vue +34 -0
  34. package/examples/js/view/requests/components/abortableRequest/AbortableRequest.vue +36 -0
  35. package/examples/js/view/requests/components/abortableRequest/GetProductsRequest.ts +25 -0
  36. package/examples/js/view/requests/components/fileDownloadRequest/DownloadFileRequest.ts +15 -0
  37. package/examples/js/view/requests/components/fileDownloadRequest/FileDownloadRequest.vue +44 -0
  38. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetProductsRequest.ts +34 -0
  39. package/examples/js/view/requests/components/getRequestWithDynamicParams/GetRequestWithDynamicParams.vue +59 -0
  40. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.ts +21 -0
  41. package/examples/js/view/requests/components/serverErrorRequest/ServerErrorRequest.vue +53 -0
  42. package/package.json +81 -0
  43. package/release-tool.json +7 -0
  44. package/src/helpers.ts +78 -0
  45. package/src/service/bulkRequests/BulkRequestEvent.enum.ts +4 -0
  46. package/src/service/bulkRequests/BulkRequestSender.ts +184 -0
  47. package/src/service/bulkRequests/BulkRequestWrapper.ts +49 -0
  48. package/src/service/bulkRequests/index.ts +6 -0
  49. package/src/service/laravel/pagination/contracts/PaginationParamsContract.ts +4 -0
  50. package/src/service/laravel/pagination/contracts/PaginationResponseBodyContract.ts +6 -0
  51. package/src/service/laravel/pagination/dataDrivers/RequestDriver.ts +32 -0
  52. package/src/service/laravel/pagination/index.ts +7 -0
  53. package/src/service/laravel/requests/JsonBaseRequest.ts +35 -0
  54. package/src/service/laravel/requests/PaginationJsonBaseRequest.ts +29 -0
  55. package/src/service/laravel/requests/index.ts +9 -0
  56. package/src/service/laravel/requests/responses/JsonResponse.ts +8 -0
  57. package/src/service/laravel/requests/responses/PaginationResponse.ts +16 -0
  58. package/src/service/pagination/InfiniteScroller.ts +21 -0
  59. package/src/service/pagination/Paginator.ts +149 -0
  60. package/src/service/pagination/contracts/PaginateableRequestContract.ts +13 -0
  61. package/src/service/pagination/contracts/PaginationDataDriverContract.ts +5 -0
  62. package/src/service/pagination/contracts/PaginationResponseContract.ts +7 -0
  63. package/src/service/pagination/contracts/PaginatorLoadDataOptions.ts +4 -0
  64. package/src/service/pagination/contracts/ViewDriverContract.ts +12 -0
  65. package/src/service/pagination/contracts/ViewDriverFactoryContract.ts +5 -0
  66. package/src/service/pagination/dataDrivers/ArrayDriver.ts +28 -0
  67. package/src/service/pagination/dtos/PaginationDataDto.ts +14 -0
  68. package/src/service/pagination/factories/VuePaginationDriverFactory.ts +9 -0
  69. package/src/service/pagination/frontendDrivers/VuePaginationDriver.ts +61 -0
  70. package/src/service/pagination/index.ts +16 -0
  71. package/src/service/persistenceDrivers/LocalStorageDriver.ts +22 -0
  72. package/src/service/persistenceDrivers/NonPersistentDriver.ts +12 -0
  73. package/src/service/persistenceDrivers/SessionStorageDriver.ts +22 -0
  74. package/src/service/persistenceDrivers/index.ts +8 -0
  75. package/src/service/persistenceDrivers/types/PersistenceDriver.ts +5 -0
  76. package/src/service/requests/BaseRequest.ts +197 -0
  77. package/src/service/requests/ErrorHandler.ts +64 -0
  78. package/src/service/requests/RequestEvents.enum.ts +3 -0
  79. package/src/service/requests/RequestMethod.enum.ts +8 -0
  80. package/src/service/requests/bodies/FormDataBody.ts +41 -0
  81. package/src/service/requests/bodies/JsonBody.ts +16 -0
  82. package/src/service/requests/contracts/AbortableRequestContract.ts +3 -0
  83. package/src/service/requests/contracts/BaseRequestContract.ts +36 -0
  84. package/src/service/requests/contracts/BodyContract.ts +7 -0
  85. package/src/service/requests/contracts/BodyFactoryContract.ts +5 -0
  86. package/src/service/requests/contracts/DriverConfigContract.ts +7 -0
  87. package/src/service/requests/contracts/HeadersContract.ts +5 -0
  88. package/src/service/requests/contracts/RequestDriverContract.ts +15 -0
  89. package/src/service/requests/contracts/RequestLoaderContract.ts +5 -0
  90. package/src/service/requests/contracts/RequestLoaderFactoryContract.ts +5 -0
  91. package/src/service/requests/contracts/ResponseContract.ts +7 -0
  92. package/src/service/requests/drivers/contracts/ResponseHandlerContract.ts +10 -0
  93. package/src/service/requests/drivers/fetch/FetchDriver.ts +115 -0
  94. package/src/service/requests/drivers/fetch/FetchResponse.ts +30 -0
  95. package/src/service/requests/exceptions/NoResponseReceivedException.ts +3 -0
  96. package/src/service/requests/exceptions/NotFoundException.ts +3 -0
  97. package/src/service/requests/exceptions/PageExpiredException.ts +3 -0
  98. package/src/service/requests/exceptions/ResponseBodyException.ts +15 -0
  99. package/src/service/requests/exceptions/ResponseException.ts +11 -0
  100. package/src/service/requests/exceptions/ServerErrorException.ts +3 -0
  101. package/src/service/requests/exceptions/UnauthorizedException.ts +3 -0
  102. package/src/service/requests/exceptions/ValidationException.ts +3 -0
  103. package/src/service/requests/exceptions/index.ts +19 -0
  104. package/src/service/requests/factories/FormDataFactory.ts +9 -0
  105. package/src/service/requests/factories/JsonBodyFactory.ts +9 -0
  106. package/src/service/requests/index.ts +50 -0
  107. package/src/service/requests/responses/BaseResponse.ts +41 -0
  108. package/src/service/requests/responses/BlobResponse.ts +19 -0
  109. package/src/service/requests/responses/JsonResponse.ts +15 -0
  110. package/src/service/requests/responses/PlainTextResponse.ts +15 -0
  111. package/src/service/support/DeferredPromise.ts +67 -0
  112. package/src/service/support/index.ts +3 -0
  113. package/src/vue/composables/useConfirmDialog.ts +59 -0
  114. package/src/vue/composables/useGlobalCheckbox.ts +145 -0
  115. package/src/vue/composables/useIsEmpty.ts +34 -0
  116. package/src/vue/composables/useIsOpen.ts +37 -0
  117. package/src/vue/composables/useIsOpenFromVar.ts +61 -0
  118. package/src/vue/composables/useModelWrapper.ts +24 -0
  119. package/src/vue/composables/useOnOpen.ts +34 -0
  120. package/src/vue/contracts/ModelValueOptions.ts +3 -0
  121. package/src/vue/contracts/ModelValueProps.ts +3 -0
  122. package/src/vue/forms/BaseForm.ts +1074 -0
  123. package/src/vue/forms/PropertyAwareArray.ts +78 -0
  124. package/src/vue/forms/index.ts +11 -0
  125. package/src/vue/forms/types/PersistedForm.ts +6 -0
  126. package/src/vue/forms/validation/ValidationMode.enum.ts +14 -0
  127. package/src/vue/forms/validation/index.ts +12 -0
  128. package/src/vue/forms/validation/rules/BaseRule.ts +7 -0
  129. package/src/vue/forms/validation/rules/ConfirmedRule.ts +39 -0
  130. package/src/vue/forms/validation/rules/MinRule.ts +61 -0
  131. package/src/vue/forms/validation/rules/RequiredRule.ts +19 -0
  132. package/src/vue/forms/validation/rules/UrlRule.ts +24 -0
  133. package/src/vue/forms/validation/types/BidirectionalRule.ts +11 -0
  134. package/src/vue/index.ts +14 -0
  135. package/src/vue/requests/factories/VueRequestLoaderFactory.ts +9 -0
  136. package/src/vue/requests/index.ts +5 -0
  137. package/src/vue/requests/loaders/VueRequestBatchLoader.ts +30 -0
  138. package/src/vue/requests/loaders/VueRequestLoader.ts +18 -0
  139. package/src/vue/router/routeModelBinding/RouteModelRequestResolver.ts +11 -0
  140. package/src/vue/router/routeModelBinding/defineRoute.ts +31 -0
  141. package/src/vue/router/routeModelBinding/index.ts +8 -0
  142. package/src/vue/router/routeModelBinding/installRouteInjection.ts +73 -0
  143. package/src/vue/router/routeModelBinding/types.ts +46 -0
  144. package/src/vue/state/State.ts +391 -0
  145. package/src/vue/state/index.ts +3 -0
  146. package/tests/service/helpers/mergeDeep.test.ts +53 -0
  147. package/tests/service/laravel/pagination/dataDrivers/RequestDriver.test.ts +84 -0
  148. package/tests/service/laravel/requests/JsonBaseRequest.test.ts +43 -0
  149. package/tests/service/laravel/requests/PaginationJsonBaseRequest.test.ts +58 -0
  150. package/tests/service/laravel/requests/responses/JsonResponse.test.ts +59 -0
  151. package/tests/service/laravel/requests/responses/PaginationResponse.test.ts +127 -0
  152. package/tests/service/pagination/dtos/PaginationDataDto.test.ts +35 -0
  153. package/tests/service/pagination/factories/VuePaginationDriverFactory.test.ts +32 -0
  154. package/tests/service/pagination/frontendDrivers/VuePaginationDriver.test.ts +66 -0
  155. package/tests/service/requests/ErrorHandler.test.ts +141 -0
  156. package/tsconfig.json +114 -0
  157. package/vite.config.ts +34 -0
  158. package/vitest.config.ts +14 -0
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+
4
+ const versions = ref<string[]>([])
5
+ const currentVersion = ref(getVersionFromPath() || 'latest')
6
+
7
+ function getVersionFromPath() {
8
+ if (typeof window === 'undefined') return null
9
+ return window.location.pathname.split('/')[2] || 'latest'
10
+ }
11
+
12
+ // Modified to load versions.json from the correct path
13
+ onMounted(async () => {
14
+ try {
15
+ // Use relative path to current location
16
+ const res = await fetch('./versions.json', { cache: 'no-store' })
17
+
18
+ if (!res.ok) {
19
+ throw new Error(`Failed to fetch versions: ${res.status}`)
20
+ }
21
+
22
+ versions.value = await res.json()
23
+ } catch (e) {
24
+ console.error('Failed to load versions:', e)
25
+ versions.value = ['latest']
26
+ }
27
+ })
28
+
29
+ function onSelect(event: Event) {
30
+ const selected = (event.target as HTMLSelectElement).value
31
+ // Preserve the path after the version segment
32
+ const pathParts = window.location.pathname.split('/')
33
+ const rest = pathParts.slice(3).join('/') // after /ui/<version>/
34
+ let newUrl = `/ui/${selected}/`
35
+ if (rest) newUrl += rest
36
+ newUrl += window.location.search + window.location.hash
37
+ window.location.href = newUrl
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div class="version-selector">
43
+ <select :value="currentVersion" @change="onSelect" class="version-select">
44
+ <option v-for="ver in versions" :key="ver" :value="ver">{{ ver }}</option>
45
+ </select>
46
+ </div>
47
+ </template>
48
+
49
+ <style scoped>
50
+ .version-selector {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ margin-right: 0.5em;
54
+ }
55
+
56
+ .version-select {
57
+ padding: 0.25em 0.5em;
58
+ border-radius: 4px;
59
+ border: 1px solid var(--vp-c-divider);
60
+ background-color: var(--vp-c-bg);
61
+ color: var(--vp-c-text-1);
62
+ font-size: 0.9em;
63
+ }
64
+ </style>
@@ -0,0 +1,13 @@
1
+ // .vitepress/theme/index.js
2
+ import DefaultTheme from 'vitepress/theme'
3
+ import VersionSelector from './components/VersionSelector.vue'
4
+ import Layout from './Layout.vue'
5
+
6
+ export default {
7
+ extends: DefaultTheme,
8
+ Layout,
9
+ enhanceApp({ app }) {
10
+ // Register global components
11
+ app.component('VersionSelector', VersionSelector)
12
+ }
13
+ }
package/docs/index.md ADDED
@@ -0,0 +1,70 @@
1
+ # Getting Started
2
+
3
+ This library can be integrated with any frontend framework. It comes with built-in support for Vue 3, which is assumed
4
+ throughout the documentation.
5
+
6
+ ````bash
7
+ npm install @hank-it/ui --save
8
+ ````
9
+
10
+ ## Request Handling
11
+
12
+ The library leverages a fetch-based driver to perform HTTP requests. The following sections explain how to initialize
13
+ the request driver and define custom requests.
14
+
15
+ ## Initializing the Request Driver
16
+
17
+ Before making any requests, you must initialize the appropriate request driver. This is done during your application's
18
+ boot process by using the static `setRequestDriver` method.
19
+
20
+ ### Using the Fetch Driver
21
+
22
+ To set up the fetch driver, import `BaseRequest` and `FetchDriver` from '@hank-it/ui/service/requests' and initialize
23
+ the driver as shown:
24
+
25
+ ```typescript
26
+ import { BaseRequest, FetchDriver } from '@hank-it/ui/service/requests'
27
+
28
+ BaseRequest.setRequestDriver(new FetchDriver())
29
+ ```
30
+
31
+ ### Enabling Credential Support
32
+
33
+ If your requests need to include credentials (e.g., cookies for cross-origin requests), enable credential support as
34
+ follows:
35
+
36
+ ```typescript
37
+ BaseRequest.setRequestDriver(new FetchDriver({
38
+ corsWithCredentials: true,
39
+ }))
40
+ ```
41
+
42
+ ### Adding Global Headers
43
+
44
+ To include headers such as a CSRF token with every request, define them globally:
45
+
46
+ ```typescript
47
+ BaseRequest.setRequestDriver(new FetchDriver({
48
+ headers: {
49
+ 'X-XSRF-TOKEN': "<token>",
50
+ },
51
+ }))
52
+ ```
53
+
54
+ Sometimes you want to refetch the header when the request is sent. You may specify a callback for this:
55
+
56
+ ```typescript
57
+ BaseRequest.setRequestDriver(new FetchDriver({
58
+ headers: {
59
+ 'X-XSRF-TOKEN': () => getCookie('XSRF-TOKEN')
60
+ },
61
+ }))
62
+ ```
63
+
64
+ ### Specifying a Base URL
65
+
66
+ In case your backend lives on a separate domain, you may specify a default base url, which is prepended to every request url:
67
+
68
+ ```typescript
69
+ BaseRequest.setDefaultBaseUrl('https://example.com')
70
+ ```
@@ -0,0 +1,54 @@
1
+ # Working with Laravel Pagination
2
+ The `PaginationJsonBaseRequest` class extends the functionality to handle Laravel's pagination response format.
3
+
4
+ ## Example: Paginated Users List
5
+
6
+ ````typescript
7
+ import { PaginationJsonBaseRequest } from '@hank-it/ui/service/laravel/requests'
8
+ import { PaginationResponse } from '@hank-it/ui/service/laravel/requests/responses'
9
+
10
+ export interface UserListParams {
11
+ search?: string
12
+ sort_by?: string
13
+ sort_direction?: 'asc' | 'desc'
14
+ page?: number
15
+ per_page?: number
16
+ }
17
+
18
+ // Paginated GET request to list users
19
+ export class UserIndexRequest extends PaginationJsonBaseRequest<
20
+ boolean, // Loading indicator type
21
+ LaravelErrorResponse, // Laravel-style error response
22
+ UserResource, // The resource type being paginated
23
+ undefined, // No request body for GET
24
+ UserListParams // Query parameters including pagination
25
+ > {
26
+ public method(): RequestMethodEnum {
27
+ return RequestMethodEnum.GET
28
+ }
29
+
30
+ public url(): string {
31
+ return '/api/users'
32
+ }
33
+ }
34
+ ````
35
+
36
+ And now we send the request using the paginator:
37
+
38
+ ````typescript
39
+ const request = new UserIndexRequest()
40
+
41
+ const paginator = new Paginator(new RequestDriver(request))
42
+
43
+ // Fetch the initial data
44
+ paginator.init(1, 10)
45
+
46
+ // Get current page data
47
+ paginator.getPageData()
48
+
49
+ // Change page
50
+ paginator.setPageSize(value)
51
+
52
+ // Get current page size
53
+ paginator.getPageSize()
54
+ ````
@@ -0,0 +1,62 @@
1
+ # Laravel Request Integration
2
+ The Laravel integration provides specialized request classes that work seamlessly with Laravel backend APIs, handling common Laravel-specific response formats and features like pagination.
3
+
4
+ ## Using JsonBaseRequest
5
+ The `JsonBaseRequest` class is designed to work with Laravel's JSON responses, automatically handling content negotiation and response parsing.
6
+
7
+ ### Example: User API Request
8
+
9
+ We assume that Laravel's resources are used which output the requested data on the `data` json key.
10
+
11
+ ````typescript
12
+ import { JsonBaseRequest } from '@hank-it/ui/service/laravel/requests'
13
+
14
+ export interface LaravelErrorResponse {
15
+ message: string;
16
+ errors?: Record<string, string[]>
17
+ }
18
+
19
+ export interface UserResource {
20
+ id: number
21
+ name: string
22
+ email: string
23
+ created_at: string
24
+ }
25
+
26
+ export interface UserRequestParams {
27
+ include?: string[]
28
+ }
29
+
30
+ // Simple GET request to fetch a user
31
+ export class UserShowRequest extends JsonBaseRequest<
32
+ boolean, // Loading indicator type
33
+ LaravelErrorResponse, // Laravel-style error response
34
+ UserResource, // Response data structure
35
+ undefined, // No request body for GET
36
+ UserRequestParams // Query parameters
37
+ > {
38
+ constructor(private userId: number) {
39
+ super()
40
+ }
41
+
42
+ public method(): RequestMethodEnum {
43
+ return RequestMethodEnum.GET
44
+ }
45
+
46
+ public url(): string {
47
+ return `/api/users/${this.userId}`
48
+ }
49
+ }
50
+ ````
51
+
52
+ ## Sending the Request
53
+
54
+ Once the request is defined, you can send it using the following code:
55
+
56
+ ```typescript
57
+ const request = new UserShowRequest()
58
+
59
+ const response: JsonResponse<UserResource> = await request.send()
60
+
61
+ const data: UserResource[] = response.getData()
62
+ ```
@@ -0,0 +1,74 @@
1
+ # Defining Requests
2
+
3
+ Each API endpoint is represented as a separate class that extends `BaseRequest`. This class specifies the HTTP Method,
4
+ URL, and the expected request/response types.
5
+
6
+ ## Example: Expense Index Request
7
+
8
+ The following example demonstrates how to define a GET request to the `/api/v1/expenses` endpoint:
9
+
10
+ ```typescript
11
+ import { BaseRequest, RequestMethodEnum, JsonResponse } from '@hank-it/ui/service/requests'
12
+
13
+ export interface GenericResponseErrorInterface {
14
+ message: string
15
+ }
16
+
17
+ export interface ExpenseIndexRequestParams {
18
+ filter?: {
19
+ search_text?: string
20
+ };
21
+ }
22
+
23
+ export interface ExpenseResource {
24
+ id: string;
25
+ // other data fields
26
+ }
27
+
28
+ export interface ExpenseIndexRequestResponseBody {
29
+ data: ExpenseResource[]
30
+ }
31
+
32
+ export class ExpenseIndexRequest extends BaseRequest<
33
+ boolean, // Generic RequestLoaderLoadingType
34
+ GenericResponseErrorInterface, // Generic ResponseErrorBody
35
+ ExpenseIndexRequestResponseBody, // Generic ResponseBodyInterface
36
+ JsonResponse<ExpenseIndexRequestResponseBody>, // Generic ResponseClass
37
+ undefined, // Generic RequestBodyInterface
38
+ ExpenseIndexRequestParams // RequestParamsInterface
39
+ > {
40
+ public method(): RequestMethodEnum {
41
+ return RequestMethodEnum.GET
42
+ }
43
+
44
+ public url(): string {
45
+ return '/api/v1/expenses'
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Explanation
51
+
52
+ - **HTTP Method**: Uses `GET` to retrieve data from the `/api/v1/expenses` endpoint.
53
+ - **Error Handling**: On failure (4XX/5XX status codes), the response will conform to `GenericResponseErrorInterface`.
54
+ - **Success Response**: A successful response is expected to follow the `ExpenseIndexRequestResponseBody` interface.
55
+ - **Response Format**: The response is of type JSON, as indicated by `JsonResponse`.
56
+ - **Request Body**: Since this is a GET request, the body is `undefined`.
57
+ - **Query Parameters**: Accepts query parameters that match the `ExpenseIndexRequestParams` interface.
58
+
59
+ ---
60
+
61
+ ## Sending the Request
62
+
63
+ Once the request is defined, you can send it using the following code:
64
+
65
+ ```typescript
66
+ const request = new ExpenseIndexRequest()
67
+
68
+ // The response type and body are inferred automatically.
69
+ const response: JsonResponse<ExpenseIndexRequestResponseBody> = await request.send()
70
+
71
+ const body = response.getBody() // Type: ExpenseIndexRequestResponseBody
72
+ ```
73
+
74
+ This completes the setup and usage of the request driver and custom requests. You can now use these patterns to create additional requests as needed.
@@ -0,0 +1,326 @@
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 state tracking for individual 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
+
15
+ ## Key Features
16
+
17
+ ### 1. Type-Safe Form Management
18
+
19
+ The `BaseForm` is a generic class that takes two type parameters:
20
+
21
+ - `RequestBody`: The shape of the data that will be sent to the server
22
+ - `FormBody`: The shape of the form's internal state, which can differ from the request payload
23
+
24
+ ````typescript
25
+ class MyForm extends BaseForm<MyRequestPayload, MyFormState> {
26
+ // ...
27
+ }
28
+ ````
29
+
30
+ ### 2. Form State Persistence
31
+
32
+ Forms can automatically save their state to browser storage (session, local, etc.), allowing users to navigate away and
33
+ return without losing their input.
34
+
35
+ ````typescript
36
+ protected override getPersistenceDriver(): PersistenceDriver
37
+ {
38
+ return new SessionStorageDriver() // Or LocalStorageDriver, etc.
39
+ }
40
+ ````
41
+
42
+ ### 3. Transformations and Getters
43
+
44
+ Transform form values before they are sent to the server using getter methods:
45
+
46
+ ````typescript
47
+ protected getStartedAt(): string {
48
+ return DateTime.fromFormat(`${this.state.start_date} ${this.state.start_time}`, 'dd.MM.yyyy HH:mm').toISO()
49
+ }
50
+ ````
51
+
52
+ ### 4. Error Handling and Validation
53
+
54
+ Map server-side validation errors to specific form fields, with support for nested fields:
55
+
56
+ ````typescript
57
+ protected override errorMap: { [serverKey: string]: string | string[] } = {
58
+ started_at: ['start_date', 'start_time'],
59
+ ended_at: ['end_date', 'end_time']
60
+ }
61
+ ````
62
+
63
+ ### 5. Array Management
64
+
65
+ Special support for arrays with the class `PropertyAwareArray`, enabling reactive updates to array items:
66
+
67
+ ````typescript
68
+ public addPosition(): void {
69
+ this.addToArrayProperty('positions', {
70
+ index: this.properties.positions.length + 1,
71
+ gross_amount: null,
72
+ vat_rate: VatRateEnum.VAT_RATE_19,
73
+ booking_account_category_id: null
74
+ })
75
+ }
76
+ ````
77
+
78
+ ## Core Concepts
79
+
80
+ ### State and Dirty Tracking
81
+
82
+ `BaseForm` tracks the original state and current state of each form field, automatically computing "dirty" status for
83
+ fields that have been changed.
84
+
85
+ ````typescript
86
+ // Check if any field in the form has been modified
87
+ form.isDirty()
88
+ ````
89
+
90
+ ### The Properties Object
91
+
92
+ The `properties` getter provides access to each form field with its model, errors, and dirty status:
93
+
94
+ ````html
95
+
96
+ <template>
97
+ <input v-model="form.properties.email.model.value" />
98
+ <div v-if="form.properties.email.dirty">This field has been changed</div>
99
+ <div v-if="form.properties.email.errors.length">{{ form.properties.email.errors[0] }}</div>
100
+ </template>
101
+ ````
102
+
103
+ ### Form Submission
104
+
105
+ Build a payload for API submission with:
106
+
107
+ ````typescript
108
+ const payload = form.buildPayload()
109
+ ````
110
+
111
+ ## How to Use
112
+
113
+ ### 1. Create a Form Class
114
+
115
+ ````typescript
116
+ import { BaseForm, type PersistenceDriver, SessionStorageDriver } from '@hank-it/ui/vue/forms'
117
+
118
+ interface MyFormState {
119
+ name: string
120
+ email: string
121
+ }
122
+
123
+ interface MyRequestPayload {
124
+ name: string
125
+ email: string
126
+ timestamp: string // Added field not in the form
127
+ }
128
+
129
+ class MyForm extends BaseForm<MyRequestPayload, MyFormState> {
130
+ // Fields to add to the final payload that aren't in the form state
131
+ protected override append: string[] = ['timestamp']
132
+
133
+ // Fields to exclude from the final payload
134
+ protected override ignore: string[] = []
135
+
136
+ // Map server error keys to form field names
137
+ protected override errorMap: { [serverKey: string]: string | string[] } = {}
138
+
139
+ public constructor() {
140
+ super({
141
+ name: '',
142
+ email: ''
143
+ }, { persist: true, persistSuffix: 'optional-suffix' })
144
+ }
145
+
146
+ // Use session storage for persistence
147
+ protected override getPersistenceDriver(): PersistenceDriver {
148
+ return new SessionStorageDriver()
149
+ }
150
+
151
+ // Generate a timestamp for the request
152
+ protected getTimestamp(): string {
153
+ return new Date().toISOString()
154
+ }
155
+ }
156
+ ````
157
+
158
+ ### 2. Use in Components
159
+
160
+ ````vue
161
+ <template>
162
+ <form @submit.prevent="submitForm">
163
+ <div>
164
+ <label>Name</label>
165
+ <input v-model="form.properties.name.model.value" />
166
+ <div v-if="form.properties.name.errors.length" class="error">
167
+ {{ form.properties.name.errors[0] }}
168
+ </div>
169
+ </div>
170
+
171
+ <div>
172
+ <label>Email</label>
173
+ <input v-model="form.properties.email.model.value" />
174
+ <div v-if="form.properties.email.errors.length" class="error">
175
+ {{ form.properties.email.errors[0] }}
176
+ </div>
177
+ </div>
178
+
179
+ <button type="submit" :disabled="!form.isDirty()">Submit</button>
180
+ <button type="button" @click="form.reset()">Reset</button>
181
+ </form>
182
+ </template>
183
+
184
+ <script setup>
185
+ import { MyForm } from './MyForm'
186
+
187
+ const form = new MyForm({
188
+ name: '',
189
+ email: ''
190
+ })
191
+
192
+ async function submitForm() {
193
+ try {
194
+ const payload = form.buildPayload()
195
+ await api.submitForm(payload)
196
+ // Success handling
197
+ } catch (error) {
198
+ if (error.response?.data?.errors) {
199
+ form.fillErrors(error.response.data.errors)
200
+ }
201
+ }
202
+ }
203
+ </script>
204
+ ````
205
+
206
+ ## Working with Arrays
207
+ The `PropertyAwareArray` class enables special handling for array items. Each value of objects in the PropertyAwareArray will receive a v-model, errors, etc.:
208
+
209
+ ````typescript
210
+ import { BaseForm, PropertyAwareArray } from '@hank-it/ui/vue/forms'
211
+
212
+ export interface FormWithPositions {
213
+ // ...other fields
214
+ positions: PositionItem[]
215
+ }
216
+
217
+ export class MyComplexForm extends BaseForm<RequestType, FormWithPositions> {
218
+ constructor() {
219
+ super({
220
+ // ...other defaults
221
+ positions: new PropertyAwareArray([
222
+ { id: 1, value: '' }
223
+ ])
224
+ })
225
+ }
226
+
227
+ // Add a new position to the array
228
+ public addPosition(): void {
229
+ this.addToArrayProperty('positions', {
230
+ id: this.properties.positions.length + 1,
231
+ value: ''
232
+ })
233
+ }
234
+
235
+ // Remove a position by id
236
+ public removePosition(id: number): void {
237
+ this.state.positions = new PropertyAwareArray(
238
+ this.state.positions.filter(position => position.id !== id)
239
+ )
240
+ this.resetPositionIds()
241
+ }
242
+
243
+ // Reset the sequential IDs after removing items
244
+ protected resetPositionIds(): void {
245
+ let count = 1
246
+ this.state.positions.forEach(position => {
247
+ position.id = count
248
+ count++
249
+ })
250
+ }
251
+ }
252
+
253
+ const form = new MyComplexForm()
254
+ const id = form.properties.positions[0].id.model.value
255
+ ````
256
+
257
+ ## Advanced Features
258
+ ### 1. Form Reset
259
+ Revert all changes to the original state:
260
+
261
+ ````typescript
262
+ form.reset()
263
+ ````
264
+
265
+ ### 2. Error Handling
266
+ Fill form with validation errors from a server response:
267
+
268
+ ````typescript
269
+ try {
270
+ await submitForm(form.buildPayload())
271
+ } catch (error) {
272
+ form.fillErrors(error.response.data.errors)
273
+ }
274
+ ````
275
+
276
+ The `fileErrors` method currently only supports the Laravel style dot notation errors.
277
+
278
+ ### 3. Filling Form State
279
+ Update multiple form fields at once, preserving the dirty state:
280
+
281
+ ````typescript
282
+ form.fillState({
283
+ name: 'John Doe',
284
+ email: 'john@example.com'
285
+ })
286
+ ````
287
+
288
+ ### 4. Synchronizing Values Without Marking Dirty
289
+ Update both the current and original state, keeping the field "clean":
290
+
291
+ ````typescript
292
+ form.syncValue('email', 'new@example.com')
293
+ ````
294
+
295
+ ## Real-World Examples
296
+ ### 1. Date/Time Handling
297
+
298
+ ````typescript
299
+ export class TimeTrackingEntryCreateUpdateForm extends BaseForm<RequestPayload, FormState> {
300
+ protected override append: string[] = ['started_at', 'ended_at']
301
+ protected override ignore: string[] = ['start_date', 'start_time', 'end_date', 'end_time']
302
+
303
+ protected getStartedAt(): string {
304
+ return DateTime.fromFormat(`${this.state.start_date} ${this.state.start_time}`, 'dd.MM.yyyy HH:mm').toISO()
305
+ }
306
+
307
+ protected getEndedAt(): string {
308
+ return DateTime.fromFormat(`${this.state.end_date} ${this.state.end_time}`, 'dd.MM.yyyy HH:mm').toISO()
309
+ }
310
+ }
311
+ ````
312
+
313
+ ### 2. Complex Object Handling
314
+
315
+ ````typescript
316
+ export class IncomingVoucherCreateUpdateForm extends BaseForm<RequestPayload, FormState> {
317
+ // Extract IDs from related objects
318
+ protected getBusinessAssociateId(value: BusinessAssociateResource): string | null {
319
+ return value?.id
320
+ }
321
+
322
+ protected getFileId(value: FileResource): string | null {
323
+ return value?.id
324
+ }
325
+ }
326
+ ````