use-prms 0.2.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/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # use-prms
2
+
3
+ Type-safe URL query parameter management with minimal, human-readable encoding.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Type-safe**: Full TypeScript support with generic `Param<T>` interface
8
+ - 📦 **Tiny URLs**: Smart encoding - omit defaults, use short keys, `+` for spaces
9
+ - ⚛️ **React hooks**: `useUrlParam()` and `useUrlParams()` for seamless integration
10
+ - 🔧 **Framework-agnostic**: Core utilities work anywhere, React hooks are optional
11
+ - 🌳 **Tree-shakeable**: ESM + CJS builds with TypeScript declarations
12
+ - 0️⃣ **Zero dependencies**: Except React (peer dependency, optional)
13
+ - 🔁 **Multi-value params**: Support for repeated keys like `?tag=a&tag=b`
14
+ - #️⃣ **Hash params**: Use hash fragment (`#key=value`) instead of query string
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install use-prms
20
+ # or
21
+ pnpm add use-prms
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { useUrlParam, boolParam, stringParam, intParam } from 'use-prms'
28
+
29
+ function MyComponent() {
30
+ const [zoom, setZoom] = useUrlParam('z', boolParam)
31
+ const [device, setDevice] = useUrlParam('d', stringParam())
32
+ const [count, setCount] = useUrlParam('n', intParam(10))
33
+
34
+ // URL: ?z&d=gym&n=5
35
+ // zoom = true, device = "gym", count = 5
36
+
37
+ return (
38
+ <div>
39
+ <button onClick={() => setZoom(!zoom)}>Toggle Zoom</button>
40
+ <input value={device ?? ''} onChange={e => setDevice(e.target.value)} />
41
+ <button onClick={() => setCount(count + 1)}>Count: {count}</button>
42
+ </div>
43
+ )
44
+ }
45
+ ```
46
+
47
+ ## Built-in Param Types
48
+
49
+ ### Boolean
50
+ ```typescript
51
+ const [enabled, setEnabled] = useUrlParam('e', boolParam)
52
+ // ?e → true
53
+ // (absent) → false
54
+ ```
55
+
56
+ ### Strings
57
+ ```typescript
58
+ const [name, setName] = useUrlParam('n', stringParam()) // optional
59
+ const [mode, setMode] = useUrlParam('m', defStringParam('auto')) // with default
60
+ // ?n=foo → "foo"
61
+ // (absent) → undefined / "auto"
62
+ ```
63
+
64
+ ### Numbers
65
+ ```typescript
66
+ const [count, setCount] = useUrlParam('c', intParam(0))
67
+ const [ratio, setRatio] = useUrlParam('r', floatParam(1.0))
68
+ const [id, setId] = useUrlParam('id', optIntParam) // number | null
69
+ // ?c=5&r=1.5&id=123 → 5, 1.5, 123
70
+ // (absent) → 0, 1.0, null
71
+ ```
72
+
73
+ ### Enums
74
+ ```typescript
75
+ const [theme, setTheme] = useUrlParam(
76
+ 't',
77
+ enumParam('light', ['light', 'dark', 'auto'] as const)
78
+ )
79
+ // ?t=dark → "dark"
80
+ // ?t=invalid → "light" (warns in console)
81
+ ```
82
+
83
+ ### Arrays (delimiter-separated)
84
+ ```typescript
85
+ const [tags, setTags] = useUrlParam('tags', stringsParam([], ','))
86
+ const [ids, setIds] = useUrlParam('ids', numberArrayParam([]))
87
+ // ?tags=foo,bar,baz → ["foo", "bar", "baz"]
88
+ // ?ids=1,2,3 → [1, 2, 3]
89
+ ```
90
+
91
+ ### Multi-value Arrays (repeated keys)
92
+ ```typescript
93
+ import { useMultiUrlParam, multiStringParam, multiIntParam } from 'use-prms'
94
+
95
+ const [tags, setTags] = useMultiUrlParam('tag', multiStringParam())
96
+ // ?tag=foo&tag=bar&tag=baz → ["foo", "bar", "baz"]
97
+
98
+ const [ids, setIds] = useMultiUrlParam('id', multiIntParam())
99
+ // ?id=1&id=2&id=3 → [1, 2, 3]
100
+
101
+ // Also available: multiFloatParam()
102
+ ```
103
+
104
+ ### Compact Code Mapping
105
+ ```typescript
106
+ // Single value with short codes
107
+ const [metric, setMetric] = useUrlParam('y', codeParam('Rides', {
108
+ Rides: 'r',
109
+ Minutes: 'm',
110
+ }))
111
+ // ?y=m → "Minutes", omitted for default "Rides"
112
+
113
+ // Multi-value with short codes (omits when all selected)
114
+ const [regions, setRegions] = useUrlParam('r', codesParam(
115
+ ['NYC', 'JC', 'HOB'],
116
+ { NYC: 'n', JC: 'j', HOB: 'h' }
117
+ ))
118
+ // ?r=nj → ["NYC", "JC"], omitted when all three selected
119
+ ```
120
+
121
+ ### Pagination
122
+ ```typescript
123
+ const [page, setPage] = useUrlParam('p', paginationParam(20))
124
+ // Encodes offset + pageSize compactly using + as delimiter:
125
+ // { offset: 0, pageSize: 20 } → (omitted)
126
+ // { offset: 0, pageSize: 50 } → ?p=+50
127
+ // { offset: 100, pageSize: 20 } → ?p=100
128
+ // { offset: 100, pageSize: 50 } → ?p=100+50
129
+ ```
130
+
131
+ ## Custom Params
132
+
133
+ Create your own param encoders/decoders:
134
+
135
+ ```typescript
136
+ import type { Param } from 'use-prms'
137
+
138
+ // Example: Compact date encoding (YYMMDD)
139
+ const dateParam: Param<Date> = {
140
+ encode: (date) => {
141
+ const yy = String(date.getFullYear()).slice(-2)
142
+ const mm = String(date.getMonth() + 1).padStart(2, '0')
143
+ const dd = String(date.getDate()).padStart(2, '0')
144
+ return `${yy}${mm}${dd}`
145
+ },
146
+ decode: (str) => {
147
+ if (!str || str.length !== 6) return new Date()
148
+ const yy = parseInt('20' + str.slice(0, 2), 10)
149
+ const mm = parseInt(str.slice(2, 4), 10) - 1
150
+ const dd = parseInt(str.slice(4, 6), 10)
151
+ return new Date(yy, mm, dd)
152
+ }
153
+ }
154
+
155
+ const [date, setDate] = useUrlParam('d', dateParam)
156
+ // ?d=251123 → Date(2025, 10, 23)
157
+ ```
158
+
159
+ ## Batch Updates
160
+
161
+ Use `useUrlParams()` to update multiple parameters atomically:
162
+
163
+ ```typescript
164
+ import { useUrlParams, intParam, boolParam } from 'use-prms'
165
+
166
+ const { values, setValues } = useUrlParams({
167
+ page: intParam(1),
168
+ size: intParam(20),
169
+ grid: boolParam
170
+ })
171
+
172
+ // Update multiple params at once (single history entry)
173
+ setValues({ page: 2, size: 50 })
174
+ ```
175
+
176
+ ## URL Encoding
177
+
178
+ - **Spaces**: Encoded as `+` (standard form-urlencoded)
179
+ - **Defaults**: Omitted from URL (keeps URLs minimal)
180
+ - **Booleans**: Present = true (`?z`), absent = false
181
+ - **Empty values**: Valueless params (`?key` without `=`)
182
+
183
+ Example:
184
+ ```typescript
185
+ const [devices, setDevices] = useUrlParam('d', stringsParam([], ' '))
186
+ setDevices(['gym', 'bedroom'])
187
+ // URL: ?d=gym+bedroom
188
+ ```
189
+
190
+ ## Framework-Agnostic Core
191
+
192
+ Use the core utilities without React:
193
+
194
+ ```typescript
195
+ import { boolParam, serializeParams, parseParams } from 'use-prms'
196
+
197
+ // Encode
198
+ const params = { z: boolParam.encode(true), d: 'gym' }
199
+ const search = serializeParams(params) // "z&d=gym"
200
+
201
+ // Decode
202
+ const parsed = parseParams(window.location.search)
203
+ const zoom = boolParam.decode(parsed.z) // true
204
+ ```
205
+
206
+ ## Hash Params
207
+
208
+ Use hash fragment (`#key=value`) instead of query string (`?key=value`):
209
+
210
+ ```typescript
211
+ // Just change the import path
212
+ import { useUrlParam, boolParam } from 'use-prms/hash'
213
+
214
+ const [zoom, setZoom] = useUrlParam('z', boolParam)
215
+ // URL: https://example.com/#z (instead of ?z)
216
+ ```
217
+
218
+ Same API, different URL location. Useful when query strings conflict with server routing or you want params to survive page reloads without server involvement.
219
+
220
+ ## API Reference
221
+
222
+ ### `useUrlParam<T>(key: string, param: Param<T>, push?: boolean)`
223
+
224
+ React hook for managing a single URL parameter.
225
+
226
+ - `key`: Query parameter key
227
+ - `param`: Param encoder/decoder
228
+ - `push`: Use pushState (true) or replaceState (false, default)
229
+ - Returns: `[value: T, setValue: (value: T) => void]`
230
+
231
+ ### `useUrlParams<P>(params: P, push?: boolean)`
232
+
233
+ React hook for managing multiple URL parameters together.
234
+
235
+ - `params`: Object mapping keys to Param types
236
+ - `push`: Use pushState (true) or replaceState (false, default)
237
+ - Returns: `{ values, setValues }`
238
+
239
+ ### `useMultiUrlParam<T>(key: string, param: MultiParam<T>, push?: boolean)`
240
+
241
+ React hook for managing a multi-value URL parameter (repeated keys).
242
+
243
+ - `key`: Query parameter key
244
+ - `param`: MultiParam encoder/decoder
245
+ - `push`: Use pushState (true) or replaceState (false, default)
246
+ - Returns: `[value: T, setValue: (value: T) => void]`
247
+
248
+ ### `Param<T>`
249
+
250
+ Bidirectional encoder/decoder interface:
251
+
252
+ ```typescript
253
+ type Param<T> = {
254
+ encode: (value: T) => string | undefined
255
+ decode: (encoded: string | undefined) => T
256
+ }
257
+ ```
258
+
259
+ ### `MultiParam<T>`
260
+
261
+ Multi-value encoder/decoder interface:
262
+
263
+ ```typescript
264
+ type MultiParam<T> = {
265
+ encode: (value: T) => string[]
266
+ decode: (encoded: string[]) => T
267
+ }
268
+ ```
269
+
270
+ ### Built-in Param Types
271
+
272
+ | Param | Type | Description |
273
+ |-------|------|-------------|
274
+ | `boolParam` | `Param<boolean>` | `?key` = true, absent = false |
275
+ | `stringParam(init?)` | `Param<string \| undefined>` | Optional string |
276
+ | `defStringParam(init)` | `Param<string>` | Required string with default |
277
+ | `intParam(init)` | `Param<number>` | Integer with default |
278
+ | `floatParam(init)` | `Param<number>` | Float with default |
279
+ | `optIntParam` | `Param<number \| null>` | Optional integer |
280
+ | `enumParam(init, values)` | `Param<T>` | Validated enum |
281
+ | `stringsParam(init?, delim?)` | `Param<string[]>` | Delimiter-separated strings |
282
+ | `numberArrayParam(init?)` | `Param<number[]>` | Comma-separated numbers |
283
+ | `codeParam(init, codeMap)` | `Param<T>` | Enum with short URL codes |
284
+ | `codesParam(allValues, codeMap, sep?)` | `Param<T[]>` | Multi-value with short codes |
285
+ | `paginationParam(defaultSize, validSizes?)` | `Param<Pagination>` | Offset + page size |
286
+
287
+ ### Built-in MultiParam Types
288
+
289
+ | Param | Type | Description |
290
+ |-------|------|-------------|
291
+ | `multiStringParam(init?)` | `MultiParam<string[]>` | Repeated string params |
292
+ | `multiIntParam(init?)` | `MultiParam<number[]>` | Repeated integer params |
293
+ | `multiFloatParam(init?)` | `MultiParam<number[]>` | Repeated float params |
294
+
295
+ ### Core Utilities
296
+
297
+ - `serializeParams(params)`: Convert params object to URL query string *(deprecated, use `serializeMultiParams`)*
298
+ - `parseParams(source)`: Parse URL string or URLSearchParams to object *(deprecated, use `parseMultiParams`)*
299
+ - `serializeMultiParams(params)`: Convert multi-value params to URL query string
300
+ - `parseMultiParams(source)`: Parse URL to multi-value params object
301
+ - `getCurrentParams()`: Get current URL params (browser only)
302
+ - `updateUrl(params, push?)`: Update URL without reloading (browser only)
303
+
304
+ ## Examples
305
+
306
+ Projects using `use-prms`:
307
+
308
+ - [runsascoded/awair] – Air quality dashboard with URL-persisted chart settings
309
+
310
+ Example: [`awair.runsascoded.com/?d=+br&y=thZ&t=-3d`][awair-example]
311
+ - `d=+br`: show default device + "bedroom" (`+` encodes space, leading space means "include default")
312
+ - `y=thZ`: left axis = temp (`t`), right axis = humidity (`h`), Y-axes don't start from zero (`Z`)
313
+ - `t=-3d`: time range = last 3 days
314
+
315
+ [runsascoded/awair]: https://github.com/runsascoded/awair
316
+ [awair-example]: https://awair.runsascoded.com/?d=+br&y=thZ&t=-3d
317
+
318
+ ## License
319
+
320
+ MIT