@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +181 -17
  3. package/alert.spec.ts +71 -0
  4. package/alert.ts +56 -0
  5. package/asyncState.spec.ts +114 -0
  6. package/asyncState.ts +84 -0
  7. package/base64.spec.ts +286 -0
  8. package/base64.ts +183 -0
  9. package/breakpoints.spec.ts +173 -0
  10. package/breakpoints.ts +135 -0
  11. package/clipboard.spec.ts +228 -0
  12. package/clipboard.ts +122 -0
  13. package/colorScheme.spec.ts +209 -0
  14. package/colorScheme.ts +155 -0
  15. package/confirmDialog.spec.ts +85 -0
  16. package/confirmDialog.ts +89 -0
  17. package/containerScroll.spec.ts +169 -0
  18. package/containerScroll.ts +94 -0
  19. package/cookie.spec.ts +248 -0
  20. package/cookie.ts +229 -0
  21. package/debounce.spec.ts +145 -0
  22. package/debounce.ts +94 -0
  23. package/disclosure.spec.ts +62 -0
  24. package/disclosure.ts +39 -0
  25. package/elementSize.spec.ts +175 -0
  26. package/elementSize.ts +96 -0
  27. package/fetch.spec.ts +754 -0
  28. package/fetch.ts +419 -0
  29. package/fieldArray.spec.ts +170 -0
  30. package/fieldArray.ts +155 -0
  31. package/file.spec.ts +544 -0
  32. package/file.ts +657 -0
  33. package/focusTrap.spec.ts +316 -0
  34. package/focusTrap.ts +149 -0
  35. package/formPersistence.spec.ts +105 -0
  36. package/formPersistence.ts +101 -0
  37. package/formState.spec.ts +150 -0
  38. package/formState.ts +175 -0
  39. package/formValidation.spec.ts +384 -0
  40. package/formValidation.ts +463 -0
  41. package/format.spec.ts +121 -0
  42. package/format.ts +95 -0
  43. package/id.spec.ts +34 -0
  44. package/id.ts +24 -0
  45. package/index.ts +169 -3
  46. package/infiniteQuery.spec.ts +364 -0
  47. package/infiniteQuery.ts +326 -0
  48. package/intersectionObserver.spec.ts +151 -0
  49. package/intersectionObserver.ts +100 -0
  50. package/lazyQuery.spec.ts +75 -0
  51. package/lazyQuery.ts +48 -0
  52. package/loading.spec.ts +143 -0
  53. package/loading.ts +80 -0
  54. package/mutation.spec.ts +184 -0
  55. package/mutation.ts +124 -0
  56. package/notification.spec.ts +79 -0
  57. package/package.json +15 -5
  58. package/pagination.spec.ts +101 -0
  59. package/pagination.ts +101 -0
  60. package/polling.spec.ts +198 -0
  61. package/polling.ts +98 -0
  62. package/previous.spec.ts +61 -0
  63. package/previous.ts +43 -0
  64. package/query.spec.ts +347 -0
  65. package/query.ts +414 -0
  66. package/raf.spec.ts +149 -0
  67. package/raf.ts +103 -0
  68. package/scrollLock.spec.ts +81 -0
  69. package/scrollLock.ts +61 -0
  70. package/scrollToError.spec.ts +344 -0
  71. package/scrollToError.ts +200 -8
  72. package/scrollVisibility.spec.ts +126 -0
  73. package/scrollVisibility.ts +66 -0
  74. package/share.spec.ts +194 -0
  75. package/share.ts +111 -0
  76. package/slug.spec.ts +68 -0
  77. package/slug.ts +65 -0
  78. package/stepper.spec.ts +175 -0
  79. package/stepper.ts +119 -0
  80. package/stepperForm.spec.ts +90 -0
  81. package/stepperForm.ts +81 -0
  82. package/sticky.spec.ts +150 -0
  83. package/sticky.ts +98 -0
  84. package/storage.spec.ts +276 -0
  85. package/storage.ts +201 -0
  86. package/tests/utils/withSetup.ts +18 -0
  87. package/theme.spec.ts +142 -0
  88. package/themePreset.spec.ts +86 -0
  89. package/themePreset.ts +64 -0
  90. package/throttle.spec.ts +220 -0
  91. package/throttle.ts +126 -0
  92. package/timer.spec.ts +163 -0
  93. package/timer.ts +117 -0
  94. package/toggle.spec.ts +56 -0
  95. package/toggle.ts +40 -0
  96. package/vitest.config.ts +21 -0
  97. 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
- ### `useCsv`
37
+ ### `useFile`
38
38
 
39
- A composable for CSV parsing, creating, and downloading. Uses PapaParse for RFC 4180-compliant handling of quoted fields, commas in values, and edge cases.
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 { useCsv } from "@baklavue/composables";
45
+ import { useFile } from "@baklavue/composables";
46
46
 
47
- const { parse, parseFile, create, download } = useCsv();
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 `useCsv` composable returns an object with the following methods:
82
+ The `useFile` composable returns an object with the following methods:
83
83
 
84
- ##### `parse(csv: string, options?: CsvParseOptions)`
84
+ ##### `parse(content: string, options?: FileParseOptions<T>)`
85
85
 
86
- Parses a CSV string and returns `{ data, errors, meta }`. Synchronous.
86
+ Parses a string (CSV, TSV, or JSON) and returns `{ data, errors, meta }`. Synchronous.
87
87
 
88
- ##### `parseFile(file: File, options?: CsvParseOptions)`
88
+ ##### `parseFile(file: File, options?: FileParseOptions<T>)`
89
89
 
90
- Parses a File or Blob asynchronously. Returns `Promise<ParseResult>`.
90
+ Parses a File asynchronously. Format auto-detected from filename when omitted.
91
91
 
92
- ##### `create(data: CsvData, options?: CsvCreateOptions)`
92
+ ##### `parseStream(file: File, options)`
93
93
 
94
- Creates a CSV string from an array of objects, array of arrays, or `{ fields, data }` object.
94
+ Chunked parsing for large CSV/TSV files. Returns `{ abort }`. Use `step` or `chunk` callbacks.
95
95
 
96
- ##### `download(data: CsvData, filename?: string, options?: CsvCreateOptions)`
96
+ ##### `preview(file: File, options?)`
97
97
 
98
- Creates a CSV string and triggers a browser download. Adds UTF-8 BOM for Excel compatibility.
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
- ├── csv.ts # CSV parsing, creating, and download composable
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 # This file
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
+ }