vue-context-storage 0.1.33 → 0.1.35

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/README.md CHANGED
@@ -52,7 +52,7 @@ In Vue applications, reactive state often needs to live beyond a single componen
52
52
  - **URL query parameters** stay in sync with your data automatically - users can bookmark or share a page and get the exact same state back.
53
53
  - **localStorage and sessionStorage** are kept up to date without manual `getItem`/`setItem` calls, including cross-tab synchronization.
54
54
  - **Type safety** is preserved end-to-end: URL strings are coerced back to numbers, booleans, and arrays via transform helpers or Zod schemas.
55
- - **Multiple independent contexts** (e.g. two data tables on the same page) are supported out of the box through the prefix pattern, so query parameters never collide.
55
+ - **Multiple independent contexts** (e.g. two data tables on the same page) are supported out of the box through the key pattern, so query parameters never collide.
56
56
 
57
57
  The goal is a single, declarative API - `useContextStorage('query', data, options)` - that replaces scattered watchers, router guards, and storage listeners with one composable call per piece of state.
58
58
 
@@ -118,7 +118,7 @@ const filters = reactive({
118
118
 
119
119
  // Sync with URL query
120
120
  useContextStorage('query', filters, {
121
- prefix: 'filters',
121
+ key: 'filters',
122
122
  })
123
123
 
124
124
  // Sync with localStorage
@@ -141,7 +141,7 @@ You can also pass an injection key directly instead of a string:
141
141
  import { contextStorageQueryHandlerInjectKey } from 'vue-context-storage'
142
142
 
143
143
  useContextStorage(contextStorageQueryHandlerInjectKey, filters, {
144
- prefix: 'filters',
144
+ key: 'filters',
145
145
  })
146
146
  ```
147
147
 
@@ -181,11 +181,11 @@ The `<ContextStoragePrefix>` component adds a prefix to all `useContextStorage`
181
181
  </template>
182
182
  ```
183
183
 
184
- Inside `MyTable`, any `useContextStorage('query', data)` call will automatically get `prefix: 'tables'`. If the composable also specifies its own prefix, they are combined:
184
+ Inside `MyTable`, any `useContextStorage('query', data)` call will automatically get `key: 'tables'`. If the composable also specifies its own key, they are combined:
185
185
 
186
186
  ```typescript
187
- // Inside MyTable — effective prefix becomes 'table[filters]'
188
- useContextStorage('query', filters, { prefix: 'filters' })
187
+ // Inside MyTable — effective key becomes 'table[filters]'
188
+ useContextStorage('query', filters, { key: 'filters' })
189
189
  // URL: ?table[filters][search]=...
190
190
  ```
191
191
 
@@ -248,21 +248,11 @@ const filters = reactive<Filters>({
248
248
 
249
249
  // Automatically syncs filters with URL query
250
250
  useContextStorage('query', filters, {
251
- prefix: 'filters', // URL will be: ?filters[search]=...&filters[status]=...
251
+ key: 'filters', // URL will be: ?filters[search]=...&filters[status]=...
252
252
  })
253
253
  </script>
254
254
  ```
255
255
 
256
- Also available as a dedicated composable:
257
-
258
- ```typescript
259
- import { useContextStorageQueryHandler } from 'vue-context-storage'
260
-
261
- useContextStorageQueryHandler(filters, {
262
- prefix: 'filters',
263
- })
264
- ```
265
-
266
256
  ## Advanced Usage
267
257
 
268
258
  ### Using Transform Helpers
@@ -286,7 +276,7 @@ const state = ref<TableState>({
286
276
  })
287
277
 
288
278
  useContextStorage('query', state, {
289
- prefix: 'table',
279
+ key: 'table',
290
280
  transform: (deserialized, initial) => ({
291
281
  page: transform.asNumber(deserialized.page, { fallback: 1 }),
292
282
  search: transform.asString(deserialized.search, { fallback: '' }),
@@ -302,6 +292,7 @@ useContextStorage('query', state, {
302
292
  - `asBoolean(value, options)` - Convert to boolean
303
293
  - `asArray(value, options)` - Convert to array
304
294
  - `asNumberArray(value, options)` - Convert to number array
295
+ - `asObjectArray(value, options)` - Convert indexed object to array of objects (see [Arrays of Objects](#arrays-of-objects))
305
296
 
306
297
  ### Using Zod Schemas
307
298
 
@@ -322,7 +313,7 @@ const filters = ref(FiltersSchema.parse({}))
322
313
 
323
314
  // Use schema for automatic validation
324
315
  useContextStorage('query', filters, {
325
- prefix: 'filters',
316
+ key: 'filters',
326
317
  schema: FiltersSchema,
327
318
  })
328
319
  ```
@@ -335,25 +326,104 @@ useContextStorage('query', filters, {
335
326
  - Less boilerplate code
336
327
  - Single source of truth for structure and validation
337
328
 
329
+ ### Arrays of Objects
330
+
331
+ The query handler supports arrays of objects. They are serialized as indexed query parameters:
332
+
333
+ ```
334
+ ?items[0][product]=Apple&items[0][quantity]=5&items[1][product]=Banana&items[1][quantity]=10
335
+ ```
336
+
337
+ After deserialization, URL parameters produce indexed objects (`{ '0': {...}, '1': {...} }`) rather than arrays. Use `transform.asObjectArray` or the Zod helper `zObjectArray` to convert them back.
338
+
339
+ **With transform helpers:**
340
+
341
+ ```typescript
342
+ import { reactive } from 'vue'
343
+ import { useContextStorage, transform } from 'vue-context-storage'
344
+
345
+ const data = reactive({
346
+ title: '',
347
+ items: [] as { product: string; quantity: number }[],
348
+ })
349
+
350
+ useContextStorage('query', data, {
351
+ transform: (value) => ({
352
+ title: transform.asString(value.title),
353
+ items: transform.asObjectArray(value.items, (entry) => ({
354
+ product: transform.asString(entry.product),
355
+ quantity: transform.asNumber(entry.quantity),
356
+ })),
357
+ }),
358
+ })
359
+ ```
360
+
361
+ `asObjectArray` also supports a callback shorthand — pass a function as the second argument instead of an options object.
362
+
363
+ **With Zod schema:**
364
+
365
+ ```typescript
366
+ import { z } from 'zod'
367
+ import { zObjectArray } from 'vue-context-storage/zod'
368
+
369
+ const ItemSchema = z.object({
370
+ product: z.string().default(''),
371
+ quantity: z.coerce.number().default(0),
372
+ })
373
+
374
+ const DataSchema = z.object({
375
+ title: z.string().default(''),
376
+ items: zObjectArray(ItemSchema),
377
+ })
378
+
379
+ useContextStorage('query', data, { schema: DataSchema })
380
+ ```
381
+
382
+ See [Zod Helpers](#zod-helpers-vue-context-storagezod) for more details.
383
+
338
384
  ### Preserve Empty State
339
385
 
340
386
  Keep empty state in URL to prevent resetting on reload:
341
387
 
342
388
  ```typescript
343
389
  useContextStorage('query', filters, {
344
- prefix: 'filters',
390
+ key: 'filters',
345
391
  preserveEmptyState: true,
346
392
  // Empty filters will show as: ?filters
347
393
  // Without this option, empty filters would clear the URL completely
348
394
  })
349
395
  ```
350
396
 
397
+ ### Additional Default Data
398
+
399
+ When `onlyChanges` is enabled (the default), a key is omitted from the URL if its current value matches the initial snapshot. `additionalDefaultData` lets you specify extra values that should also be treated as defaults and excluded from the URL.
400
+
401
+ This is useful when the initial reactive data starts with `undefined` (e.g. before an API response), but you also want a specific value (like `1`) to be considered a default:
402
+
403
+ ```typescript
404
+ const data = ref({ page: undefined as number | undefined })
405
+
406
+ useContextStorage('query', data, {
407
+ key: 'filters',
408
+ onlyChanges: true,
409
+ additionalDefaultData: { page: 1 },
410
+ })
411
+
412
+ // page=undefined → not in query (matches initial)
413
+ // page=1 → not in query (matches additionalDefaultData)
414
+ // page=2 → appears in query as ?filters[page]=2
415
+ ```
416
+
351
417
  ### Configure Query Handler
352
418
 
353
419
  Customize behavior by passing options to the factory:
354
420
 
355
421
  ```typescript
356
- import { createQueryHandler, createLocalStorageHandler, createSessionStorageHandler } from 'vue-context-storage'
422
+ import {
423
+ createQueryHandler,
424
+ createLocalStorageHandler,
425
+ createSessionStorageHandler,
426
+ } from 'vue-context-storage'
357
427
 
358
428
  const customHandlers = [
359
429
  createQueryHandler({
@@ -391,16 +461,6 @@ useContextStorage('localStorage', settings, {
391
461
  </script>
392
462
  ```
393
463
 
394
- Also available as a dedicated composable:
395
-
396
- ```typescript
397
- import { useContextStorageLocalStorage } from 'vue-context-storage'
398
-
399
- useContextStorageLocalStorage(settings, {
400
- key: 'app-settings',
401
- })
402
- ```
403
-
404
464
  ### Configure localStorage Handler
405
465
 
406
466
  ```typescript
@@ -433,36 +493,26 @@ useContextStorage('sessionStorage', formDraft, {
433
493
  </script>
434
494
  ```
435
495
 
436
- Also available as a dedicated composable:
496
+ ### Multiple Registrations Under One Root Key
437
497
 
438
- ```typescript
439
- import { useContextStorageSessionStorage } from 'vue-context-storage'
440
-
441
- useContextStorageSessionStorage(formDraft, {
442
- key: 'contact-form-draft',
443
- })
444
- ```
445
-
446
- ### Using Prefix
447
-
448
- The prefix is appended to the storage key in bracket notation, so each prefixed registration gets its own storage entry:
498
+ Use bracket notation in `key` to store multiple data objects under a common root:
449
499
 
450
500
  ```typescript
451
501
  const filters = reactive({ search: '', status: 'active' })
452
502
 
453
503
  useContextStorage('sessionStorage', filters, {
454
- key: 'app-state',
455
- prefix: 'filters', // Storage key: 'app-state[filters]', value: { search: '', status: 'active' }
504
+ key: 'app-state[filters]', // Storage key: 'app-state[filters]'
456
505
  })
457
506
 
458
507
  const pagination = reactive({ page: 1, perPage: 25 })
459
508
 
460
509
  useContextStorage('sessionStorage', pagination, {
461
- key: 'app-state',
462
- prefix: 'pagination', // Storage key: 'app-state[pagination]', value: { page: 1, perPage: 25 }
510
+ key: 'app-state[pagination]', // Storage key: 'app-state[pagination]'
463
511
  })
464
512
  ```
465
513
 
514
+ Or use `<ContextStoragePrefix>` for automatic scoping (see [Prefix Scoping](#prefix-scoping-with-contextstorageprefix)).
515
+
466
516
  ### Using Transform with Storage Handlers
467
517
 
468
518
  Convert stored values to proper types when reading from storage:
@@ -488,7 +538,7 @@ useContextStorage('localStorage', settings, {
488
538
 
489
539
  ```typescript
490
540
  import { z } from 'zod'
491
- import { useContextStorageLocalStorage } from 'vue-context-storage'
541
+ import { useContextStorage } from 'vue-context-storage'
492
542
 
493
543
  const SettingsSchema = z.object({
494
544
  theme: z.enum(['light', 'dark']).default('light'),
@@ -542,19 +592,6 @@ Unified composable that delegates to the correct handler based on `type`.
542
592
  - `defineContextStorageHandler(name, injectionKey)` - Register a custom handler
543
593
  - `resolveHandlerInjectionKey(type)` - Look up an injection key by name
544
594
 
545
- #### `useContextStorageQueryHandler<T>(data, options)`
546
-
547
- Registers reactive data for URL query synchronization.
548
-
549
- **Parameters:**
550
-
551
- - `data: MaybeRefOrGetter<T>` - Reactive reference to sync
552
- - `options?: RegisterQueryHandlerOptions<T>`
553
- - `prefix?: string` - Query parameter prefix
554
- - `transform?: (deserialized, initial) => T` - Transform function
555
- - `preserveEmptyState?: boolean` - Keep empty state in URL
556
- - `mergeOnlyExistingKeysWithoutTransform?: boolean` - Only merge existing keys (default: true)
557
-
558
595
  ### Handler Factories
559
596
 
560
597
  #### `createQueryHandler(options?)`
@@ -585,25 +622,6 @@ Creates a sessionStorage handler factory.
585
622
 
586
623
  - `listenToStorageEvents?: boolean` - Listen to storage events (default: `false`)
587
624
 
588
- #### `useContextStorageLocalStorage<T>(data, options)`
589
-
590
- Registers reactive data for localStorage synchronization.
591
-
592
- **Parameters:**
593
-
594
- - `data: MaybeRefOrGetter<T>` - Reactive reference to sync
595
- - `options: RegisterWebStorageHandlerBaseOptions<T>`
596
- - `key: string` - Storage key (required)
597
- - `prefix?: string` - Appended to the storage key in bracket notation (e.g. key `'app'` + prefix `'filters'` = storage key `'app[filters]'`)
598
- - `transform?: (deserialized, initial) => T` - Transform function
599
- - `schema?: ZodSchema` - Zod schema for validation
600
- - `serializer?: (data: T) => string` - Custom serializer (default: `JSON.stringify`)
601
- - `deserializer?: (str: string) => unknown` - Custom deserializer (default: `JSON.parse`)
602
-
603
- #### `useContextStorageSessionStorage<T>(data, options)`
604
-
605
- Registers reactive data for sessionStorage synchronization. Same options as `useContextStorageLocalStorage`.
606
-
607
625
  ### Components
608
626
 
609
627
  #### `<ContextStoragePrefix>`
@@ -628,6 +646,91 @@ transform.asNumber(value, {
628
646
  })
629
647
  ```
630
648
 
649
+ ## Zod Helpers (`vue-context-storage/zod`)
650
+
651
+ The library provides a separate entry point with Zod-specific helpers. Since `zod` is an optional peer dependency, these helpers are isolated in `vue-context-storage/zod` to avoid importing Zod in the main bundle.
652
+
653
+ ```bash
654
+ npm install zod
655
+ ```
656
+
657
+ ### `zObjectArray(itemSchema)`
658
+
659
+ Creates a Zod schema for arrays of objects serialized as indexed query parameters. Wraps `z.record()` + `.transform()` to convert indexed objects back to sorted arrays.
660
+
661
+ ```typescript
662
+ import { z } from 'zod'
663
+ import { zObjectArray } from 'vue-context-storage/zod'
664
+
665
+ const ItemSchema = z.object({
666
+ product: z.string().default(''),
667
+ quantity: z.coerce.number().default(0),
668
+ })
669
+
670
+ const DataSchema = z.object({
671
+ title: z.string().default(''),
672
+ items: zObjectArray(ItemSchema),
673
+ })
674
+ ```
675
+
676
+ ### `zUrlBoolean(defaultValue?)`
677
+
678
+ Creates a Zod schema for booleans serialized as URL query parameters. Standard `z.coerce.boolean()` cannot be used because `Boolean('0')` is `true` in JavaScript. This helper correctly handles `'1'`, `'true'`, `'0'`, `'false'`, and native booleans.
679
+
680
+ ```typescript
681
+ import { z } from 'zod'
682
+ import { zUrlBoolean } from 'vue-context-storage/zod'
683
+
684
+ const Schema = z.object({
685
+ active: zUrlBoolean(), // defaults to false
686
+ enabled: zUrlBoolean(true), // defaults to true
687
+ })
688
+ ```
689
+
690
+ ### `createSchemaObject(schema, options?)`
691
+
692
+ Creates a plain object with empty/default values based on a Zod schema. Useful for initializing reactive data from a schema definition.
693
+
694
+ ```typescript
695
+ import { z } from 'zod'
696
+ import { createSchemaObject } from 'vue-context-storage/zod'
697
+
698
+ const FiltersSchema = z.object({
699
+ search: z.string().default(''),
700
+ page: z.coerce.number().default(1),
701
+ active: z.boolean().default(false),
702
+ score: z.number().nullable(),
703
+ })
704
+
705
+ const filters = reactive(createSchemaObject(FiltersSchema))
706
+ // Result: { search: '', page: 1, active: false, score: null }
707
+ ```
708
+
709
+ **Options:**
710
+
711
+ - `useDefaults` (default: `true`) — When `true`, uses `.default()` values from the schema. When `false`, uses type-based empty values (`''` for strings, `0` for numbers, `false` for booleans, etc.).
712
+ - `withSchema` (default: `false`) — When `true`, attaches the schema to the result object via `SCHEMA_SYMBOL` (wrapped with `markRaw`). Nested objects also receive their respective schemas.
713
+
714
+ ```typescript
715
+ import { createSchemaObject, SCHEMA_SYMBOL } from 'vue-context-storage/zod'
716
+
717
+ const data = createSchemaObject(FiltersSchema, { withSchema: true })
718
+ data[SCHEMA_SYMBOL] // → FiltersSchema
719
+ ```
720
+
721
+ **Type-based defaults** (when `useDefaults: false` or no `.default()` is set):
722
+
723
+ | Zod type | Default value |
724
+ | ------------- | -------------------------------------------- |
725
+ | `z.string()` | `''` |
726
+ | `z.number()` | `0` (respects `.min()` / `.positive()`) |
727
+ | `z.boolean()` | `false` |
728
+ | `z.array()` | `[]` |
729
+ | `z.object()` | Recursively created via `createSchemaObject` |
730
+ | `z.date()` | `null` |
731
+ | `.nullable()` | `null` |
732
+ | `.optional()` | `undefined` |
733
+
631
734
  ## TypeScript Support
632
735
 
633
736
  Full TypeScript support with type inference:
@@ -658,7 +761,7 @@ type Filters = z.infer<typeof FiltersSchema>
658
761
 
659
762
  ```typescript
660
763
  import { ref } from 'vue'
661
- import { useContextStorageQueryHandler, transform } from 'vue-context-storage'
764
+ import { useContextStorage, transform } from 'vue-context-storage'
662
765
 
663
766
  const pagination = ref({
664
767
  page: 1,
@@ -666,8 +769,8 @@ const pagination = ref({
666
769
  total: 0,
667
770
  })
668
771
 
669
- useContextStorageQueryHandler(pagination, {
670
- prefix: 'page',
772
+ useContextStorage('query', pagination, {
773
+ key: 'page',
671
774
  transform: (data, initial) => ({
672
775
  page: transform.asNumber(data.page, { fallback: 1 }),
673
776
  perPage: transform.asNumber(data.perPage, { fallback: 25 }),
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import * as vue9 from "vue";
1
+ import * as vue29 from "vue";
2
2
  import { ComputedRef, InjectionKey, MaybeRefOrGetter, Plugin, PropType, UnwrapNestedRefs } from "vue";
3
3
  import { LocationQuery, LocationQueryValue } from "vue-router";
4
4
 
5
5
  //#region src/components/ContextStorageActivator.vue.d.ts
6
6
  declare const _default$1: typeof __VLS_export$4;
7
- declare const __VLS_export$4: vue9.DefineComponent<{}, () => vue9.VNode<vue9.RendererNode, vue9.RendererElement, {
7
+ declare const __VLS_export$4: vue29.DefineComponent<{}, () => vue29.VNode<vue29.RendererNode, vue29.RendererElement, {
8
8
  [key: string]: any;
9
- }>, {}, {}, {}, vue9.ComponentOptionsMixin, vue9.ComponentOptionsMixin, {}, string, vue9.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue9.ComponentProvideOptions, true, {}, any>;
9
+ }>, {}, {}, {}, vue29.ComponentOptionsMixin, vue29.ComponentOptionsMixin, {}, string, vue29.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue29.ComponentProvideOptions, true, {}, any>;
10
10
  //#endregion
11
11
  //#region src/handlers/types.d.ts
12
12
  interface HandlerSchema<T> {
@@ -43,8 +43,8 @@ interface RegisterBaseOptions<T> {
43
43
  * status: z.enum(['active', 'inactive']).default('active'),
44
44
  * })
45
45
  *
46
- * useContextStorageQueryHandler(filters, {
47
- * prefix: 'filters',
46
+ * useContextStorage('query', filters, {
47
+ * key: 'filters',
48
48
  * schema: FiltersSchema,
49
49
  * })
50
50
  * ```
@@ -65,55 +65,55 @@ interface ContextStorageHandler<T, O> {
65
65
  //#endregion
66
66
  //#region src/components/ContextStorageCollection.vue.d.ts
67
67
  declare const _default$2: typeof __VLS_export$3;
68
- declare const __VLS_export$3: vue9.DefineComponent<vue9.ExtractPropTypes<{
68
+ declare const __VLS_export$3: vue29.DefineComponent<vue29.ExtractPropTypes<{
69
69
  handlers: {
70
70
  type: PropType<ContextStorageHandlerFactory[]>;
71
71
  default: () => ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
72
72
  };
73
- }>, () => vue9.VNode<vue9.RendererNode, vue9.RendererElement, {
73
+ }>, () => vue29.VNode<vue29.RendererNode, vue29.RendererElement, {
74
74
  [key: string]: any;
75
- }>[] | undefined, {}, {}, {}, vue9.ComponentOptionsMixin, vue9.ComponentOptionsMixin, {}, string, vue9.PublicProps, Readonly<vue9.ExtractPropTypes<{
75
+ }>[] | undefined, {}, {}, {}, vue29.ComponentOptionsMixin, vue29.ComponentOptionsMixin, {}, string, vue29.PublicProps, Readonly<vue29.ExtractPropTypes<{
76
76
  handlers: {
77
77
  type: PropType<ContextStorageHandlerFactory[]>;
78
78
  default: () => ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
79
79
  };
80
80
  }>> & Readonly<{}>, {
81
81
  handlers: ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
82
- }, {}, {}, {}, string, vue9.ComponentProvideOptions, true, {}, any>;
82
+ }, {}, {}, {}, string, vue29.ComponentProvideOptions, true, {}, any>;
83
83
  //#endregion
84
84
  //#region src/components/ContextStorageProvider.vue.d.ts
85
85
  declare const _default$4: typeof __VLS_export$2;
86
- declare const __VLS_export$2: vue9.DefineComponent<vue9.ExtractPropTypes<{
86
+ declare const __VLS_export$2: vue29.DefineComponent<vue29.ExtractPropTypes<{
87
87
  itemKey: {
88
88
  type: StringConstructor;
89
89
  required: true;
90
90
  };
91
- }>, () => vue9.VNode<vue9.RendererNode, vue9.RendererElement, {
91
+ }>, () => vue29.VNode<vue29.RendererNode, vue29.RendererElement, {
92
92
  [key: string]: any;
93
- }>[] | undefined, {}, {}, {}, vue9.ComponentOptionsMixin, vue9.ComponentOptionsMixin, {}, string, vue9.PublicProps, Readonly<vue9.ExtractPropTypes<{
93
+ }>[] | undefined, {}, {}, {}, vue29.ComponentOptionsMixin, vue29.ComponentOptionsMixin, {}, string, vue29.PublicProps, Readonly<vue29.ExtractPropTypes<{
94
94
  itemKey: {
95
95
  type: StringConstructor;
96
96
  required: true;
97
97
  };
98
- }>> & Readonly<{}>, {}, {}, {}, {}, string, vue9.ComponentProvideOptions, true, {}, any>;
98
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, vue29.ComponentProvideOptions, true, {}, any>;
99
99
  //#endregion
100
100
  //#region src/components/ContextStorage.vue.d.ts
101
101
  declare const _default: typeof __VLS_export$1;
102
- declare const __VLS_export$1: vue9.DefineComponent<vue9.ExtractPropTypes<{
102
+ declare const __VLS_export$1: vue29.DefineComponent<vue29.ExtractPropTypes<{
103
103
  handlers: {
104
104
  type: PropType<ContextStorageHandlerFactory[]>;
105
105
  default: () => ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
106
106
  };
107
- }>, () => vue9.VNode<vue9.RendererNode, vue9.RendererElement, {
107
+ }>, () => vue29.VNode<vue29.RendererNode, vue29.RendererElement, {
108
108
  [key: string]: any;
109
- }>[] | undefined, {}, {}, {}, vue9.ComponentOptionsMixin, vue9.ComponentOptionsMixin, {}, string, vue9.PublicProps, Readonly<vue9.ExtractPropTypes<{
109
+ }>[] | undefined, {}, {}, {}, vue29.ComponentOptionsMixin, vue29.ComponentOptionsMixin, {}, string, vue29.PublicProps, Readonly<vue29.ExtractPropTypes<{
110
110
  handlers: {
111
111
  type: PropType<ContextStorageHandlerFactory[]>;
112
112
  default: () => ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
113
113
  };
114
114
  }>> & Readonly<{}>, {
115
115
  handlers: ContextStorageHandlerFactory<{}, RegisterOptions<{}>>[];
116
- }, {}, {}, {}, string, vue9.ComponentProvideOptions, true, {}, any>;
116
+ }, {}, {}, {}, string, vue29.ComponentProvideOptions, true, {}, any>;
117
117
  //#endregion
118
118
  //#region src/prefix.d.ts
119
119
  /**
@@ -129,26 +129,25 @@ type ContextStoragePrefixSegment = string | Partial<Record<string, string>>;
129
129
  * bracket notation (`a[b][c]`).
130
130
  *
131
131
  * @param segments — array provided via inject (stacked by nested ContextStoragePrefix components)
132
- * @param handlerInjectionKeyinjection key of the handler to resolve for
133
- * @param knownHandlerKeys — mapping from injection key to handler type name
132
+ * @param handlerTypehandler type name (e.g. 'query', 'localStorage')
134
133
  */
135
134
  declare const contextStoragePrefixSegmentsInjectKey: InjectionKey<MaybeRefOrGetter<ContextStoragePrefixSegment[]>>;
136
135
  //#endregion
137
136
  //#region src/components/ContextStoragePrefix.vue.d.ts
138
137
  declare const _default$3: typeof __VLS_export;
139
- declare const __VLS_export: vue9.DefineComponent<vue9.ExtractPropTypes<{
138
+ declare const __VLS_export: vue29.DefineComponent<vue29.ExtractPropTypes<{
140
139
  name: {
141
140
  type: PropType<ContextStoragePrefixSegment>;
142
141
  required: true;
143
142
  };
144
- }>, () => vue9.VNode<vue9.RendererNode, vue9.RendererElement, {
143
+ }>, () => vue29.VNode<vue29.RendererNode, vue29.RendererElement, {
145
144
  [key: string]: any;
146
- }>, {}, {}, {}, vue9.ComponentOptionsMixin, vue9.ComponentOptionsMixin, {}, string, vue9.PublicProps, Readonly<vue9.ExtractPropTypes<{
145
+ }>, {}, {}, {}, vue29.ComponentOptionsMixin, vue29.ComponentOptionsMixin, {}, string, vue29.PublicProps, Readonly<vue29.ExtractPropTypes<{
147
146
  name: {
148
147
  type: PropType<ContextStoragePrefixSegment>;
149
148
  required: true;
150
149
  };
151
- }>> & Readonly<{}>, {}, {}, {}, {}, string, vue9.ComponentProvideOptions, true, {}, any>;
150
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, vue29.ComponentProvideOptions, true, {}, any>;
152
151
  //#endregion
153
152
  //#region src/plugin.d.ts
154
153
  declare const VueContextStoragePlugin: Plugin;
@@ -205,7 +204,7 @@ interface QueryHandlerSharedOptions {
205
204
  * Useful, when you have default values, and want to preserve empty state in query.
206
205
  * @example
207
206
  * ```
208
- * Options: {preserveEmptyState: true, prefix: 'filters'}
207
+ * Options: {preserveEmptyState: true, key: 'filters'}
209
208
  *
210
209
  * When filters are empty we will get this in query string:
211
210
  *
@@ -216,7 +215,7 @@ interface QueryHandlerSharedOptions {
216
215
  *
217
216
  * @example
218
217
  * ```
219
- * Options: {preserveEmptyState: false, prefix: 'filters'}
218
+ * Options: {preserveEmptyState: false, key: 'filters'}
220
219
  *
221
220
  * When filters are empty we will get this in query string:
222
221
  *
@@ -270,14 +269,14 @@ interface QueryHandlerBaseOptions extends QueryHandlerSharedOptions {
270
269
  }
271
270
  interface RegisterQueryHandlerBaseOptions<T> extends QueryHandlerSharedOptions {
272
271
  /**
273
- * Prefix in query string.
272
+ * Key in query string.
274
273
  *
275
274
  * @example
276
275
  * ```
277
276
  * filters, table-1[filters], table-2[filters]
278
277
  * ```
279
278
  */
280
- prefix?: string;
279
+ key?: string;
281
280
  /**
282
281
  * Transform function to convert deserialized query parameters to the expected type.
283
282
  *
@@ -298,8 +297,8 @@ interface RegisterQueryHandlerBaseOptions<T> extends QueryHandlerSharedOptions {
298
297
  * ```ts
299
298
  * const data = ref({ page: undefined as number | undefined })
300
299
  *
301
- * useContextStorageQueryHandler(data, {
302
- * prefix: 'filters',
300
+ * useContextStorage('query', data, {
301
+ * key: 'filters',
303
302
  * onlyChanges: true,
304
303
  * additionalDefaultData: { page: 1 },
305
304
  * })
@@ -325,11 +324,6 @@ interface WebStorageHandlerBaseOptions {
325
324
  }
326
325
  interface RegisterWebStorageHandlerBaseOptions<T> {
327
326
  key?: string;
328
- /**
329
- * Optional prefix for nested data within the storage key.
330
- * When provided, data will be stored under this prefix within the main storage object.
331
- */
332
- prefix?: string;
333
327
  /**
334
328
  * Transform function to convert deserialized storage data to the expected type.
335
329
  *
@@ -365,7 +359,10 @@ interface ContextStorageHandlerMap<T> {
365
359
  localStorage: RegisterWebStorageHandlerOptions<T>;
366
360
  sessionStorage: RegisterWebStorageHandlerOptions<T>;
367
361
  }
368
- declare function defineContextStorageHandler<T, O extends object>(name: string, injectionKey: InjectionKey<ContextStorageHandler<T, O>>): void;
362
+ declare function defineContextStorageHandler<T, O extends object>(name: string, injectionKey: InjectionKey<ContextStorageHandler<T, O>>, options?: {
363
+ prefixProperty?: string;
364
+ prefixMergeStrategy?: 'prepend' | 'append';
365
+ }): void;
369
366
  declare function resolveHandlerInjectionKey<K$1 extends keyof ContextStorageHandlerMap<T>, T>(type: K$1): InjectionKey<ContextStorageHandler<T, ContextStorageHandlerMap<T>[K$1]>> | undefined;
370
367
  //#endregion
371
368
  //#region src/composables/types.d.ts
@@ -401,16 +398,13 @@ declare function useContextStorageCollection(handlers?: ContextStorageHandlerFac
401
398
  declare function useContextStorageProvider(key: string): void;
402
399
  //#endregion
403
400
  //#region src/handlers/query/index.d.ts
404
- declare function useContextStorageQueryHandler<T extends Record<string, unknown>>(data: MaybeRefOrGetter<T>, options?: RegisterQueryHandlerOptions<T>): UseContextStorageResult<T>;
405
401
  declare function createQueryHandler(baseOptions?: QueryHandlerBaseOptions): ContextStorageHandlerFactory;
406
402
  //#endregion
407
403
  //#region src/handlers/local-storage/index.d.ts
408
404
  declare function createLocalStorageHandler(customOptions?: WebStorageHandlerBaseOptions): ContextStorageHandlerFactory;
409
- declare const useContextStorageLocalStorage: (data: vue9.MaybeRefOrGetter<Record<string, unknown>>, options: RegisterWebStorageHandlerOptions<Record<string, unknown>>) => UseContextStorageResult<Record<string, unknown>>;
410
405
  //#endregion
411
406
  //#region src/handlers/session-storage/index.d.ts
412
407
  declare function createSessionStorageHandler(customOptions?: WebStorageHandlerBaseOptions): ContextStorageHandlerFactory;
413
- declare const useContextStorageSessionStorage: (data: vue9.MaybeRefOrGetter<Record<string, unknown>>, options: RegisterWebStorageHandlerOptions<Record<string, unknown>>) => UseContextStorageResult<Record<string, unknown>>;
414
408
  //#endregion
415
409
  //#region src/handlers/query/transform-helpers.d.ts
416
410
  declare function asNumber(value: QueryValue | number | undefined): number;
@@ -562,13 +556,13 @@ declare const transform: {
562
556
  //#region src/handlers/query/helpers.d.ts
563
557
  interface SerializeOptions {
564
558
  /**
565
- * Custom prefix for serialized keys.
559
+ * Custom key prefix for serialized keys.
566
560
  * @example
567
- * - prefix: 'filters' => 'filters[key]'
568
- * - prefix: 'search' => 'search[key]'
569
- * - prefix: '' => 'key' (no prefix)
561
+ * - key: 'filters' => 'filters[field]'
562
+ * - key: 'search' => 'search[field]'
563
+ * - key: '' => 'field' (no prefix)
570
564
  */
571
- prefix?: string;
565
+ key?: string;
572
566
  }
573
567
  /**
574
568
  * Serializes filter parameters into a URL-friendly format.
@@ -616,4 +610,4 @@ declare const contextStorageSessionStorageHandlerInjectKey: InjectionKey<Context
616
610
  //#region src/constants.d.ts
617
611
  declare const defaultHandlers: ContextStorageHandlerFactory[];
618
612
  //#endregion
619
- export { type CollectionManager, type CollectionManagerItem, _default as ContextStorage, _default$1 as ContextStorageActivator, _default$2 as ContextStorageCollection, type ContextStorageHandler, type ContextStorageHandlerFactory, type ContextStorageHandlerMap, _default$3 as ContextStoragePrefix, type ContextStoragePrefixSegment, _default$4 as ContextStorageProvider, type WebStorageHandlerBaseOptions as LocalStorageHandlerBaseOptions, type QueryValue, type RegisterBaseOptions, type RegisterWebStorageHandlerBaseOptions as RegisterLocalStorageHandlerBaseOptions, VueContextStoragePlugin, asArray, asBoolean, asNumber, asNumberArray, asObjectArray, asString, contextStorageCollectionInjectKey, contextStorageCollectionItemInjectKey, contextStorageHandlersInjectKey, contextStorageLocalStorageHandlerInjectKey, contextStoragePrefixSegmentsInjectKey, contextStorageQueryHandlerInjectKey, contextStorageSessionStorageHandlerInjectKey, createCollectionManager, createLocalStorageHandler, createQueryHandler, createSessionStorageHandler, defaultHandlers, defineContextStorageHandler, deserializeParams as deserializeQueryParams, resolveHandlerInjectionKey, serializeParams as serializeQueryParams, transform, useContextStorage, useContextStorageActivator, useContextStorageCollection, useContextStorageLocalStorage, useContextStorageProvider, useContextStorageQueryHandler, useContextStorageSessionStorage };
613
+ export { type CollectionManager, type CollectionManagerItem, _default as ContextStorage, _default$1 as ContextStorageActivator, _default$2 as ContextStorageCollection, type ContextStorageHandler, type ContextStorageHandlerFactory, type ContextStorageHandlerMap, _default$3 as ContextStoragePrefix, type ContextStoragePrefixSegment, _default$4 as ContextStorageProvider, type WebStorageHandlerBaseOptions as LocalStorageHandlerBaseOptions, type QueryValue, type RegisterBaseOptions, type RegisterWebStorageHandlerBaseOptions as RegisterLocalStorageHandlerBaseOptions, VueContextStoragePlugin, asArray, asBoolean, asNumber, asNumberArray, asObjectArray, asString, contextStorageCollectionInjectKey, contextStorageCollectionItemInjectKey, contextStorageHandlersInjectKey, contextStorageLocalStorageHandlerInjectKey, contextStoragePrefixSegmentsInjectKey, contextStorageQueryHandlerInjectKey, contextStorageSessionStorageHandlerInjectKey, createCollectionManager, createLocalStorageHandler, createQueryHandler, createSessionStorageHandler, defaultHandlers, defineContextStorageHandler, deserializeParams as deserializeQueryParams, resolveHandlerInjectionKey, serializeParams as serializeQueryParams, transform, useContextStorage, useContextStorageActivator, useContextStorageCollection, useContextStorageProvider };
package/dist/index.js CHANGED
@@ -19,8 +19,7 @@ function joinPrefix(left, right) {
19
19
  return `${left}[${right}]`;
20
20
  }
21
21
  const contextStoragePrefixSegmentsInjectKey = contextStoragePrefixSegments;
22
- function resolvePrefixSegments(segments, handlerInjectionKey, knownHandlerKeys$1) {
23
- const handlerType = knownHandlerKeys$1.get(handlerInjectionKey);
22
+ function resolvePrefixSegments(segments, handlerType) {
24
23
  let combined = "";
25
24
  for (const segment of segments) {
26
25
  let value;
@@ -62,7 +61,7 @@ var ContextStorageActivator_default = ContextStorageActivator_vue_vue_type_scrip
62
61
  //#endregion
63
62
  //#region src/handlers/query/helpers.ts
64
63
  function serializeParams(params, options = {}) {
65
- const { prefix = "" } = options;
64
+ const { key: prefix = "" } = options;
66
65
  const result = {};
67
66
  Object.keys(params).forEach((key) => {
68
67
  const value = params[key];
@@ -80,12 +79,12 @@ function serializeParams(params, options = {}) {
80
79
  });
81
80
  Object.assign(result, serializeParams(indexed, {
82
81
  ...options,
83
- prefix: formattedKey
82
+ key: formattedKey
84
83
  }));
85
84
  } else result[formattedKey] = value.map(String);
86
85
  else Object.assign(result, serializeParams(value, {
87
86
  ...options,
88
- prefix: formattedKey
87
+ key: formattedKey
89
88
  }));
90
89
  else if (typeof value === "boolean") result[formattedKey] = value ? "1" : "0";
91
90
  else result[formattedKey] = String(value);
@@ -145,8 +144,17 @@ function applyTransform(input) {
145
144
  };
146
145
  }
147
146
  const knownHandlerKeys = /* @__PURE__ */ new Map();
148
- function registerKnownHandlerKey(injectionKey, handlerType) {
149
- knownHandlerKeys.set(injectionKey, handlerType);
147
+ function registerKnownHandlerKey(injectionKey, handlerType, prefixProperty = "key", prefixMergeStrategy = "prepend") {
148
+ knownHandlerKeys.set(injectionKey, {
149
+ handlerType,
150
+ prefixProperty,
151
+ prefixMergeStrategy
152
+ });
153
+ }
154
+ function appendBracketNotation(base, suffix) {
155
+ const bracketIdx = suffix.indexOf("[");
156
+ if (bracketIdx === -1) return `${base}[${suffix}]`;
157
+ return `${base}[${suffix.slice(0, bracketIdx)}]${suffix.slice(bracketIdx)}`;
150
158
  }
151
159
  function buildContextStorageHandler(handler, data, options) {
152
160
  const uid = getCurrentInstance()?.uid || 0;
@@ -158,11 +166,16 @@ function buildContextStorageHandler(handler, data, options) {
158
166
  const rawPrefixSegments = inject(contextStoragePrefixSegmentsInjectKey, void 0);
159
167
  const prefixSegments = rawPrefixSegments ? toValue(rawPrefixSegments) : void 0;
160
168
  if (prefixSegments && prefixSegments.length > 0) {
161
- const resolvedPrefix = resolvePrefixSegments(prefixSegments, handler.getInjectionKey(), knownHandlerKeys);
169
+ const handlerInjectionKey = handler.getInjectionKey();
170
+ const handlerInfo = knownHandlerKeys.get(handlerInjectionKey);
171
+ const resolvedPrefix = resolvePrefixSegments(prefixSegments, handlerInfo?.handlerType);
162
172
  if (resolvedPrefix) {
163
- const optionsPrefix = mergedOptions.prefix;
164
- if (optionsPrefix) mergedOptions.prefix = `${resolvedPrefix}[${optionsPrefix}]`;
165
- else mergedOptions.prefix = resolvedPrefix;
173
+ const prefixProp = handlerInfo?.prefixProperty ?? "key";
174
+ const strategy = handlerInfo?.prefixMergeStrategy ?? "prepend";
175
+ const optionsValue = mergedOptions[prefixProp];
176
+ if (optionsValue) if (strategy === "append") mergedOptions[prefixProp] = appendBracketNotation(optionsValue, resolvedPrefix);
177
+ else mergedOptions[prefixProp] = `${resolvedPrefix}[${optionsValue}]`;
178
+ else mergedOptions[prefixProp] = resolvedPrefix;
166
179
  }
167
180
  }
168
181
  const { stop, reset, wasChanged } = handler.register(data, mergedOptions);
@@ -199,23 +212,23 @@ function buildQuery(input) {
199
212
  const warnings = [];
200
213
  const newQueryRaw = {};
201
214
  input.items.forEach((item) => {
202
- const { prefix, onlyChanges = input.onlyChanges } = item;
215
+ const { key, onlyChanges = input.onlyChanges } = item;
203
216
  let preserveEmptyState = item.preserveEmptyState ?? input.preserveEmptyState;
204
- const patch = serializeParams(item.data, { prefix });
217
+ const patch = serializeParams(item.data, { key });
205
218
  if (onlyChanges) {
206
219
  if (preserveEmptyState) {
207
220
  preserveEmptyState = false;
208
221
  warnings.push("[vue-context-storage] preserveEmptyState is not supported with onlyChanges");
209
222
  }
210
- Object.keys(patch).forEach((key) => {
211
- if (isEqual(patch[key], item.initialQueryData[key]) || item.additionalDefaultQueryData && isEqual(patch[key], item.additionalDefaultQueryData[key])) delete patch[key];
223
+ Object.keys(patch).forEach((key2) => {
224
+ if (isEqual(patch[key2], item.initialQueryData[key2]) || item.additionalDefaultQueryData && isEqual(patch[key2], item.additionalDefaultQueryData[key2])) delete patch[key2];
212
225
  });
213
226
  }
214
227
  const patchKeys = Object.keys(patch);
215
- patchKeys.forEach((key) => {
216
- if (Object.hasOwn(newQueryRaw, key)) warnings.push(`[vue-context-storage] Key ${key} is already present, overriding ` + (item.causer || ""));
228
+ patchKeys.forEach((key2) => {
229
+ if (Object.hasOwn(newQueryRaw, key2)) warnings.push(`[vue-context-storage] Key ${key2} is already present, overriding ` + (item.causer || ""));
217
230
  });
218
- if (!patchKeys.length && preserveEmptyState) patch[prefix || input.emptyPlaceholder] = null;
231
+ if (!patchKeys.length && preserveEmptyState) patch[key || input.emptyPlaceholder] = null;
219
232
  Object.assign(newQueryRaw, patch);
220
233
  });
221
234
  let newQuery = { ...newQueryRaw };
@@ -239,8 +252,8 @@ function buildQuery(input) {
239
252
  //#region src/handlers/query/compute-sync-state.ts
240
253
  function computeSyncState(input) {
241
254
  let state = input.deserializedState;
242
- if (typeof input.prefix === "string" && input.prefix.length > 0) {
243
- const parts = input.prefix.split(/[\[\]]/).filter(Boolean);
255
+ if (typeof input.key === "string" && input.key.length > 0) {
256
+ const parts = input.key.split(/[\[\]]/).filter(Boolean);
244
257
  for (const part of parts) {
245
258
  if (state === void 0 || state === null) break;
246
259
  state = state[part];
@@ -262,11 +275,6 @@ function computeSyncState(input) {
262
275
 
263
276
  //#endregion
264
277
  //#region src/handlers/query/index.ts
265
- function useContextStorageQueryHandler(data, options) {
266
- const handler = inject(contextStorageQueryHandler);
267
- if (!handler) throw new Error("[vue-context-storage] ContextStorageQueryHandler is not provided");
268
- return buildContextStorageHandler(handler, data, options);
269
- }
270
278
  function createQueryHandler(baseOptions) {
271
279
  const factory = () => {
272
280
  const route = useRoute();
@@ -358,13 +366,13 @@ function createQueryHandler(baseOptions) {
358
366
  registered.forEach((item) => syncInitialStateToRegisteredItem(item));
359
367
  }
360
368
  function syncInitialStateToRegisteredItem(item) {
361
- const prefix = item.options?.prefix;
369
+ const key = item.options?.key;
362
370
  const { mergeOnlyExistingKeysWithoutTransform = options.mergeOnlyExistingKeysWithoutTransform } = item.options || {};
363
371
  const itemState = toValue(item.data);
364
372
  const result = computeSyncState({
365
373
  deserializedState: deserializeParams(route.query),
366
374
  initialData: item.initialData,
367
- prefix,
375
+ key,
368
376
  emptyPlaceholder: options.emptyPlaceholder
369
377
  });
370
378
  if (result.type === "none") return;
@@ -383,45 +391,45 @@ function createQueryHandler(baseOptions) {
383
391
  });
384
392
  transformed.warnings.forEach((w) => console.warn(w.message, ...w.args));
385
393
  const finalData = { ...transformed.data };
386
- for (const key of Object.keys(itemState)) if (!urlKeys.has(key)) finalData[key] = itemState[key];
394
+ for (const key2 of Object.keys(itemState)) if (!urlKeys.has(key2)) finalData[key2] = itemState[key2];
387
395
  if (isEqual(itemState, finalData)) return;
388
396
  ({ ...itemState });
389
397
  syncReactive(itemState, finalData);
390
398
  }
391
399
  function register(data, registerOptions) {
392
400
  const resolvedData = toValue(data);
393
- if (registeredDataObjects.has(resolvedData)) console.warn("[vue-context-storage] The same data object is already registered in ContextStorageQueryHandler.", { prefix: registerOptions?.prefix });
401
+ if (registeredDataObjects.has(resolvedData)) console.warn("[vue-context-storage] The same data object is already registered in ContextStorageQueryHandler.", { key: registerOptions?.key });
394
402
  registeredDataObjects.add(resolvedData);
395
403
  hasAnyRegistered = true;
396
404
  const watchHandle = watch(data, () => {
397
- registerOptions.prefix;
405
+ registerOptions.key;
398
406
  scheduleSyncToQuery();
399
407
  }, { deep: true });
400
408
  const initialData = cloneDeep(resolvedData);
401
409
  const item = {
402
410
  data,
403
411
  initialData,
404
- initialQueryData: serializeParams(initialData, { prefix: registerOptions.prefix }),
405
- additionalDefaultQueryData: registerOptions.additionalDefaultData ? serializeParams(registerOptions.additionalDefaultData, { prefix: registerOptions.prefix }) : void 0,
412
+ initialQueryData: serializeParams(initialData, { key: registerOptions.key }),
413
+ additionalDefaultQueryData: registerOptions.additionalDefaultData ? serializeParams(registerOptions.additionalDefaultData, { key: registerOptions.key }) : void 0,
406
414
  options: registerOptions,
407
415
  watchHandle
408
416
  };
409
417
  registered.push(item);
410
418
  const syncCallback = () => {
411
- registerOptions.prefix;
419
+ registerOptions.key;
412
420
  syncInitialStateToRegisteredItem(item);
413
421
  scheduleSyncToQuery();
414
422
  };
415
423
  if (preventAfterEachRouteCallsWhileCallingRouter) {
416
- registerOptions.prefix;
424
+ registerOptions.key;
417
425
  setTimeout(syncCallback);
418
426
  } else {
419
- registerOptions.prefix;
427
+ registerOptions.key;
420
428
  syncCallback();
421
429
  }
422
430
  return {
423
431
  stop: () => {
424
- registerOptions.prefix;
432
+ registerOptions.key;
425
433
  item.watchHandle.stop();
426
434
  const index = registered.indexOf(item);
427
435
  if (index !== -1) registered.splice(index, 1);
@@ -430,7 +438,7 @@ function createQueryHandler(baseOptions) {
430
438
  currentQuery = void 0;
431
439
  },
432
440
  reset: () => {
433
- registerOptions.prefix;
441
+ registerOptions.key;
434
442
  syncReactive(toValue(data), cloneDeep(initialData));
435
443
  },
436
444
  wasChanged: computed(() => !isEqual(toValue(data), initialData))
@@ -442,7 +450,7 @@ function createQueryHandler(baseOptions) {
442
450
  data: toValue(item.data),
443
451
  initialQueryData: item.initialQueryData,
444
452
  additionalDefaultQueryData: item.additionalDefaultQueryData,
445
- prefix: item.options?.prefix,
453
+ key: item.options?.key,
446
454
  onlyChanges: item.options?.onlyChanges,
447
455
  preserveEmptyState: item.options?.preserveEmptyState,
448
456
  causer: item.options?.causer
@@ -485,19 +493,10 @@ function createWebStorageHandlerInstance(config) {
485
493
  window.removeEventListener("storage", handler);
486
494
  });
487
495
  }
488
- function resolveStorageKey(key, prefix) {
489
- if (prefix) {
490
- const bracketIdx = prefix.indexOf("[");
491
- if (bracketIdx === -1) return `${key}[${prefix}]`;
492
- return `${key}[${prefix.slice(0, bracketIdx)}]${prefix.slice(bracketIdx)}`;
493
- }
494
- return key;
495
- }
496
496
  function handleStorageEvent(event) {
497
497
  if (!enabled) return;
498
498
  registered.forEach((item) => {
499
- const effectiveKey = resolveStorageKey(item.options.key, item.options.prefix);
500
- if (event.key === effectiveKey) syncStorageToRegisteredItem(item);
499
+ if (event.key === item.options.key) syncStorageToRegisteredItem(item);
501
500
  });
502
501
  }
503
502
  function getInjectionKey() {
@@ -514,23 +513,23 @@ function createWebStorageHandlerInstance(config) {
514
513
  function syncRegisteredToStorage() {
515
514
  if (!enabled) return;
516
515
  registered.forEach((item) => {
517
- const effectiveKey = resolveStorageKey(item.options.key, item.options.prefix);
516
+ const storageKey = item.options.key;
518
517
  const data = toValue(item.data);
519
518
  const { serializer } = item.options;
520
519
  try {
521
- if (serializer) config.storage.setItem(effectiveKey, serializer(data));
522
- else config.storage.setItem(effectiveKey, JSON.stringify(data));
520
+ if (serializer) config.storage.setItem(storageKey, serializer(data));
521
+ else config.storage.setItem(storageKey, JSON.stringify(data));
523
522
  } catch (e) {
524
523
  console.error("[vue-context-storage] Error writing to storage", e);
525
524
  }
526
525
  });
527
526
  }
528
527
  function syncStorageToRegisteredItem(item) {
529
- const { key, prefix, deserializer } = item.options;
530
- const effectiveKey = resolveStorageKey(key, prefix);
528
+ const { key, deserializer } = item.options;
529
+ const storageKey = key;
531
530
  let stored = null;
532
531
  try {
533
- stored = config.storage.getItem(effectiveKey);
532
+ stored = config.storage.getItem(storageKey);
534
533
  } catch {
535
534
  return;
536
535
  }
@@ -540,7 +539,7 @@ function createWebStorageHandlerInstance(config) {
540
539
  if (deserializer) deserialized = deserializer(stored);
541
540
  else deserialized = JSON.parse(stored);
542
541
  } catch {
543
- console.warn("[vue-context-storage] Failed to parse storage data for key:", effectiveKey);
542
+ console.warn("[vue-context-storage] Failed to parse storage data for key:", storageKey);
544
543
  return;
545
544
  }
546
545
  if (deserialized === void 0 || deserialized === null) return;
@@ -562,10 +561,7 @@ function createWebStorageHandlerInstance(config) {
562
561
  function register(data, options) {
563
562
  if (!options.key) throw new Error("[vue-context-storage] Storage handler requires a key option");
564
563
  const resolvedData = toValue(data);
565
- if (registeredDataObjects.has(resolvedData)) console.warn(`[vue-context-storage] The same data object is already registered in ${config.handlerName}.`, {
566
- key: options.key,
567
- prefix: options.prefix
568
- });
564
+ if (registeredDataObjects.has(resolvedData)) console.warn(`[vue-context-storage] The same data object is already registered in ${config.handlerName}.`, { key: options.key });
569
565
  registeredDataObjects.add(resolvedData);
570
566
  hasAnyRegistered = true;
571
567
  const watchHandle = watch(data, () => syncRegisteredToStorage(), { deep: true });
@@ -597,13 +593,6 @@ function createWebStorageHandlerInstance(config) {
597
593
  getInjectionKey
598
594
  };
599
595
  }
600
- function createWebStorageComposable(injectionKey, handlerName) {
601
- return function useContextStorageWebStorage(data, options) {
602
- const handler = inject(injectionKey);
603
- if (!handler) throw new Error(`[vue-context-storage] ${handlerName} is not provided`);
604
- return buildContextStorageHandler(handler, data, options);
605
- };
606
- }
607
596
 
608
597
  //#endregion
609
598
  //#region src/handlers/local-storage/index.ts
@@ -619,7 +608,6 @@ function createLocalStorageHandler(customOptions) {
619
608
  });
620
609
  return factory;
621
610
  }
622
- const useContextStorageLocalStorage = createWebStorageComposable(contextStorageLocalStorageHandler, "ContextStorageLocalStorageHandler");
623
611
 
624
612
  //#endregion
625
613
  //#region src/handlers/session-storage/index.ts
@@ -635,7 +623,6 @@ function createSessionStorageHandler(customOptions) {
635
623
  });
636
624
  return factory;
637
625
  }
638
- const useContextStorageSessionStorage = createWebStorageComposable(contextStorageSessionStorageHandler, "ContextStorageSessionStorageHandler");
639
626
 
640
627
  //#endregion
641
628
  //#region src/constants.ts
@@ -847,16 +834,22 @@ const VueContextStoragePlugin = { install(app) {
847
834
  //#endregion
848
835
  //#region src/registry.ts
849
836
  const handlerRegistry = /* @__PURE__ */ new Map();
850
- function defineContextStorageHandler(name, injectionKey) {
837
+ function defineContextStorageHandler(name, injectionKey, options) {
851
838
  handlerRegistry.set(name, injectionKey);
852
- registerKnownHandlerKey(injectionKey, name);
839
+ registerKnownHandlerKey(injectionKey, name, options?.prefixProperty, options?.prefixMergeStrategy);
853
840
  }
854
841
  function resolveHandlerInjectionKey(type) {
855
842
  return handlerRegistry.get(type);
856
843
  }
857
- defineContextStorageHandler("query", contextStorageQueryHandlerInjectKey);
858
- defineContextStorageHandler("localStorage", contextStorageLocalStorageHandlerInjectKey);
859
- defineContextStorageHandler("sessionStorage", contextStorageSessionStorageHandlerInjectKey);
844
+ defineContextStorageHandler("query", contextStorageQueryHandlerInjectKey, { prefixProperty: "key" });
845
+ defineContextStorageHandler("localStorage", contextStorageLocalStorageHandlerInjectKey, {
846
+ prefixProperty: "key",
847
+ prefixMergeStrategy: "append"
848
+ });
849
+ defineContextStorageHandler("sessionStorage", contextStorageSessionStorageHandlerInjectKey, {
850
+ prefixProperty: "key",
851
+ prefixMergeStrategy: "append"
852
+ });
860
853
 
861
854
  //#endregion
862
855
  //#region src/composables/useContextStorage.ts
@@ -945,4 +938,4 @@ const transform = {
945
938
  };
946
939
 
947
940
  //#endregion
948
- export { ContextStorage_default as ContextStorage, ContextStorageActivator_default as ContextStorageActivator, ContextStorageCollection_default as ContextStorageCollection, ContextStoragePrefix_default as ContextStoragePrefix, ContextStorageProvider_default as ContextStorageProvider, VueContextStoragePlugin, asArray, asBoolean, asNumber, asNumberArray, asObjectArray, asString, contextStorageCollectionInjectKey, contextStorageCollectionItemInjectKey, contextStorageHandlersInjectKey, contextStorageLocalStorageHandlerInjectKey, contextStoragePrefixSegmentsInjectKey, contextStorageQueryHandlerInjectKey, contextStorageSessionStorageHandlerInjectKey, createCollectionManager, createLocalStorageHandler, createQueryHandler, createSessionStorageHandler, defaultHandlers, defineContextStorageHandler, deserializeParams as deserializeQueryParams, resolveHandlerInjectionKey, serializeParams as serializeQueryParams, transform, useContextStorage, useContextStorageActivator, useContextStorageCollection, useContextStorageLocalStorage, useContextStorageProvider, useContextStorageQueryHandler, useContextStorageSessionStorage };
941
+ export { ContextStorage_default as ContextStorage, ContextStorageActivator_default as ContextStorageActivator, ContextStorageCollection_default as ContextStorageCollection, ContextStoragePrefix_default as ContextStoragePrefix, ContextStorageProvider_default as ContextStorageProvider, VueContextStoragePlugin, asArray, asBoolean, asNumber, asNumberArray, asObjectArray, asString, contextStorageCollectionInjectKey, contextStorageCollectionItemInjectKey, contextStorageHandlersInjectKey, contextStorageLocalStorageHandlerInjectKey, contextStoragePrefixSegmentsInjectKey, contextStorageQueryHandlerInjectKey, contextStorageSessionStorageHandlerInjectKey, createCollectionManager, createLocalStorageHandler, createQueryHandler, createSessionStorageHandler, defaultHandlers, defineContextStorageHandler, deserializeParams as deserializeQueryParams, resolveHandlerInjectionKey, serializeParams as serializeQueryParams, transform, useContextStorage, useContextStorageActivator, useContextStorageCollection, useContextStorageProvider };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vue-context-storage",
3
3
  "type": "module",
4
- "version": "0.1.33",
4
+ "version": "0.1.35",
5
5
  "description": "Vue 3 context storage system with URL query synchronization support",
6
6
  "author": "",
7
7
  "license": "MIT",