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