vuerl 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marius Oseth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # vuerl
2
+
3
+ [![npm version](https://img.shields.io/npm/v/vuerl.svg)](https://www.npmjs.com/package/vuerl)
4
+ [![CI](https://github.com/mariusoseth/vuerls/actions/workflows/ci.yml/badge.svg)](https://github.com/mariusoseth/vuerls/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > Type-safe URL query state management for Vue 3 + Vue Router.
8
+
9
+ This package gives you typed composables that keep Vue state and the URL query string in sync. You get the same ergonomics as `const [value, setValue] = useQueryState(...)` in React/nuqs, including parser-based type safety, automatic batching, and browser-aware rate limiting.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add vuerl
15
+ # or
16
+ npm install vuerl
17
+ # or
18
+ yarn add vuerl
19
+ ```
20
+
21
+ Peer dependencies:
22
+
23
+ ```bash
24
+ pnpm add vue@^3.3 vue-router@^4.0
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```ts
30
+ <script setup lang="ts">
31
+ import { useQueryState, parseAsString, parseAsInteger } from 'vuerl'
32
+
33
+ const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
34
+ const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1), {
35
+ debounce: 200,
36
+ history: 'replace'
37
+ })
38
+ </script>
39
+
40
+ <template>
41
+ <input v-model="search" placeholder="Search" />
42
+ <button @click="setPage(page.value + 1)">Next Page</button>
43
+ </template>
44
+ ```
45
+
46
+ - State updates immediately when you type.
47
+ - URL updates are debounced to keep browsers happy.
48
+ - Defaults never show up in the URL (unless you turn off `clearOnDefault`).
49
+ - TypeScript knows `search.value` is `string` and `page.value` is `number`.
50
+
51
+ ## Multi-parameter State
52
+
53
+ Use `useQueryStates` when several params should update together.
54
+
55
+ ```ts
56
+ import {
57
+ useQueryStates,
58
+ parseAsString,
59
+ parseAsInteger,
60
+ parseAsStringLiteral
61
+ } from 'vuerl'
62
+
63
+ const [filters, setFilters] = useQueryStates({
64
+ search: parseAsString.withDefault(''),
65
+ status: parseAsStringLiteral(['active', 'inactive']).withDefault('active'),
66
+ limit: parseAsInteger.withDefault(20)
67
+ }, {
68
+ debounce: 120,
69
+ history: 'push'
70
+ })
71
+
72
+ setFilters({ search: 'vue' }) // single field
73
+ setFilters({ status: 'inactive', limit: 50 }) // batched update → one router push
74
+ setFilters({ status: null }) // drop from URL + reset to parser default
75
+ setFilters(null) // reset every field to defaults
76
+ ```
77
+
78
+ Updates inside the same tick (`setFilters({...}); setFilters({...})`) merge before touching the router, so you only pay for one navigation.
79
+
80
+ ## Parser Cheatsheet
81
+
82
+ Parsers define how a query string turns into a typed value (and back). Every parser supports `.withDefault(value)` and `.withOptions({ ...queryStateOptions })`.
83
+
84
+ | Parser | Example | URL → Value |
85
+ | --- | --- | --- |
86
+ | `parseAsString` | `"hello"` | `'hello'`
87
+ | `parseAsInteger` | `"42"` | `42`
88
+ | `parseAsFloat` | `"3.14"` | `3.14`
89
+ | `parseAsBoolean` | `"true"` | `true`
90
+ | `parseAsStringLiteral(['asc','desc'])` | `"asc"` | `'asc'`
91
+ | `parseAsStringEnum(MyEnum)` | `"VALUE"` | `MyEnum.VALUE`
92
+ | `parseAsArrayOf(parseAsInteger)` | `"1,2,3"` | `[1,2,3]`
93
+ | `parseAsNativeArrayOf(parseAsString)` | `?tag=a&tag=b` | `['a','b']`
94
+ | `parseAsIsoDate` | `"2025-11-22"` | `Date`
95
+ | `parseAsIsoDateTime` | `"2025-11-22T10:30:00Z"` | `Date`
96
+ | `parseAsJson()` | `"%7B%5C"id%5C":1%7D"` | `{ id: 1 }`
97
+ | `parseAsHex` | `"ff00ff"` | `'ff00ff'`
98
+ | `withDefault(customParser, defaultValue)` | – | Keeps custom parser logic but adds defaults |
99
+
100
+ Need something custom? Implement the `Parser<T>` interface or wrap an existing parser with `.withDefault`/`.withOptions`.
101
+
102
+ ```ts
103
+ const parseAsSlug = withDefault({
104
+ parse: (value) => (value && /[a-z0-9-]+/.test(value) ? value : null),
105
+ serialize: (value) => value ?? null
106
+ }, 'home')
107
+ ```
108
+
109
+ ## Options Reference
110
+
111
+ Both hooks accept the same `QueryStateOptions` either directly or via parser `.withOptions`.
112
+
113
+ | Option | Default | What it does |
114
+ | --- | --- | --- |
115
+ | `history` | `'replace'` | `'push'` to record every change in browser history. |
116
+ | `debounce` | browser-safe (50ms / 120ms Safari) | Delay before writing to the URL. |
117
+ | `throttle` | `null` | Alternate rate limiter if you prefer throttling. |
118
+ | `clearOnDefault` | `true` | Drop params from the URL when they match parser default. |
119
+ | `shallow` | `true` | Skip full router navigation (like `router.replace({ query })`). Set `false` when SSR needs to know about query changes. |
120
+ | `scroll` | `false` | Force scroll to top on navigation. |
121
+
122
+ Parser-level options merge with hook options, so you can keep most defaults global and override only the odd field:
123
+
124
+ ```ts
125
+ const parser = parseAsInteger
126
+ .withDefault(20)
127
+ .withOptions({ clearOnDefault: false })
128
+
129
+ const [, setLimit] = useQueryState('limit', parser)
130
+ ```
131
+
132
+ ## Watching External Changes
133
+
134
+ Both hooks watch `route.query` so back/forward navigation, shared URLs, or manual `router.push` calls stay in sync automatically. Pending debounced updates are cancelled when the user navigates elsewhere.
135
+
136
+ ## Working With Arrays
137
+
138
+ `parseAsArrayOf` (comma separated) and `parseAsNativeArrayOf` (repeated params) always return concrete arrays:
139
+
140
+ ```ts
141
+ const [tags] = useQueryState('tags', parseAsArrayOf(parseAsString))
142
+ tags.value // always string[]
143
+
144
+ const [, setIds] = useQueryState('id', parseAsNativeArrayOf(parseAsInteger))
145
+ setIds([1, 2, 3]) // → ?id=1&id=2&id=3
146
+ ```
147
+
148
+ ## Custom Equality
149
+
150
+ If your parser returns objects/arrays that shouldn't trigger updates when reference changes, provide `eq(a, b)`:
151
+
152
+ ```ts
153
+ const parseAsJsonFilters = withDefault({
154
+ parse: (value) => value ? JSON.parse(value) : null,
155
+ serialize: (value) => value ? JSON.stringify(value) : null,
156
+ eq: (a, b) => JSON.stringify(a) === JSON.stringify(b)
157
+ }, {})
158
+ ```
159
+
160
+ ## Testing Helpers
161
+
162
+ The test suite uses Vue Test Utils + Vue Router in memory. If you need to write your own tests, copy the helper from `tests/helpers.ts` to mount a composable inside a dummy component.
163
+
164
+ ## FAQ
165
+
166
+ **Does this work with SSR?**
167
+
168
+ Yes. Hooks guard against `window` access and fall back to safe defaults in SSR environments. Set `{ shallow: false }` if your framework needs full navigations for query changes.
169
+
170
+ **What about browser rate limits?**
171
+
172
+ We auto-detect Safari vs other browsers and ensure the debounce/throttle never drops below 120ms/50ms respectively. You can still pass your own values—they're clamped to the safe floor.
173
+
174
+ **Can I mix `useQueryState` and `useQueryStates`?**
175
+
176
+ Absolutely. They both watch the same router instance and will stay in sync. When both write the same key the last writer wins, so prefer one hook per param to avoid confusion.
177
+
178
+ ## Contributing
179
+
180
+ PRs welcome! If you add new parsers, remember to extend `src/parsers.ts`, export from `src/index.ts`, and cover them in `tests/parsers.test.ts`.
181
+
182
+ ## License
183
+
184
+ MIT