@baklavue/composables 1.0.0-preview.1 → 1.0.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +181 -17
- package/alert.spec.ts +71 -0
- package/alert.ts +56 -0
- package/asyncState.spec.ts +114 -0
- package/asyncState.ts +84 -0
- package/base64.spec.ts +286 -0
- package/base64.ts +183 -0
- package/breakpoints.spec.ts +173 -0
- package/breakpoints.ts +135 -0
- package/clipboard.spec.ts +228 -0
- package/clipboard.ts +122 -0
- package/colorScheme.spec.ts +209 -0
- package/colorScheme.ts +155 -0
- package/confirmDialog.spec.ts +85 -0
- package/confirmDialog.ts +89 -0
- package/containerScroll.spec.ts +169 -0
- package/containerScroll.ts +94 -0
- package/cookie.spec.ts +248 -0
- package/cookie.ts +229 -0
- package/debounce.spec.ts +145 -0
- package/debounce.ts +94 -0
- package/disclosure.spec.ts +62 -0
- package/disclosure.ts +39 -0
- package/elementSize.spec.ts +175 -0
- package/elementSize.ts +96 -0
- package/fetch.spec.ts +754 -0
- package/fetch.ts +419 -0
- package/fieldArray.spec.ts +170 -0
- package/fieldArray.ts +155 -0
- package/file.spec.ts +544 -0
- package/file.ts +657 -0
- package/focusTrap.spec.ts +316 -0
- package/focusTrap.ts +149 -0
- package/formPersistence.spec.ts +105 -0
- package/formPersistence.ts +101 -0
- package/formState.spec.ts +150 -0
- package/formState.ts +175 -0
- package/formValidation.spec.ts +384 -0
- package/formValidation.ts +463 -0
- package/format.spec.ts +121 -0
- package/format.ts +95 -0
- package/id.spec.ts +34 -0
- package/id.ts +24 -0
- package/index.ts +169 -3
- package/infiniteQuery.spec.ts +364 -0
- package/infiniteQuery.ts +326 -0
- package/intersectionObserver.spec.ts +151 -0
- package/intersectionObserver.ts +100 -0
- package/lazyQuery.spec.ts +75 -0
- package/lazyQuery.ts +48 -0
- package/loading.spec.ts +143 -0
- package/loading.ts +80 -0
- package/mutation.spec.ts +184 -0
- package/mutation.ts +124 -0
- package/notification.spec.ts +79 -0
- package/package.json +15 -5
- package/pagination.spec.ts +101 -0
- package/pagination.ts +101 -0
- package/polling.spec.ts +198 -0
- package/polling.ts +98 -0
- package/previous.spec.ts +61 -0
- package/previous.ts +43 -0
- package/query.spec.ts +347 -0
- package/query.ts +414 -0
- package/raf.spec.ts +149 -0
- package/raf.ts +103 -0
- package/scrollLock.spec.ts +81 -0
- package/scrollLock.ts +61 -0
- package/scrollToError.spec.ts +344 -0
- package/scrollToError.ts +200 -8
- package/scrollVisibility.spec.ts +126 -0
- package/scrollVisibility.ts +66 -0
- package/share.spec.ts +194 -0
- package/share.ts +111 -0
- package/slug.spec.ts +68 -0
- package/slug.ts +65 -0
- package/stepper.spec.ts +175 -0
- package/stepper.ts +119 -0
- package/stepperForm.spec.ts +90 -0
- package/stepperForm.ts +81 -0
- package/sticky.spec.ts +150 -0
- package/sticky.ts +98 -0
- package/storage.spec.ts +276 -0
- package/storage.ts +201 -0
- package/tests/utils/withSetup.ts +18 -0
- package/theme.spec.ts +142 -0
- package/themePreset.spec.ts +86 -0
- package/themePreset.ts +64 -0
- package/throttle.spec.ts +220 -0
- package/throttle.ts +126 -0
- package/timer.spec.ts +163 -0
- package/timer.ts +117 -0
- package/toggle.spec.ts +56 -0
- package/toggle.ts +40 -0
- package/vitest.config.ts +21 -0
- package/csv.ts +0 -126
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
# [@baklavue/composables-v1.0.0-preview.3](https://github.com/erbilnas/baklavue/compare/@baklavue/composables-v1.0.0-preview.2...@baklavue/composables-v1.0.0-preview.3) (2026-02-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add new composables ([8e5cb7e](https://github.com/erbilnas/baklavue/commit/8e5cb7eb49ed6079748d7e5e282a51042a6ee2af))
|
|
7
|
+
* add new composables ([fe130be](https://github.com/erbilnas/baklavue/commit/fe130be0123821e5cae0d9330f2e6709753a40b9))
|
|
8
|
+
* add query composable ([28101d0](https://github.com/erbilnas/baklavue/commit/28101d03b747a0b737f889e0310856c779699844))
|
|
9
|
+
* add zod form validation composable ([839a77f](https://github.com/erbilnas/baklavue/commit/839a77f1c42077fd2bdcf3b374f53d819a5cfa6b))
|
|
10
|
+
|
|
11
|
+
# [Unreleased]
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add useColorScheme composable - Light/dark/system color scheme with persistence
|
|
16
|
+
* add useThemePreset composable - Persist Baklava theme preset across sessions
|
|
17
|
+
* add useElementSize composable - Reactive element dimensions via ResizeObserver
|
|
18
|
+
* add useWindowSize composable - Reactive viewport width and height
|
|
19
|
+
* add useContainerScroll composable - Scroll position inside a scrollable container
|
|
20
|
+
* add useSticky composable - Detect when sticky element is stuck
|
|
21
|
+
* add useMutation composable - Mutations (POST/PUT/DELETE) with onSuccess, onError, onSettled
|
|
22
|
+
* add refetchInterval and refetchIntervalInBackground to useQuery - Polling with optional pause when tab hidden
|
|
23
|
+
* add useInfiniteQuery composable - Infinite scroll / cursor-based pagination
|
|
24
|
+
* add prefetchQuery to useQueryClient - Preload data before navigation
|
|
25
|
+
* add useLazyQuery composable - On-demand queries that fetch when execute() is called
|
|
26
|
+
* add usePolling composable - Polling with fetch state for non-query scenarios
|
|
27
|
+
* add useShare composable - Web Share API for sharing text, URLs, or files
|
|
28
|
+
* add useBase64 composable - Convert Blob/File/ArrayBuffer/canvas to Base64
|
|
29
|
+
* add usePrevious composable - Track previous value of a ref
|
|
30
|
+
* add useToggle composable - Simple boolean toggle
|
|
31
|
+
* add useDateFormat composable - Reactive locale-aware date formatting (Intl)
|
|
32
|
+
* add useNumberFormat composable - Reactive locale-aware number/currency formatting (Intl)
|
|
33
|
+
* add useSlug composable - Convert string to URL-friendly slug
|
|
34
|
+
* add useAsyncState composable - Generic async state (loading, error, data)
|
|
35
|
+
* rename csv.ts to file.ts and add useFile composable
|
|
36
|
+
* add multi-format support (CSV, TSV, JSON) for parse, create, and download
|
|
37
|
+
* add parseStream for chunked parsing of large CSV/TSV files
|
|
38
|
+
* add preview for parsing first N rows without full load
|
|
39
|
+
* add Zod schema validation option for parsed data
|
|
40
|
+
* add transform hook for row-level transformation/filtering
|
|
41
|
+
* add typed generics for parse, parseFile, and preview results
|
|
42
|
+
* add Excel (.xlsx, .xls) support for parseFile, preview, create, and download
|
|
43
|
+
* add Blob support for parseFile, preview, and parseStream (in addition to File)
|
|
44
|
+
|
|
45
|
+
# [@baklavue/composables-v1.0.0-preview.2](https://github.com/erbilnas/baklavue/compare/@baklavue/composables-v1.0.0-preview.1...@baklavue/composables-v1.0.0-preview.2) (2026-02-11)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Features
|
|
49
|
+
|
|
50
|
+
* add new composables ([49105ea](https://github.com/erbilnas/baklavue/commit/49105eaa106f1a5b888f9e3f9638ed9776b3d55b))
|
|
51
|
+
|
|
1
52
|
# @baklavue/composables-v1.0.0-preview.1 (2026-02-11)
|
|
2
53
|
|
|
3
54
|
|
package/README.md
CHANGED
|
@@ -34,23 +34,23 @@ bun add @baklavue/composables
|
|
|
34
34
|
|
|
35
35
|
## 📚 Available Composables
|
|
36
36
|
|
|
37
|
-
### `
|
|
37
|
+
### `useFile`
|
|
38
38
|
|
|
39
|
-
A composable for
|
|
39
|
+
A composable for file parsing, creating, and downloading. Supports CSV, TSV, and JSON formats with streaming, Zod validation, transforms, and preview. Uses PapaParse for RFC 4180-compliant CSV/TSV handling.
|
|
40
40
|
|
|
41
41
|
#### Basic Usage
|
|
42
42
|
|
|
43
43
|
```vue
|
|
44
44
|
<script setup lang="ts">
|
|
45
|
-
import {
|
|
45
|
+
import { useFile } from "@baklavue/composables";
|
|
46
46
|
|
|
47
|
-
const { parse, parseFile, create, download } =
|
|
47
|
+
const { parse, parseFile, create, download } = useFile();
|
|
48
48
|
|
|
49
49
|
// Parse CSV string
|
|
50
|
-
const result = parse("name,age\nAlice,30\nBob,25", { header: true });
|
|
50
|
+
const result = parse("name,age\nAlice,30\nBob,25", { format: "csv", header: true });
|
|
51
51
|
console.log(result.data); // [{ name: "Alice", age: "30" }, ...]
|
|
52
52
|
|
|
53
|
-
// Parse file (async)
|
|
53
|
+
// Parse file (async), format auto-detected from filename
|
|
54
54
|
const handleFileUpload = async (event: Event) => {
|
|
55
55
|
const file = (event.target as HTMLInputElement).files?.[0];
|
|
56
56
|
if (file) {
|
|
@@ -79,23 +79,31 @@ const exportData = () => {
|
|
|
79
79
|
|
|
80
80
|
#### API Reference
|
|
81
81
|
|
|
82
|
-
The `
|
|
82
|
+
The `useFile` composable returns an object with the following methods:
|
|
83
83
|
|
|
84
|
-
##### `parse(
|
|
84
|
+
##### `parse(content: string, options?: FileParseOptions<T>)`
|
|
85
85
|
|
|
86
|
-
Parses a CSV
|
|
86
|
+
Parses a string (CSV, TSV, or JSON) and returns `{ data, errors, meta }`. Synchronous.
|
|
87
87
|
|
|
88
|
-
##### `parseFile(file: File, options?:
|
|
88
|
+
##### `parseFile(file: File, options?: FileParseOptions<T>)`
|
|
89
89
|
|
|
90
|
-
Parses a File
|
|
90
|
+
Parses a File asynchronously. Format auto-detected from filename when omitted.
|
|
91
91
|
|
|
92
|
-
##### `
|
|
92
|
+
##### `parseStream(file: File, options)`
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Chunked parsing for large CSV/TSV files. Returns `{ abort }`. Use `step` or `chunk` callbacks.
|
|
95
95
|
|
|
96
|
-
##### `
|
|
96
|
+
##### `preview(file: File, options?)`
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Parses only the first N rows. Returns `Promise<{ data, meta?, truncated }>`.
|
|
99
|
+
|
|
100
|
+
##### `create(data: FileData, options?: FileCreateOptions)`
|
|
101
|
+
|
|
102
|
+
Creates a string from an array of objects, array of arrays, or `{ fields, data }` object. Supports CSV, TSV, JSON.
|
|
103
|
+
|
|
104
|
+
##### `download(data: FileData, filename?: string, options?: FileCreateOptions)`
|
|
105
|
+
|
|
106
|
+
Creates a string and triggers a browser download. Adds UTF-8 BOM for CSV/TSV (Excel compatibility).
|
|
99
107
|
|
|
100
108
|
#### Options
|
|
101
109
|
|
|
@@ -212,16 +220,172 @@ interface NotificationProps {
|
|
|
212
220
|
- **Duration**: If no duration is specified, notifications will use Baklava's default duration
|
|
213
221
|
- **Error Handling**: The composable includes built-in error handling and will warn if the notification element is not found
|
|
214
222
|
|
|
223
|
+
### `useScrollToError`
|
|
224
|
+
|
|
225
|
+
Scroll to an element with validation error. Scrolls into view, optionally applies a highlight effect, and focuses the first focusable control. Works with `input`, `select`, `textarea`, `bl-select`, `bl-input`.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const { scrollToError } = useScrollToError();
|
|
229
|
+
scrollToError('[data-field="tags"]');
|
|
230
|
+
scrollToError(validationError); // when error has scrollTarget
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### `useBaklavaTheme`
|
|
234
|
+
|
|
235
|
+
Overwrite Baklava design system colors and tokens. Use the Vue preset, pass a custom preset object, or override specific colors, border radius, size, typography, or z-index.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const { applyTheme } = useBaklavaTheme();
|
|
239
|
+
applyTheme({ preset: 'vue' });
|
|
240
|
+
applyTheme({ colors: { primary: '#41B883' } });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `useDisclosure`
|
|
244
|
+
|
|
245
|
+
Open/close state for Dialog, Drawer, Dropdown, Accordion, and Tooltip. Avoids repetitive `ref(false)`, `open()`, `close()`, `toggle()`.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const { isOpen, open, close, toggle } = useDisclosure(false);
|
|
249
|
+
// Use with v-model:open on BvDialog, BvDrawer, BvDropdown
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `usePagination`
|
|
253
|
+
|
|
254
|
+
Pagination state for tables and lists. Provides `currentPage`, `pageSize`, `totalItems`, `totalPages`, `offset`, `slice` helper.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const { currentPage, pageSize, totalItems, totalPages, setPage, setPageSize, slice } = usePagination({ totalItems: 100, pageSize: 10 });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `useConfirmDialog`
|
|
261
|
+
|
|
262
|
+
Drive BvDialog for confirm/cancel flows. Returns a promise that resolves to `true` when confirmed, `false` when cancelled.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const { confirm, isOpen, caption, description, handleConfirm, handleCancel } = useConfirmDialog();
|
|
266
|
+
const ok = await confirm({ caption: "Delete?", description: "Sure?" });
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `useClipboard`
|
|
270
|
+
|
|
271
|
+
Copy text to clipboard. Integrates well with `useNotification`.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
const { copy, copied } = useClipboard();
|
|
275
|
+
await copy("token");
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `useBreakpoints` / `useMediaQuery`
|
|
279
|
+
|
|
280
|
+
Responsive breakpoints. `useBreakpoints` provides `isMobile`, `isTablet`, `isDesktop`. `useMediaQuery` for custom queries.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const { isMobile, isTablet, isDesktop } = useBreakpoints();
|
|
284
|
+
const matches = useMediaQuery("(max-width: 768px)");
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `useLocalStorage` / `useSessionStorage`
|
|
288
|
+
|
|
289
|
+
Reactive sync with `localStorage` and `sessionStorage`. Works well with `useBaklavaTheme` and `usePagination` for persisting preferences.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const pageSize = useLocalStorage("table-page-size", 10);
|
|
293
|
+
const draft = useSessionStorage("form-draft", null);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `useCookie`
|
|
297
|
+
|
|
298
|
+
Reactive sync with `document.cookie`. Returns `{ value, get, set, remove }`. Use for auth tokens or when values must be sent to the server.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const { value: token, set, remove } = useCookie("auth-token", "", { path: "/", maxAge: 86400 });
|
|
302
|
+
set("new-token");
|
|
303
|
+
remove();
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `useDebounceFn` / `useDebouncedRef`
|
|
307
|
+
|
|
308
|
+
Debounce function execution or ref value. `useDebounceFn` returns a debounced function. `useDebouncedRef` returns a ref that updates after the delay. Useful for search inputs, autocomplete.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const debouncedSearch = useDebounceFn((q: string) => fetchResults(q), 300);
|
|
312
|
+
const searchQuery = ref("");
|
|
313
|
+
const debouncedQuery = useDebouncedRef(searchQuery, 300);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### `useThrottleFn` / `useThrottledRef`
|
|
317
|
+
|
|
318
|
+
Throttle function execution or ref value. Useful for scroll, resize, mousemove handlers.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const throttledHandler = useThrottleFn(() => updateScroll(), 100);
|
|
322
|
+
const throttledScrollY = useThrottledRef(scrollY, 100);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `useIntervalFn` / `useTimeoutFn`
|
|
326
|
+
|
|
327
|
+
Pausable interval and cancellable timeout. `useIntervalFn` returns `{ pause, resume, isActive }`. `useTimeoutFn` returns `{ run, cancel, isPending }`.
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
const { pause, resume } = useIntervalFn(() => fetchData(), 5000);
|
|
331
|
+
const { run, cancel } = useTimeoutFn(() => showToast("Saved!"), 2000);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### `useFetch`
|
|
335
|
+
|
|
336
|
+
Reactive fetch with loading/error/data. Supports abort, timeout, and manual execute.
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
const { data, error, isFetching, execute } = useFetch<User>(
|
|
340
|
+
() => `https://api.example.com/users/${id.value}`,
|
|
341
|
+
{ immediate: true }
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### `useIntersectionObserver`
|
|
346
|
+
|
|
347
|
+
Detects when a target element enters or leaves the viewport. Useful for lazy loading, scroll-triggered animations.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const target = ref<HTMLElement | null>(null);
|
|
351
|
+
const isVisible = useIntersectionObserver(target, { threshold: 0.5 });
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### `useRafFn`
|
|
355
|
+
|
|
356
|
+
Calls a function on every requestAnimationFrame. Returns `{ pause, resume, isActive }`. Useful for animation loops.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const { pause, resume } = useRafFn(({ delta }) => {
|
|
360
|
+
position.value += velocity * (delta / 1000);
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
215
364
|
## 🏗️ Project Structure
|
|
216
365
|
|
|
217
366
|
```
|
|
218
367
|
packages/composables/
|
|
219
368
|
├── index.ts # Main export file
|
|
220
|
-
├──
|
|
369
|
+
├── file.ts # File parsing, creating, download (CSV, TSV, JSON) composable
|
|
221
370
|
├── notification.ts # Notification composable
|
|
371
|
+
├── scrollToError.ts # Scroll to validation error composable
|
|
372
|
+
├── theme.ts # Baklava theme composable
|
|
373
|
+
├── disclosure.ts # Open/close state composable
|
|
374
|
+
├── pagination.ts # Pagination state composable
|
|
375
|
+
├── confirmDialog.ts # Confirm dialog composable
|
|
376
|
+
├── clipboard.ts # Clipboard composable
|
|
377
|
+
├── cookie.ts # useCookie composable
|
|
378
|
+
├── breakpoints.ts # Responsive breakpoints composable
|
|
379
|
+
├── storage.ts # useLocalStorage, useSessionStorage composables
|
|
380
|
+
├── debounce.ts # useDebounceFn, useDebouncedRef
|
|
381
|
+
├── throttle.ts # useThrottleFn, useThrottledRef
|
|
382
|
+
├── timer.ts # useIntervalFn, useTimeoutFn
|
|
383
|
+
├── fetch.ts # useFetch composable
|
|
384
|
+
├── intersectionObserver.ts # useIntersectionObserver
|
|
385
|
+
├── raf.ts # useRafFn composable
|
|
222
386
|
├── package.json # Package configuration
|
|
223
387
|
├── tsconfig.json # TypeScript configuration
|
|
224
|
-
└── README.md
|
|
388
|
+
└── README.md # This file
|
|
225
389
|
```
|
|
226
390
|
|
|
227
391
|
## 🔧 Development
|
package/alert.spec.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { defineComponent } from "vue";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { useAlert } from "./alert";
|
|
5
|
+
|
|
6
|
+
function withSetup<T>(composable: () => T) {
|
|
7
|
+
let result: T;
|
|
8
|
+
const TestComponent = defineComponent({
|
|
9
|
+
setup() {
|
|
10
|
+
result = composable();
|
|
11
|
+
return () => null;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
const wrapper = mount(TestComponent);
|
|
15
|
+
return { result: result!, wrapper };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("useAlert", () => {
|
|
19
|
+
it("returns initial state", () => {
|
|
20
|
+
const { result } = withSetup(() => useAlert());
|
|
21
|
+
|
|
22
|
+
expect(result.isVisible.value).toBe(false);
|
|
23
|
+
expect(result.variant.value).toBe("info");
|
|
24
|
+
expect(result.caption.value).toBe("");
|
|
25
|
+
expect(result.description.value).toBe("");
|
|
26
|
+
expect(result.closable.value).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("show() sets isVisible and options", async () => {
|
|
30
|
+
const { result, wrapper } = withSetup(() => useAlert());
|
|
31
|
+
|
|
32
|
+
result.show({
|
|
33
|
+
variant: "success",
|
|
34
|
+
caption: "Title",
|
|
35
|
+
description: "Saved!",
|
|
36
|
+
closable: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await wrapper.vm.$nextTick();
|
|
40
|
+
expect(result.isVisible.value).toBe(true);
|
|
41
|
+
expect(result.variant.value).toBe("success");
|
|
42
|
+
expect(result.caption.value).toBe("Title");
|
|
43
|
+
expect(result.description.value).toBe("Saved!");
|
|
44
|
+
expect(result.closable.value).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("show() uses defaults for omitted options", async () => {
|
|
48
|
+
const { result, wrapper } = withSetup(() => useAlert());
|
|
49
|
+
|
|
50
|
+
result.show();
|
|
51
|
+
|
|
52
|
+
await wrapper.vm.$nextTick();
|
|
53
|
+
expect(result.isVisible.value).toBe(true);
|
|
54
|
+
expect(result.variant.value).toBe("info");
|
|
55
|
+
expect(result.caption.value).toBe("");
|
|
56
|
+
expect(result.description.value).toBe("");
|
|
57
|
+
expect(result.closable.value).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("hide() sets isVisible to false", async () => {
|
|
61
|
+
const { result, wrapper } = withSetup(() => useAlert());
|
|
62
|
+
|
|
63
|
+
result.show({ description: "Message" });
|
|
64
|
+
await wrapper.vm.$nextTick();
|
|
65
|
+
expect(result.isVisible.value).toBe(true);
|
|
66
|
+
|
|
67
|
+
result.hide();
|
|
68
|
+
await wrapper.vm.$nextTick();
|
|
69
|
+
expect(result.isVisible.value).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/alert.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export type AlertVariant = "info" | "success" | "warning" | "danger";
|
|
4
|
+
|
|
5
|
+
export interface UseAlertOptions {
|
|
6
|
+
variant?: AlertVariant;
|
|
7
|
+
caption?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
closable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Composable for programmatic show/hide of inline BvAlert.
|
|
14
|
+
* Use with v-if="isVisible" or :closed="!isVisible" on BvAlert.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const { isVisible, variant, caption, description, show, hide } = useAlert();
|
|
19
|
+
*
|
|
20
|
+
* show({ variant: "success", description: "Saved!" });
|
|
21
|
+
* hide();
|
|
22
|
+
*
|
|
23
|
+
* <BvAlert v-if="isVisible" :variant="variant" :caption="caption" :description="description" closable @close="hide" />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @returns Object with isVisible, variant, caption, description, closable, show, hide
|
|
27
|
+
*/
|
|
28
|
+
export function useAlert() {
|
|
29
|
+
const isVisible = ref(false);
|
|
30
|
+
const variant = ref<AlertVariant>("info");
|
|
31
|
+
const caption = ref("");
|
|
32
|
+
const description = ref("");
|
|
33
|
+
const closable = ref(false);
|
|
34
|
+
|
|
35
|
+
const show = (options: UseAlertOptions = {}) => {
|
|
36
|
+
variant.value = options.variant ?? "info";
|
|
37
|
+
caption.value = options.caption ?? "";
|
|
38
|
+
description.value = options.description ?? "";
|
|
39
|
+
closable.value = options.closable ?? false;
|
|
40
|
+
isVisible.value = true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const hide = () => {
|
|
44
|
+
isVisible.value = false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
isVisible,
|
|
49
|
+
variant,
|
|
50
|
+
caption,
|
|
51
|
+
description,
|
|
52
|
+
closable,
|
|
53
|
+
show,
|
|
54
|
+
hide,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { defineComponent } from "vue";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { useAsyncState } from "./asyncState";
|
|
5
|
+
|
|
6
|
+
function withSetup<T>(composable: () => T) {
|
|
7
|
+
let result: T;
|
|
8
|
+
const TestComponent = defineComponent({
|
|
9
|
+
setup() {
|
|
10
|
+
result = composable();
|
|
11
|
+
return () => null;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
const wrapper = mount(TestComponent);
|
|
15
|
+
return { result: result!, wrapper };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("useAsyncState", () => {
|
|
19
|
+
it("returns initial state", () => {
|
|
20
|
+
const fn = vi.fn();
|
|
21
|
+
const { result } = withSetup(() => useAsyncState(fn));
|
|
22
|
+
|
|
23
|
+
expect(result.state.value).toBeUndefined();
|
|
24
|
+
expect(result.isLoading.value).toBe(false);
|
|
25
|
+
expect(result.error.value).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("execute() runs async fn and updates state", async () => {
|
|
29
|
+
let resolveFn!: (value: number) => void;
|
|
30
|
+
const fn = vi.fn().mockReturnValue(
|
|
31
|
+
new Promise<number>((r) => {
|
|
32
|
+
resolveFn = r;
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
const { result, wrapper } = withSetup(() => useAsyncState(fn));
|
|
36
|
+
|
|
37
|
+
const promise = result.execute();
|
|
38
|
+
await wrapper.vm.$nextTick();
|
|
39
|
+
expect(result.isLoading.value).toBe(true);
|
|
40
|
+
|
|
41
|
+
resolveFn(42);
|
|
42
|
+
const value = await promise;
|
|
43
|
+
await wrapper.vm.$nextTick();
|
|
44
|
+
expect(value).toBe(42);
|
|
45
|
+
expect(result.state.value).toBe(42);
|
|
46
|
+
expect(result.isLoading.value).toBe(false);
|
|
47
|
+
expect(result.error.value).toBeNull();
|
|
48
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("execute() sets error on rejection", async () => {
|
|
52
|
+
const err = new Error("Failed");
|
|
53
|
+
const fn = vi.fn().mockRejectedValue(err);
|
|
54
|
+
const { result, wrapper } = withSetup(() => useAsyncState(fn));
|
|
55
|
+
|
|
56
|
+
await expect(result.execute()).rejects.toThrow("Failed");
|
|
57
|
+
await wrapper.vm.$nextTick();
|
|
58
|
+
expect(result.error.value).toEqual(err);
|
|
59
|
+
expect(result.isLoading.value).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("respects initialData option", () => {
|
|
63
|
+
const fn = vi.fn();
|
|
64
|
+
const { result } = withSetup(() =>
|
|
65
|
+
useAsyncState(fn, { initialData: "initial" }),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result.state.value).toBe("initial");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("calls onSuccess when fn resolves", async () => {
|
|
72
|
+
const onSuccess = vi.fn();
|
|
73
|
+
const fn = vi.fn().mockResolvedValue("data");
|
|
74
|
+
const { result } = withSetup(() =>
|
|
75
|
+
useAsyncState(fn, { onSuccess }),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await result.execute();
|
|
79
|
+
expect(onSuccess).toHaveBeenCalledWith("data");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("calls onError when fn rejects", async () => {
|
|
83
|
+
const err = new Error("Error");
|
|
84
|
+
const onError = vi.fn();
|
|
85
|
+
const fn = vi.fn().mockRejectedValue(err);
|
|
86
|
+
const { result } = withSetup(() => useAsyncState(fn, { onError }));
|
|
87
|
+
|
|
88
|
+
await expect(result.execute()).rejects.toThrow("Error");
|
|
89
|
+
expect(onError).toHaveBeenCalledWith(err);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("wraps non-Error thrown values in Error", async () => {
|
|
93
|
+
const fn = vi.fn().mockRejectedValue("string error");
|
|
94
|
+
const { result } = withSetup(() => useAsyncState(fn));
|
|
95
|
+
|
|
96
|
+
await expect(result.execute()).rejects.toThrow("string error");
|
|
97
|
+
expect(result.error.value).toBeInstanceOf(Error);
|
|
98
|
+
expect(result.error.value?.message).toBe("string error");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("immediate: true executes on mount", async () => {
|
|
102
|
+
const fn = vi.fn().mockResolvedValue(1);
|
|
103
|
+
const { result, wrapper } = withSetup(() =>
|
|
104
|
+
useAsyncState(fn, { immediate: true }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await wrapper.vm.$nextTick();
|
|
108
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
109
|
+
await wrapper.vm.$nextTick();
|
|
110
|
+
expect(result.state.value).toBe(1);
|
|
111
|
+
expect(result.isLoading.value).toBe(false);
|
|
112
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/asyncState.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ref, shallowRef } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface UseAsyncStateOptions<T> {
|
|
4
|
+
/** Execute immediately on mount. Default: false */
|
|
5
|
+
immediate?: boolean;
|
|
6
|
+
/** Initial data value. Default: undefined */
|
|
7
|
+
initialData?: T;
|
|
8
|
+
/** Callback on success */
|
|
9
|
+
onSuccess?: (data: T) => void;
|
|
10
|
+
/** Callback on error */
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Composable for generic async state (loading, error, data) without fetch.
|
|
16
|
+
* Complements useFetch/useQuery for non-HTTP async (IndexedDB, Web Worker, custom logic).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const { state, execute, isLoading, error } = useAsyncState(async () => {
|
|
21
|
+
* const data = await readFromIndexedDB();
|
|
22
|
+
* return data;
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await execute();
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const { state, execute, isLoading } = useAsyncState(
|
|
31
|
+
* () => fetchUser(userId),
|
|
32
|
+
* { immediate: true, initialData: null }
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param asyncFn - Async function returning the data
|
|
37
|
+
* @param options - Optional: immediate, initialData, onSuccess, onError
|
|
38
|
+
* @returns state (data), execute, isLoading, error
|
|
39
|
+
*/
|
|
40
|
+
export function useAsyncState<T>(
|
|
41
|
+
asyncFn: () => Promise<T>,
|
|
42
|
+
options: UseAsyncStateOptions<T> = {},
|
|
43
|
+
): {
|
|
44
|
+
state: ReturnType<typeof shallowRef<T | undefined>>;
|
|
45
|
+
execute: () => Promise<T>;
|
|
46
|
+
isLoading: ReturnType<typeof ref<boolean>>;
|
|
47
|
+
error: ReturnType<typeof ref<Error | null>>;
|
|
48
|
+
} {
|
|
49
|
+
const { immediate = false, initialData, onSuccess, onError } = options;
|
|
50
|
+
|
|
51
|
+
const state = shallowRef<T | undefined>(initialData);
|
|
52
|
+
const isLoading = ref(false);
|
|
53
|
+
const error = ref<Error | null>(null);
|
|
54
|
+
|
|
55
|
+
const execute = async (): Promise<T> => {
|
|
56
|
+
isLoading.value = true;
|
|
57
|
+
error.value = null;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const data = await asyncFn();
|
|
61
|
+
state.value = data;
|
|
62
|
+
onSuccess?.(data);
|
|
63
|
+
return data;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
66
|
+
error.value = e;
|
|
67
|
+
onError?.(e);
|
|
68
|
+
throw e;
|
|
69
|
+
} finally {
|
|
70
|
+
isLoading.value = false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (immediate) {
|
|
75
|
+
execute();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
state,
|
|
80
|
+
execute,
|
|
81
|
+
isLoading,
|
|
82
|
+
error,
|
|
83
|
+
};
|
|
84
|
+
}
|