vue-context-storage 0.1.40 → 0.1.42
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 +963 -910
- package/dist/index.d.ts +19 -19
- package/dist/index.js +120 -5
- package/dist/zod.d.ts +1 -81
- package/dist/zod.js +1 -13
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,910 +1,963 @@
|
|
|
1
|
-
# vue-context-storage
|
|
2
|
-
|
|
3
|
-
Vue 3
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/vue-context-storage)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://vuejs.org/)
|
|
8
|
-
[](https://bundlephobia.com/package/vue-context-storage)
|
|
9
|
-
[](https://github.com/lviobio/vue-context-storage/issues)
|
|
10
|
-
[](https://github.com/lviobio/vue-context-storage)
|
|
11
|
-

|
|
12
|
-

|
|
13
|
-
[](https://codecov.io/gh/lviobio/vue-context-storage)
|
|
14
|
-
[](https://lviobio.github.io/vue-context-storage/)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
app
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```vue
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
import {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
useContextStorage
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
useContextStorage('
|
|
418
|
-
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
})
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
useContextStorage('
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
})
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1
|
+
# vue-context-storage
|
|
2
|
+
|
|
3
|
+
Vue 3 reactive state management — sync state with the URL query, localStorage, and sessionStorage through a single type-safe composable.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vue-context-storage)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://vuejs.org/)
|
|
8
|
+
[](https://bundlephobia.com/package/vue-context-storage)
|
|
9
|
+
[](https://github.com/lviobio/vue-context-storage/issues)
|
|
10
|
+
[](https://github.com/lviobio/vue-context-storage)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
[](https://codecov.io/gh/lviobio/vue-context-storage)
|
|
14
|
+
[](https://lviobio.github.io/vue-context-storage/)
|
|
15
|
+
|
|
16
|
+
Key features:
|
|
17
|
+
|
|
18
|
+
- **Automatic URL query sync** — painless type coercion (numbers, booleans, arrays) and nested objects
|
|
19
|
+
- **localStorage & sessionStorage** for persistent and session-scoped state — same API as the query handler
|
|
20
|
+
- **Context prefixing** to avoid key collisions when the same key is reused across instances
|
|
21
|
+
- **Type-safe** with optional Zod schema validation
|
|
22
|
+
- **Tree-shakeable** and lightweight
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const filters = reactive({ search: '', page: 1 })
|
|
26
|
+
|
|
27
|
+
// reactive state ⇄ URL query — kept in sync automatically, both directions
|
|
28
|
+
useContextStorage('query', filters) // URL: /products?search=shoes&page=2
|
|
29
|
+
useContextStorage('query', filters, { key: 'filters' }) // URL: /products?filters[search]=shoes&filters[page]=2
|
|
30
|
+
// zod schema support with type coercion, 'page' will be converted to number
|
|
31
|
+
useContextStorage('query', filters, { schema: z.object({ search: z.string(), page: z.number() }) }) // URL: /products?filters[search]=shoes&filters[page]=2
|
|
32
|
+
// transform function support with type coercion, 'page' will be converted to number
|
|
33
|
+
useContextStorage('query', filters, { transform: (value) => ({ search: value.search, page: Number(value.page) }) }) // URL: /products?filters[search]=shoes&filters[page]=2
|
|
34
|
+
// And a lot of other features... (onlyChanges option; createEmptyObject helper for zod schemas; additional default values; passing 'key' via ContextStoragePrefix wrapper)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Live Demo
|
|
38
|
+
|
|
39
|
+
🚀 **[Try the interactive playground](https://lviobio.github.io/vue-context-storage)**
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install vue-context-storage
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- ✅ **Vue 3 Composition API** - Built with modern Vue patterns
|
|
50
|
+
- ✅ **URL Query Sync** - Automatically sync state with URL parameters
|
|
51
|
+
- ✅ **localStorage Handler** - Persist state to localStorage with cross-tab sync
|
|
52
|
+
- ✅ **sessionStorage Handler** - Session-scoped state that survives page refreshes
|
|
53
|
+
- ✅ **Multiple Contexts** - Support multiple independent storage contexts
|
|
54
|
+
- ✅ **TypeScript** - Full type safety and IntelliSense support
|
|
55
|
+
- ✅ **Flexible** - Works with vue-router 4+ or 5+
|
|
56
|
+
- ✅ **Transform Helpers** - Built-in utilities for type conversion
|
|
57
|
+
|
|
58
|
+
## Motivation
|
|
59
|
+
|
|
60
|
+
In Vue applications, reactive state often needs to live beyond a single component. Filters, pagination, sorting, and user preferences must survive page reloads, be shareable via URL, or persist across sessions. Solving this typically means writing the same boilerplate over and over: manually reading and writing query parameters with vue-router, serializing objects to localStorage, handling type coercion from URL strings, and keeping everything in sync.
|
|
61
|
+
|
|
62
|
+
`vue-context-storage` eliminates that repetitive work. You declare your reactive state once, point it at a storage target, and the library handles the rest:
|
|
63
|
+
|
|
64
|
+
- **URL query parameters** stay in sync with your data automatically - users can bookmark or share a page and get the exact same state back.
|
|
65
|
+
- **localStorage and sessionStorage** are kept up to date without manual `getItem`/`setItem` calls, including cross-tab synchronization.
|
|
66
|
+
- **Type safety** is preserved end-to-end: URL strings are coerced back to numbers, booleans, and arrays via transform helpers or Zod schemas.
|
|
67
|
+
- **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.
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
## Basic Usage
|
|
72
|
+
|
|
73
|
+
### Option 1: Manual Component Import (Recommended)
|
|
74
|
+
|
|
75
|
+
Import ContextStorage component in your `App.vue`:
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<template>
|
|
79
|
+
<ContextStorage>
|
|
80
|
+
<router-view />
|
|
81
|
+
</ContextStorage>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { ContextStorage } from 'vue-context-storage'
|
|
86
|
+
</script>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Option 2: Using Vue Plugin
|
|
90
|
+
|
|
91
|
+
Register the plugin in your main app file:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createApp } from 'vue'
|
|
95
|
+
import { VueContextStoragePlugin } from 'vue-context-storage'
|
|
96
|
+
import App from './App.vue'
|
|
97
|
+
|
|
98
|
+
const app = createApp(App)
|
|
99
|
+
|
|
100
|
+
// Register components globally
|
|
101
|
+
app.use(VueContextStoragePlugin)
|
|
102
|
+
|
|
103
|
+
app.mount('#app')
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then use components without importing in your `App.vue`:
|
|
107
|
+
|
|
108
|
+
```vue
|
|
109
|
+
<template>
|
|
110
|
+
<ContextStorage>
|
|
111
|
+
<router-view />
|
|
112
|
+
</ContextStorage>
|
|
113
|
+
</template>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Unified Composable
|
|
117
|
+
|
|
118
|
+
`useContextStorage()` provides a single entry point for all handler types:
|
|
119
|
+
|
|
120
|
+
```vue
|
|
121
|
+
<script setup lang="ts">
|
|
122
|
+
import { reactive } from 'vue'
|
|
123
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
124
|
+
|
|
125
|
+
const filters = reactive({
|
|
126
|
+
search: '',
|
|
127
|
+
status: 'active',
|
|
128
|
+
page: 1,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Sync with URL query
|
|
132
|
+
useContextStorage('query', filters, {
|
|
133
|
+
key: 'filters',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Sync with localStorage
|
|
137
|
+
useContextStorage('localStorage', filters, {
|
|
138
|
+
key: 'saved-filters',
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Sync with sessionStorage
|
|
142
|
+
useContextStorage('sessionStorage', filters, {
|
|
143
|
+
key: 'temp-filters',
|
|
144
|
+
})
|
|
145
|
+
</script>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Important: Query handler type coercion.** URL query parameters are always strings. When state is restored from the URL, non-string values lose their original types — `{ page: 1 }` becomes `{ page: "1" }`, booleans become `"true"` / `"false"`, and arrays are restored as plain objects. Always use `schema` or `transform` option when using the query handler with non-string values:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Option 1: Zod schema (recommended)
|
|
152
|
+
useContextStorage('query', filters, {
|
|
153
|
+
key: 'filters',
|
|
154
|
+
schema: z.object({
|
|
155
|
+
page: z.coerce.number().default(1),
|
|
156
|
+
search: z.string().default(''),
|
|
157
|
+
status: z.string().default('active'),
|
|
158
|
+
}),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Option 2: Transform function
|
|
162
|
+
import { transform } from 'vue-context-storage'
|
|
163
|
+
|
|
164
|
+
useContextStorage('query', filters, {
|
|
165
|
+
key: 'filters',
|
|
166
|
+
transform: (deserialized, initial) => ({
|
|
167
|
+
page: transform.asNumber(deserialized.page, { fallback: initial.page }),
|
|
168
|
+
search: transform.asString(deserialized.search, { fallback: initial.search }),
|
|
169
|
+
status: transform.asString(deserialized.status, { fallback: initial.status }),
|
|
170
|
+
}),
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Without type coercion, comparisons like `page === 1` will silently fail after URL restore (the actual value will be `"1"`). The library emits a runtime `console.warn` when it detects non-string values registered without `schema` or `transform`.
|
|
175
|
+
|
|
176
|
+
Options are type-checked per handler — `'query'` accepts query options, `'localStorage'` and `'sessionStorage'` require a `key`, etc.
|
|
177
|
+
|
|
178
|
+
You can also pass an injection key directly instead of a string:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { contextStorageQueryHandlerInjectKey } from 'vue-context-storage'
|
|
182
|
+
|
|
183
|
+
useContextStorage(contextStorageQueryHandlerInjectKey, filters, {
|
|
184
|
+
key: 'filters',
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Use Query Handler in Components
|
|
189
|
+
|
|
190
|
+
Sync reactive state with URL query parameters:
|
|
191
|
+
|
|
192
|
+
```vue
|
|
193
|
+
<script setup lang="ts">
|
|
194
|
+
import { reactive } from 'vue'
|
|
195
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
196
|
+
|
|
197
|
+
interface Filters {
|
|
198
|
+
search: string
|
|
199
|
+
status: string
|
|
200
|
+
page: number
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const filters = reactive<Filters>({
|
|
204
|
+
search: '',
|
|
205
|
+
status: 'active',
|
|
206
|
+
page: 1,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Automatically syncs filters with URL query
|
|
210
|
+
useContextStorage('query', filters, {
|
|
211
|
+
key: 'filters', // URL will be: ?filters[search]=...&filters[status]=...
|
|
212
|
+
})
|
|
213
|
+
</script>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Advanced Usage
|
|
217
|
+
|
|
218
|
+
### Using Transform Helpers
|
|
219
|
+
|
|
220
|
+
Convert URL query string values to proper types:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { ref } from 'vue'
|
|
224
|
+
import { useContextStorage, transform } from 'vue-context-storage'
|
|
225
|
+
|
|
226
|
+
interface TableState {
|
|
227
|
+
page: number
|
|
228
|
+
search: string
|
|
229
|
+
perPage: number
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const state = ref<TableState>({
|
|
233
|
+
page: 1,
|
|
234
|
+
search: '',
|
|
235
|
+
perPage: 25,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
useContextStorage('query', state, {
|
|
239
|
+
key: 'table',
|
|
240
|
+
transform: (deserialized, initial) => ({
|
|
241
|
+
page: transform.asNumber(deserialized.page, { fallback: 1 }),
|
|
242
|
+
search: transform.asString(deserialized.search, { fallback: '' }),
|
|
243
|
+
perPage: transform.asNumber(deserialized.perPage, { fallback: 25 }),
|
|
244
|
+
}),
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Available Transform Helpers
|
|
249
|
+
|
|
250
|
+
- `asNumber(value, options)` - Convert to number
|
|
251
|
+
- `asString(value, options)` - Convert to string
|
|
252
|
+
- `asBoolean(value, options)` - Convert to boolean
|
|
253
|
+
- `asArray(value, options)` - Convert to array
|
|
254
|
+
- `asNumberArray(value, options)` - Convert to number array
|
|
255
|
+
- `asObjectArray(value, options)` - Convert indexed object to array of objects (see [Arrays of Objects](#arrays-of-objects))
|
|
256
|
+
|
|
257
|
+
### Using Zod Schemas
|
|
258
|
+
|
|
259
|
+
Alternatively, you can use [Zod](https://zod.dev/) schemas for automatic validation and type inference:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { z } from 'zod'
|
|
263
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
264
|
+
|
|
265
|
+
// Define schema with automatic coercion
|
|
266
|
+
const FiltersSchema = z.object({
|
|
267
|
+
search: z.string().default(''),
|
|
268
|
+
page: z.coerce.number().int().positive().default(1),
|
|
269
|
+
status: z.enum(['active', 'inactive']).default('active'),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const filters = ref(FiltersSchema.parse({}))
|
|
273
|
+
|
|
274
|
+
// Use schema for automatic validation
|
|
275
|
+
useContextStorage('query', filters, {
|
|
276
|
+
key: 'filters',
|
|
277
|
+
schema: FiltersSchema,
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Benefits:**
|
|
282
|
+
|
|
283
|
+
- Automatic type coercion (strings → numbers, etc.)
|
|
284
|
+
- Runtime validation with detailed errors
|
|
285
|
+
- Automatic TypeScript type inference
|
|
286
|
+
- Less boilerplate code
|
|
287
|
+
- Single source of truth for structure and validation
|
|
288
|
+
|
|
289
|
+
### Arrays of Objects
|
|
290
|
+
|
|
291
|
+
The query handler supports arrays of objects. They are serialized as indexed query parameters:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
?items[0][product]=Apple&items[0][quantity]=5&items[1][product]=Banana&items[1][quantity]=10
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
After deserialization, URL parameters produce indexed objects (`{ '0': {...}, '1': {...} }`) rather than arrays. Use `transform.asObjectArray` or the Zod helper `zObjectArray` to convert them back.
|
|
298
|
+
|
|
299
|
+
**With transform helpers:**
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { reactive } from 'vue'
|
|
303
|
+
import { useContextStorage, transform } from 'vue-context-storage'
|
|
304
|
+
|
|
305
|
+
const data = reactive({
|
|
306
|
+
title: '',
|
|
307
|
+
items: [] as { product: string; quantity: number }[],
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
useContextStorage('query', data, {
|
|
311
|
+
transform: (value) => ({
|
|
312
|
+
title: transform.asString(value.title),
|
|
313
|
+
items: transform.asObjectArray(value.items, (entry) => ({
|
|
314
|
+
product: transform.asString(entry.product),
|
|
315
|
+
quantity: transform.asNumber(entry.quantity),
|
|
316
|
+
})),
|
|
317
|
+
}),
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`asObjectArray` also supports a callback shorthand — pass a function as the second argument instead of an options object.
|
|
322
|
+
|
|
323
|
+
**With Zod schema:**
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { z } from 'zod'
|
|
327
|
+
import { zObjectArray } from 'vue-context-storage/zod'
|
|
328
|
+
|
|
329
|
+
const ItemSchema = z.object({
|
|
330
|
+
product: z.string().default(''),
|
|
331
|
+
quantity: z.coerce.number().default(0),
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const DataSchema = z.object({
|
|
335
|
+
title: z.string().default(''),
|
|
336
|
+
items: zObjectArray(ItemSchema),
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
useContextStorage('query', data, { schema: DataSchema })
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
See [Zod Helpers](#zod-helpers-vue-context-storagezod) for more details.
|
|
343
|
+
|
|
344
|
+
### Preserve Empty State
|
|
345
|
+
|
|
346
|
+
Keep empty state in URL to prevent resetting on reload:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
useContextStorage('query', filters, {
|
|
350
|
+
key: 'filters',
|
|
351
|
+
preserveEmptyState: true,
|
|
352
|
+
// Empty filters will show as: ?filters
|
|
353
|
+
// Without this option, empty filters would clear the URL completely
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Additional Default Data
|
|
358
|
+
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
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:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const data = ref({ page: undefined as number | undefined })
|
|
365
|
+
|
|
366
|
+
useContextStorage('query', data, {
|
|
367
|
+
key: 'filters',
|
|
368
|
+
onlyChanges: true,
|
|
369
|
+
additionalDefaultData: { page: 1 },
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// page=undefined → not in query (matches initial)
|
|
373
|
+
// page=1 → not in query (matches additionalDefaultData)
|
|
374
|
+
// page=2 → appears in query as ?filters[page]=2
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
When using Zod schemas, you can also specify `additionalDefaultData` per-field via `.meta()` instead of (or in addition to) the option:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const Schema = z.object({
|
|
381
|
+
page: z.coerce.number().default(1).meta({ additionalDefaultData: 3 }),
|
|
382
|
+
search: z.string().default(''),
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const data = reactive({ page: undefined as number | undefined, search: '' })
|
|
386
|
+
|
|
387
|
+
useContextStorage('query', data, {
|
|
388
|
+
key: 'filters',
|
|
389
|
+
schema: Schema,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// page=1 → not in query (matches default from schema)
|
|
393
|
+
// page=2 → appears in query as ?filters[page]=2
|
|
394
|
+
// page=3 → not in query (matches additionalDefaultData from schema meta)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
A field's schema `.default(...)` value is itself treated as a default baseline: a value equal to it is omitted from the URL, exactly like the initial snapshot and `additionalDefaultData`. So both the `.default()` value and any `.meta({ additionalDefaultData })` value are excluded, while every other value appears in the query.
|
|
398
|
+
|
|
399
|
+
Each source of defaults is an **independent baseline** — the initial snapshot, the option-level `additionalDefaultData`, the schema `.meta({ additionalDefaultData })`, and the schema `.default()`. A value is omitted from the URL if it matches **any** of them, so they never overwrite each other (e.g. a field can have a `.default()` of `1`, a meta value of `5`, and an option value of `3`, and all three are excluded).
|
|
400
|
+
|
|
401
|
+
Nested objects work the same way. Following the documented best practice of declaring `.default()` at the object level, a value matching the nested default is omitted too:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const Schema = z.object({
|
|
405
|
+
filters: z
|
|
406
|
+
.object({
|
|
407
|
+
page: z.coerce.number(),
|
|
408
|
+
sort: z.string(),
|
|
409
|
+
})
|
|
410
|
+
.default({ page: 1, sort: 'asc' }),
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
const data = reactive({
|
|
414
|
+
filters: { page: undefined as number | undefined, sort: undefined as string | undefined },
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
useContextStorage('query', data, { key: 'q', schema: Schema })
|
|
418
|
+
|
|
419
|
+
// filters = { page: 1, sort: 'asc' } → not in query (matches the nested default)
|
|
420
|
+
// filters = { page: 2, sort: 'asc' } → ?q[filters][page]=2 (sort matches default, omitted)
|
|
421
|
+
// filters = { page: 2, sort: 'desc' } → ?q[filters][page]=2&q[filters][sort]=desc
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Field-level defaults inside a nested object take priority over the object-level default and fill in any fields the object-level default omits.
|
|
425
|
+
|
|
426
|
+
### Configure Query Handler
|
|
427
|
+
|
|
428
|
+
Customize behavior by passing options to the factory:
|
|
429
|
+
|
|
430
|
+
```vue
|
|
431
|
+
<template>
|
|
432
|
+
<!-- Override a single default handler without listing all of them -->
|
|
433
|
+
<ContextStorage :additional-handlers="[createQueryHandler({ mode: 'push' })]">
|
|
434
|
+
<RouterView />
|
|
435
|
+
</ContextStorage>
|
|
436
|
+
</template>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
The `additional-handlers` prop merges with the default handlers, replacing any handler of the same type (matched by injection key). This is the recommended way to customize a single handler.
|
|
440
|
+
|
|
441
|
+
If you need full control over all handlers, use the `handlers` prop instead:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import {
|
|
445
|
+
createQueryHandler,
|
|
446
|
+
createLocalStorageHandler,
|
|
447
|
+
createSessionStorageHandler,
|
|
448
|
+
} from 'vue-context-storage'
|
|
449
|
+
|
|
450
|
+
const customHandlers = [
|
|
451
|
+
createQueryHandler({
|
|
452
|
+
mode: 'push', // 'replace' (default) or 'push' for history
|
|
453
|
+
preserveUnusedKeys: false, // Default is true — set to false for exclusive query ownership
|
|
454
|
+
preserveEmptyState: false,
|
|
455
|
+
}),
|
|
456
|
+
createLocalStorageHandler(),
|
|
457
|
+
createSessionStorageHandler(),
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
// Pass to ContextStorage or ContextStorageCollection component:
|
|
461
|
+
// <ContextStorage :handlers="customHandlers">
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Use localStorage Handler in Components
|
|
465
|
+
|
|
466
|
+
Persist reactive state to `localStorage`. Data is automatically synced across browser tabs.
|
|
467
|
+
|
|
468
|
+
```vue
|
|
469
|
+
<script setup lang="ts">
|
|
470
|
+
import { reactive } from 'vue'
|
|
471
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
472
|
+
|
|
473
|
+
const settings = reactive({
|
|
474
|
+
theme: 'light',
|
|
475
|
+
fontSize: 14,
|
|
476
|
+
sidebarOpen: true,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
// Automatically syncs settings with localStorage under the key "app-settings"
|
|
480
|
+
useContextStorage('localStorage', settings, {
|
|
481
|
+
key: 'app-settings',
|
|
482
|
+
})
|
|
483
|
+
</script>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Configure localStorage Handler
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { createLocalStorageHandler } from 'vue-context-storage'
|
|
490
|
+
|
|
491
|
+
const customLocalStorage = createLocalStorageHandler({
|
|
492
|
+
listenToStorageEvents: true, // Cross-tab sync (default: true)
|
|
493
|
+
})
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Use sessionStorage Handler in Components
|
|
497
|
+
|
|
498
|
+
Persist reactive state to `sessionStorage`. Data survives page refreshes but is cleared when the tab is closed.
|
|
499
|
+
|
|
500
|
+
```vue
|
|
501
|
+
<script setup lang="ts">
|
|
502
|
+
import { reactive } from 'vue'
|
|
503
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
504
|
+
|
|
505
|
+
const formDraft = reactive({
|
|
506
|
+
email: '',
|
|
507
|
+
message: '',
|
|
508
|
+
step: 1,
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// Automatically syncs form draft with sessionStorage
|
|
512
|
+
useContextStorage('sessionStorage', formDraft, {
|
|
513
|
+
key: 'contact-form-draft',
|
|
514
|
+
})
|
|
515
|
+
</script>
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Multiple Registrations Under One Root Key
|
|
519
|
+
|
|
520
|
+
Use bracket notation in `key` to store multiple data objects under a common root:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
const filters = reactive({ search: '', status: 'active' })
|
|
524
|
+
|
|
525
|
+
useContextStorage('sessionStorage', filters, {
|
|
526
|
+
key: 'app-state[filters]', // Storage key: 'app-state[filters]'
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
const pagination = reactive({ page: 1, perPage: 25 })
|
|
530
|
+
|
|
531
|
+
useContextStorage('sessionStorage', pagination, {
|
|
532
|
+
key: 'app-state[pagination]', // Storage key: 'app-state[pagination]'
|
|
533
|
+
})
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Or use `<ContextStoragePrefix>` for automatic scoping (see [Prefix Scoping](#prefix-scoping-with-contextstorageprefix)).
|
|
537
|
+
|
|
538
|
+
### Using Transform with Storage Handlers
|
|
539
|
+
|
|
540
|
+
Convert stored values to proper types when reading from storage:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import { useContextStorage, transform } from 'vue-context-storage'
|
|
544
|
+
|
|
545
|
+
const settings = reactive({
|
|
546
|
+
theme: 'light',
|
|
547
|
+
fontSize: 14,
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
useContextStorage('localStorage', settings, {
|
|
551
|
+
key: 'app-settings',
|
|
552
|
+
transform: (deserialized, initial) => ({
|
|
553
|
+
theme: transform.asString(deserialized.theme, { fallback: 'light' }),
|
|
554
|
+
fontSize: transform.asNumber(deserialized.fontSize, { fallback: 14 }),
|
|
555
|
+
}),
|
|
556
|
+
})
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Using Zod Schemas with Storage Handlers
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { z } from 'zod'
|
|
563
|
+
import { useContextStorage } from 'vue-context-storage'
|
|
564
|
+
|
|
565
|
+
const SettingsSchema = z.object({
|
|
566
|
+
theme: z.enum(['light', 'dark']).default('light'),
|
|
567
|
+
fontSize: z.number().int().positive().default(14),
|
|
568
|
+
sidebarOpen: z.boolean().default(true),
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
const settings = reactive(SettingsSchema.parse({}))
|
|
572
|
+
|
|
573
|
+
useContextStorage('localStorage', settings, {
|
|
574
|
+
key: 'app-settings',
|
|
575
|
+
schema: SettingsSchema,
|
|
576
|
+
})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Custom Serialization
|
|
580
|
+
|
|
581
|
+
Provide custom serializer/deserializer functions:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
useContextStorage('localStorage', settings, {
|
|
585
|
+
key: 'app-settings',
|
|
586
|
+
serializer: (data) => btoa(JSON.stringify(data)),
|
|
587
|
+
deserializer: (str) => JSON.parse(atob(str)),
|
|
588
|
+
})
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## Prefix Scoping with `<ContextStoragePrefix>`
|
|
592
|
+
|
|
593
|
+
The `<ContextStoragePrefix>` component adds a prefix to all `useContextStorage` calls within its subtree. Prefixes stack when nested, and are concatenated with bracket notation.
|
|
594
|
+
|
|
595
|
+
### Basic Usage
|
|
596
|
+
|
|
597
|
+
```vue
|
|
598
|
+
<template>
|
|
599
|
+
<ContextStoragePrefix name="table">
|
|
600
|
+
<MyTable />
|
|
601
|
+
</ContextStoragePrefix>
|
|
602
|
+
</template>
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Inside `MyTable`, any `useContextStorage('query', data)` call will automatically get `key: 'tables'`. If the composable also specifies its own key, they are combined:
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
// Inside MyTable — effective key becomes 'table[filters]'
|
|
609
|
+
useContextStorage('query', filters, { key: 'filters' })
|
|
610
|
+
// URL: ?table[filters][search]=...
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Stacking Prefixes
|
|
614
|
+
|
|
615
|
+
Nested `<ContextStoragePrefix>` components stack their prefixes:
|
|
616
|
+
|
|
617
|
+
```vue
|
|
618
|
+
<ContextStoragePrefix name="tables">
|
|
619
|
+
<ContextStoragePrefix name="first">
|
|
620
|
+
<!-- All handlers here get prefix 'tables[first]' -->
|
|
621
|
+
<!-- useContextStorage('query', data) → URL: ?tables[first][search]=... -->
|
|
622
|
+
<!-- useContextStorage('localStorage', data, { key: 'state' }) → key: 'state[tables][first]' -->
|
|
623
|
+
</ContextStoragePrefix>
|
|
624
|
+
</ContextStoragePrefix>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Per-Handler Prefixes
|
|
628
|
+
|
|
629
|
+
Pass an object to apply different prefixes per handler type:
|
|
630
|
+
|
|
631
|
+
```vue
|
|
632
|
+
<ContextStoragePrefix :name="{ query: 'url-tables', localStorage: 'ls-data' }">
|
|
633
|
+
<!-- query handler gets prefix 'url-tables' -->
|
|
634
|
+
<!-- localStorage handler gets prefix 'ls-data' -->
|
|
635
|
+
<!-- sessionStorage handler gets no prefix (not specified) -->
|
|
636
|
+
</ContextStoragePrefix>
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Dynamic Prefix
|
|
640
|
+
|
|
641
|
+
When the `name` prop changes, all descendant components are re-created and re-registered with the new prefix:
|
|
642
|
+
|
|
643
|
+
```vue
|
|
644
|
+
<ContextStoragePrefix :name="activeTab">
|
|
645
|
+
<TabContent />
|
|
646
|
+
</ContextStoragePrefix>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Registering Custom Handlers
|
|
650
|
+
|
|
651
|
+
Register your own handlers at runtime and extend the type map for full type safety:
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
import { defineContextStorageHandler } from 'vue-context-storage'
|
|
655
|
+
import { myHandlerInjectionKey } from './my-handler'
|
|
656
|
+
|
|
657
|
+
// Runtime registration
|
|
658
|
+
defineContextStorageHandler('myHandler', myHandlerInjectionKey)
|
|
659
|
+
|
|
660
|
+
// TypeScript augmentation (e.g. in a .d.ts or at module level)
|
|
661
|
+
declare module 'vue-context-storage' {
|
|
662
|
+
interface ContextStorageHandlerMap {
|
|
663
|
+
myHandler: { key: string }
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Now fully type-checked
|
|
668
|
+
useContextStorage('myHandler', data, { key: 'example' })
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## API Reference
|
|
672
|
+
|
|
673
|
+
### Composables
|
|
674
|
+
|
|
675
|
+
#### `useContextStorage(type, data, options)`
|
|
676
|
+
|
|
677
|
+
Unified composable that delegates to the correct handler based on `type`.
|
|
678
|
+
|
|
679
|
+
**Parameters:**
|
|
680
|
+
|
|
681
|
+
- `type: 'query' | 'localStorage' | 'sessionStorage' | InjectionKey` - Handler type or injection key
|
|
682
|
+
- `data: MaybeRefOrGetter<T>` - Reactive reference to sync
|
|
683
|
+
- `options` - Handler-specific options (type-checked per handler)
|
|
684
|
+
|
|
685
|
+
**Returns:** `{ data, stop, reset, wasChanged }`
|
|
686
|
+
|
|
687
|
+
- `data` - The reactive reference passed in
|
|
688
|
+
- `stop()` - Unregister and stop syncing (called automatically on unmount)
|
|
689
|
+
- `reset()` - Restore data to its initial state
|
|
690
|
+
- `wasChanged: ComputedRef<boolean>` - Whether data differs from initial state
|
|
691
|
+
|
|
692
|
+
**Custom handler registration:**
|
|
693
|
+
|
|
694
|
+
- `defineContextStorageHandler(name, injectionKey)` - Register a custom handler
|
|
695
|
+
- `resolveHandlerInjectionKey(type)` - Look up an injection key by name
|
|
696
|
+
|
|
697
|
+
### Handler Factories
|
|
698
|
+
|
|
699
|
+
#### `createQueryHandler(options?)`
|
|
700
|
+
|
|
701
|
+
Creates a query handler factory for URL query synchronization.
|
|
702
|
+
|
|
703
|
+
**Options:**
|
|
704
|
+
|
|
705
|
+
- `mode?: 'replace' | 'push'` - Router navigation mode (default: `'replace'`)
|
|
706
|
+
- `preserveUnusedKeys?: boolean` - Keep other query params (default: `true`)
|
|
707
|
+
- `preserveEmptyState?: boolean` - Preserve empty state in URL (default: `false`)
|
|
708
|
+
- `emptyPlaceholder?: string` - Placeholder for empty state (default: `'_'`)
|
|
709
|
+
- `onlyChanges?: boolean` - Only write changed values to URL (default: `true`)
|
|
710
|
+
|
|
711
|
+
#### `createLocalStorageHandler(options?)`
|
|
712
|
+
|
|
713
|
+
Creates a localStorage handler factory.
|
|
714
|
+
|
|
715
|
+
**Options:**
|
|
716
|
+
|
|
717
|
+
- `listenToStorageEvents?: boolean` - Enable cross-tab sync (default: `true`)
|
|
718
|
+
|
|
719
|
+
#### `createSessionStorageHandler(options?)`
|
|
720
|
+
|
|
721
|
+
Creates a sessionStorage handler factory.
|
|
722
|
+
|
|
723
|
+
**Options:**
|
|
724
|
+
|
|
725
|
+
- `listenToStorageEvents?: boolean` - Listen to storage events (default: `false`)
|
|
726
|
+
|
|
727
|
+
### Components
|
|
728
|
+
|
|
729
|
+
#### `<ContextStoragePrefix>`
|
|
730
|
+
|
|
731
|
+
Scopes a prefix for all descendant `useContextStorage` calls via provide/inject.
|
|
732
|
+
|
|
733
|
+
**Props:**
|
|
734
|
+
|
|
735
|
+
- `name: string | Partial<Record<string, string>>` (required) - Prefix to apply. A string applies to all handlers; an object applies per handler type (e.g. `{ query: 'q', localStorage: 'ls' }`)
|
|
736
|
+
|
|
737
|
+
Nested `<ContextStoragePrefix>` components stack their prefixes using bracket notation. When `name` changes dynamically, all descendant components are re-created.
|
|
738
|
+
|
|
739
|
+
### Transform Helpers
|
|
740
|
+
|
|
741
|
+
All transform helpers support nullable and missable options:
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
transform.asNumber(value, {
|
|
745
|
+
fallback: 0, // Default value
|
|
746
|
+
nullable: false, // Allow null return
|
|
747
|
+
missable: false, // Allow undefined return
|
|
748
|
+
})
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## Zod Helpers (`vue-context-storage/zod`)
|
|
752
|
+
|
|
753
|
+
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.
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
npm install zod
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### `zObjectArray(itemSchema)`
|
|
760
|
+
|
|
761
|
+
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.
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { z } from 'zod'
|
|
765
|
+
import { zObjectArray } from 'vue-context-storage/zod'
|
|
766
|
+
|
|
767
|
+
const ItemSchema = z.object({
|
|
768
|
+
product: z.string().default(''),
|
|
769
|
+
quantity: z.coerce.number().default(0),
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
const DataSchema = z.object({
|
|
773
|
+
title: z.string().default(''),
|
|
774
|
+
items: zObjectArray(ItemSchema),
|
|
775
|
+
})
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Automatic Type Coercion
|
|
779
|
+
|
|
780
|
+
When a `schema` is provided, the library automatically coerces URL query parameter values to match the expected Zod types before validation. This handles two common URL serialization quirks:
|
|
781
|
+
|
|
782
|
+
#### Array Coercion
|
|
783
|
+
|
|
784
|
+
A URL with multiple values (`?ids=1&ids=2`) produces an array `['1', '2']`, but a single value (`?ids=1`) produces just `'1'`. Without correction, `z.string().array()` would reject the single-value case with `"expected array, received string"`.
|
|
785
|
+
|
|
786
|
+
The library introspects the Zod schema before validation and wraps non-array values into single-element arrays wherever the schema expects `.array()`. This works recursively for nested objects. **No special helpers are needed** — plain `.array()` schemas work out of the box:
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
const Schema = z.object({
|
|
790
|
+
tags: z.string().array().default([]),
|
|
791
|
+
ids: z.coerce.number().array().default([]),
|
|
792
|
+
statuses: z.enum(['active', 'inactive']).array().default([]),
|
|
793
|
+
filters: z
|
|
794
|
+
.object({
|
|
795
|
+
categories: z.string().array().default([]),
|
|
796
|
+
})
|
|
797
|
+
.default({ categories: [] }),
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
// All of these work automatically:
|
|
801
|
+
// ?tags=vue → { tags: ['vue'], ... }
|
|
802
|
+
// ?tags=vue&tags=ts → { tags: ['vue', 'ts'], ... }
|
|
803
|
+
// (no tags param) → { tags: [], ... }
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### Boolean Coercion
|
|
807
|
+
|
|
808
|
+
The query handler serializes booleans as `'1'`/`'0'` strings in URL parameters (e.g. `?active=1`). Standard `z.coerce.boolean()` cannot be used because `Boolean('0')` is `true` in JavaScript. The library automatically converts `'1'` → `true` and `'0'` → `false` when the schema expects a boolean field. **No special helpers are needed** — plain `z.boolean()` works out of the box:
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
const Schema = z.object({
|
|
812
|
+
active: z.boolean().default(false),
|
|
813
|
+
enabled: z.boolean().default(true),
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
// ?active=1 → { active: true, enabled: true }
|
|
817
|
+
// ?active=0 → { active: false, enabled: true }
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### `createSchemaObject(schema, options?)`
|
|
821
|
+
|
|
822
|
+
Creates a plain object with empty/default values based on a Zod schema. Useful for initializing reactive data from a schema definition.
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
import { z } from 'zod'
|
|
826
|
+
import { createSchemaObject } from 'vue-context-storage/zod'
|
|
827
|
+
|
|
828
|
+
const FiltersSchema = z.object({
|
|
829
|
+
search: z.string().default(''),
|
|
830
|
+
page: z.coerce.number().default(1),
|
|
831
|
+
active: z.boolean().default(false),
|
|
832
|
+
score: z.number().nullable(),
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
const filters = reactive(createSchemaObject(FiltersSchema))
|
|
836
|
+
// Result: { search: '', page: 1, active: false, score: null }
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Options:**
|
|
840
|
+
|
|
841
|
+
- `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.).
|
|
842
|
+
- `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.
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
import { createSchemaObject, SCHEMA_SYMBOL } from 'vue-context-storage/zod'
|
|
846
|
+
|
|
847
|
+
const data = createSchemaObject(FiltersSchema, { withSchema: true })
|
|
848
|
+
data[SCHEMA_SYMBOL] // → FiltersSchema
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**Type-based defaults** (when `useDefaults: false` or no `.default()` is set):
|
|
852
|
+
|
|
853
|
+
| Zod type | Default value |
|
|
854
|
+
| ------------- | -------------------------------------------- |
|
|
855
|
+
| `z.string()` | `''` |
|
|
856
|
+
| `z.number()` | `0` (respects `.min()` / `.positive()`) |
|
|
857
|
+
| `z.boolean()` | `false` |
|
|
858
|
+
| `z.array()` | `[]` |
|
|
859
|
+
| `z.object()` | Recursively created via `createSchemaObject` |
|
|
860
|
+
| `z.date()` | `null` |
|
|
861
|
+
| `.nullable()` | `null` |
|
|
862
|
+
| `.optional()` | `undefined` |
|
|
863
|
+
|
|
864
|
+
## TypeScript Support
|
|
865
|
+
|
|
866
|
+
Full TypeScript support with type inference:
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import type {
|
|
870
|
+
ContextStorageHandler,
|
|
871
|
+
ContextStorageHandlerFactory,
|
|
872
|
+
QueryValue,
|
|
873
|
+
} from 'vue-context-storage'
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
When using Zod schemas, TypeScript will automatically infer types:
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
const FiltersSchema = z.object({
|
|
880
|
+
search: z.string().default(''),
|
|
881
|
+
page: z.coerce.number().default(1),
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
type Filters = z.infer<typeof FiltersSchema>
|
|
885
|
+
// Result: { search: string; page: number }
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
## Examples
|
|
889
|
+
|
|
890
|
+
### Pagination with URL Sync
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
import { ref } from 'vue'
|
|
894
|
+
import { useContextStorage, transform } from 'vue-context-storage'
|
|
895
|
+
|
|
896
|
+
const pagination = ref({
|
|
897
|
+
page: 1,
|
|
898
|
+
perPage: 25,
|
|
899
|
+
total: 0,
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
useContextStorage('query', pagination, {
|
|
903
|
+
key: 'page',
|
|
904
|
+
transform: (data, initial) => ({
|
|
905
|
+
page: transform.asNumber(data.page, { fallback: 1 }),
|
|
906
|
+
perPage: transform.asNumber(data.perPage, { fallback: 25 }),
|
|
907
|
+
total: initial.total, // Don't sync total from URL
|
|
908
|
+
}),
|
|
909
|
+
})
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
## Peer Dependencies
|
|
913
|
+
|
|
914
|
+
- `vue`: ^3.0.0
|
|
915
|
+
- `vue-router`: ^4.0.0 || ^5.0.0
|
|
916
|
+
- `zod`: ^4.0.0 (optional - only if using schema validation)
|
|
917
|
+
|
|
918
|
+
## License
|
|
919
|
+
|
|
920
|
+
MIT
|
|
921
|
+
|
|
922
|
+
## Development
|
|
923
|
+
|
|
924
|
+
### Running Playground Locally
|
|
925
|
+
|
|
926
|
+
```bash
|
|
927
|
+
# Development mode (hot reload)
|
|
928
|
+
npm run play
|
|
929
|
+
|
|
930
|
+
# Production preview
|
|
931
|
+
npm run build:playground
|
|
932
|
+
npm run preview:playground
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Building
|
|
936
|
+
|
|
937
|
+
```bash
|
|
938
|
+
# Build library
|
|
939
|
+
npm run build
|
|
940
|
+
|
|
941
|
+
# Build playground for deployment
|
|
942
|
+
npm run build:playground
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Testing & Quality
|
|
946
|
+
|
|
947
|
+
```bash
|
|
948
|
+
# Run all checks
|
|
949
|
+
npm run check
|
|
950
|
+
|
|
951
|
+
# Type checking
|
|
952
|
+
npm run ts:check
|
|
953
|
+
|
|
954
|
+
# Linting
|
|
955
|
+
npm run lint
|
|
956
|
+
|
|
957
|
+
# Formatting
|
|
958
|
+
npm run format
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
## Contributing
|
|
962
|
+
|
|
963
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|