juststore 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +358 -0
- package/dist/form.d.ts +35 -3
- package/dist/form.js +58 -0
- package/dist/impl.d.ts +69 -6
- package/dist/impl.js +107 -18
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory.d.ts +35 -3
- package/dist/memory.js +35 -3
- package/dist/mixed_state.d.ts +4 -0
- package/dist/node.d.ts +31 -1
- package/dist/node.js +25 -1
- package/dist/root.d.ts +16 -0
- package/dist/root.js +12 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.js +23 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# juststore
|
|
2
|
+
|
|
3
|
+
A small, expressive, and type-safe state management library for React.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dot-path addressing** - Access nested values using paths like `store.user.profile.name`
|
|
8
|
+
- **Type-safe paths** - Full TypeScript inference for nested property access
|
|
9
|
+
- **Fine-grained subscriptions** - Components only re-render when their specific data changes
|
|
10
|
+
- **localStorage persistence** - Automatic persistence with cross-tab synchronization via BroadcastChannel
|
|
11
|
+
- **Memory-only stores** - Component-scoped state that doesn't persist
|
|
12
|
+
- **Form handling** - Built-in validation and error management
|
|
13
|
+
- **Array operations** - Native array methods (push, pop, splice, etc.) on array paths
|
|
14
|
+
- **Derived state** - Transform values bidirectionally without extra storage
|
|
15
|
+
- **SSR compatible** - Safe to use in server-side rendering environments
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install juststore
|
|
21
|
+
# or
|
|
22
|
+
bun add juststore
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { createStore } from 'juststore'
|
|
29
|
+
|
|
30
|
+
type AppState = {
|
|
31
|
+
user: {
|
|
32
|
+
name: string
|
|
33
|
+
preferences: {
|
|
34
|
+
theme: 'light' | 'dark'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
todos: { id: number; text: string; done: boolean }[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const store = createStore<AppState>('app', {
|
|
41
|
+
user: {
|
|
42
|
+
name: 'Guest',
|
|
43
|
+
preferences: { theme: 'light' }
|
|
44
|
+
},
|
|
45
|
+
todos: []
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Reading State
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
function UserName() {
|
|
55
|
+
// Subscribe to a specific path - re-renders only when this value changes
|
|
56
|
+
const name = store.user.name.use()
|
|
57
|
+
return <span>{name}</span>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Theme() {
|
|
61
|
+
// Deep path access
|
|
62
|
+
const theme = store.user.preferences.theme.use()
|
|
63
|
+
return <span>Current theme: {theme}</span>
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Writing State
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
function Settings() {
|
|
71
|
+
return <button onClick={() => store.user.preferences.theme.set('dark')}>Dark Mode</button>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Functional updates
|
|
75
|
+
store.user.name.set(prev => prev.toUpperCase())
|
|
76
|
+
|
|
77
|
+
// Read without subscribing
|
|
78
|
+
const currentName = store.user.name.value
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### useState-style Hook
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
function EditableName() {
|
|
85
|
+
const [name, setName] = store.user.name.useState()
|
|
86
|
+
return <input value={name ?? ''} onChange={e => setName(e.target.value)} />
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Debounced Values
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
function SearchResults() {
|
|
94
|
+
// Value updates are debounced by 300ms
|
|
95
|
+
const query = store.search.query.useDebounce(300)
|
|
96
|
+
// fetch results based on debounced query...
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Array Operations
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
function TodoList() {
|
|
104
|
+
const todos = store.todos.use()
|
|
105
|
+
|
|
106
|
+
const addTodo = () => {
|
|
107
|
+
store.todos.push({ id: Date.now(), text: 'New todo', done: false })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const removeFirst = () => {
|
|
111
|
+
store.todos.shift()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const toggleTodo = (index: number) => {
|
|
115
|
+
store.todos.at(index).done.set(prev => !prev)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<ul>
|
|
120
|
+
{todos?.map((todo, i) => (
|
|
121
|
+
<li key={todo.id} onClick={() => toggleTodo(i)}>
|
|
122
|
+
{todo.text}
|
|
123
|
+
</li>
|
|
124
|
+
))}
|
|
125
|
+
</ul>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Available array methods: `push`, `pop`, `shift`, `unshift`, `splice`, `reverse`, `sort`, `fill`, `copyWithin`, `sortedInsert`.
|
|
131
|
+
|
|
132
|
+
### Render Props
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
function Counter() {
|
|
136
|
+
return (
|
|
137
|
+
<store.counter.Render>
|
|
138
|
+
{(value, update) => (
|
|
139
|
+
<button onClick={() => update((value ?? 0) + 1)}>Count: {value ?? 0}</button>
|
|
140
|
+
)}
|
|
141
|
+
</store.counter.Render>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Conditional Rendering
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
function AdminPanel() {
|
|
150
|
+
return (
|
|
151
|
+
<store.user.role.Show on={role => role === 'admin'}>
|
|
152
|
+
<AdminDashboard />
|
|
153
|
+
</store.user.role.Show>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Derived State
|
|
159
|
+
|
|
160
|
+
Transform values without storing the transformed version:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
function TemperatureInput() {
|
|
164
|
+
// Store holds Celsius, but we want to display/edit Fahrenheit
|
|
165
|
+
const fahrenheit = store.temperature.derived({
|
|
166
|
+
from: celsius => ((celsius ?? 0) * 9) / 5 + 32,
|
|
167
|
+
to: fahrenheit => ((fahrenheit - 32) * 5) / 9
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const [temp, setTemp] = fahrenheit.useState()
|
|
171
|
+
return <input type="number" value={temp} onChange={e => setTemp(Number(e.target.value))} />
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Computed Values
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
function TotalPrice() {
|
|
179
|
+
const total = store.cart.items.compute(
|
|
180
|
+
items => items?.reduce((sum, item) => sum + item.price * item.qty, 0) ?? 0
|
|
181
|
+
)
|
|
182
|
+
return <span>Total: ${total}</span>
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Memory-Only Stores
|
|
187
|
+
|
|
188
|
+
For complex component-local state with nested structures. Useful when you need to pass state to child components without prop drilling:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { useMemoryStore, type MemoryStore } from 'juststore'
|
|
192
|
+
|
|
193
|
+
type SearchState = {
|
|
194
|
+
query: string
|
|
195
|
+
filters: { category: string; minPrice: number }
|
|
196
|
+
results: { id: number; name: string }[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ProductSearch() {
|
|
200
|
+
const state = useMemoryStore<SearchState>({
|
|
201
|
+
query: '',
|
|
202
|
+
filters: { category: 'all', minPrice: 0 },
|
|
203
|
+
results: []
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<SearchInput state={state} />
|
|
209
|
+
<FilterPanel state={state} />
|
|
210
|
+
<ResultsList state={state} />
|
|
211
|
+
</>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function SearchInput({ state }: { state: MemoryStore<SearchState> }) {
|
|
216
|
+
const query = state.query.use()
|
|
217
|
+
return <input value={query} onChange={e => state.query.set(e.target.value)} />
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function FilterPanel({ state }: { state: MemoryStore<SearchState> }) {
|
|
221
|
+
const category = state.filters.category.use()
|
|
222
|
+
return (
|
|
223
|
+
<select value={category} onChange={e => state.filters.category.set(e.target.value)}>
|
|
224
|
+
<option value="all">All</option>
|
|
225
|
+
<option value="electronics">Electronics</option>
|
|
226
|
+
</select>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function ResultsList({ state }: { state: MemoryStore<SearchState> }) {
|
|
231
|
+
const results = state.results.use()
|
|
232
|
+
return (
|
|
233
|
+
<ul>
|
|
234
|
+
{results?.map(r => (
|
|
235
|
+
<li key={r.id}>{r.name}</li>
|
|
236
|
+
))}
|
|
237
|
+
</ul>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Form Handling
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useForm } from 'juststore'
|
|
246
|
+
|
|
247
|
+
type LoginForm = {
|
|
248
|
+
email: string
|
|
249
|
+
password: string
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function LoginPage() {
|
|
253
|
+
const form = useForm<LoginForm>(
|
|
254
|
+
{ email: '', password: '' },
|
|
255
|
+
{
|
|
256
|
+
email: { validate: 'not-empty' },
|
|
257
|
+
password: {
|
|
258
|
+
validate: value => (value && value.length < 8 ? 'Password too short' : undefined)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<form onSubmit={form.handleSubmit(values => console.log(values))}>
|
|
265
|
+
<input value={form.email.use() ?? ''} onChange={e => form.email.set(e.target.value)} />
|
|
266
|
+
{form.email.useError() && <span>{form.email.error}</span>}
|
|
267
|
+
|
|
268
|
+
<input
|
|
269
|
+
type="password"
|
|
270
|
+
value={form.password.use() ?? ''}
|
|
271
|
+
onChange={e => form.password.set(e.target.value)}
|
|
272
|
+
/>
|
|
273
|
+
{form.password.useError() && <span>{form.password.error}</span>}
|
|
274
|
+
|
|
275
|
+
<button type="submit">Login</button>
|
|
276
|
+
</form>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Validation options:
|
|
282
|
+
|
|
283
|
+
- `'not-empty'` - Field must have a value
|
|
284
|
+
- `RegExp` - Value must match the pattern
|
|
285
|
+
- `(value, form) => string | undefined` - Custom validation function
|
|
286
|
+
|
|
287
|
+
### Mixed State
|
|
288
|
+
|
|
289
|
+
Combine multiple state values into a single subscription:
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import { createMixedState } from 'juststore'
|
|
293
|
+
|
|
294
|
+
function LoadingOverlay() {
|
|
295
|
+
const loading = createMixedState(store.saving, store.fetching, store.uploading)
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<loading.Show on={([saving, fetching, uploading]) => saving || fetching || uploading}>
|
|
299
|
+
<Spinner />
|
|
300
|
+
</loading.Show>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Path-based API
|
|
306
|
+
|
|
307
|
+
The store also exposes a path-based API for dynamic access:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
// Equivalent to store.user.name.use()
|
|
311
|
+
const name = store.use('user.name')
|
|
312
|
+
|
|
313
|
+
// Equivalent to store.user.name.set('Alice')
|
|
314
|
+
store.set('user.name', 'Alice')
|
|
315
|
+
|
|
316
|
+
// Equivalent to store.user.name.value
|
|
317
|
+
const current = store.value('user.name')
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## API Reference
|
|
321
|
+
|
|
322
|
+
### createStore(namespace, defaultValue, options?)
|
|
323
|
+
|
|
324
|
+
Creates a persistent store with localStorage backing and cross-tab sync.
|
|
325
|
+
|
|
326
|
+
- `namespace` - Unique identifier for the store
|
|
327
|
+
- `defaultValue` - Initial state shape
|
|
328
|
+
- `options.memoryOnly` - Disable persistence (default: false)
|
|
329
|
+
|
|
330
|
+
### useMemoryStore(defaultValue)
|
|
331
|
+
|
|
332
|
+
Creates a component-scoped store that doesn't persist.
|
|
333
|
+
|
|
334
|
+
### useForm(defaultValue, fieldConfigs?)
|
|
335
|
+
|
|
336
|
+
Creates a form store with validation support.
|
|
337
|
+
|
|
338
|
+
### State Methods
|
|
339
|
+
|
|
340
|
+
| Method | Description |
|
|
341
|
+
| ------------------------ | ------------------------------------------------------- |
|
|
342
|
+
| `.use()` | Subscribe and read value (triggers re-render on change) |
|
|
343
|
+
| `.useDebounce(ms)` | Subscribe with debounced updates |
|
|
344
|
+
| `.useState()` | Returns `[value, setValue]` tuple |
|
|
345
|
+
| `.value` | Read without subscribing |
|
|
346
|
+
| `.set(value)` | Update value |
|
|
347
|
+
| `.set(fn)` | Functional update |
|
|
348
|
+
| `.reset()` | Delete value at path |
|
|
349
|
+
| `.subscribe(fn)` | Subscribe to changes (for effects) |
|
|
350
|
+
| `.notify()` | Manually trigger subscribers |
|
|
351
|
+
| `.compute(fn)` | Derive a computed value |
|
|
352
|
+
| `.derived({ from, to })` | Create bidirectional transform |
|
|
353
|
+
| `.Render` | Render prop component |
|
|
354
|
+
| `.Show` | Conditional render component |
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
AGPL-3.0
|
package/dist/form.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
2
|
import type { ArrayProxy, State } from './types';
|
|
3
3
|
export { useForm, type CreateFormOptions, type DeepNonNullable, type FormArrayProxy, type FormDeepProxy, type FormState, type FormStore };
|
|
4
|
+
/**
|
|
5
|
+
* Common form field methods available on every form state node.
|
|
6
|
+
*/
|
|
4
7
|
type FormCommon = {
|
|
5
|
-
/** Subscribe and read the error
|
|
8
|
+
/** Subscribe and read the validation error. Re-renders when the error changes. */
|
|
6
9
|
useError: () => string | undefined;
|
|
7
|
-
/** Read the error
|
|
10
|
+
/** Read the validation error without subscribing. */
|
|
8
11
|
readonly error: string | undefined;
|
|
9
|
-
/**
|
|
12
|
+
/** Manually set a validation error. */
|
|
10
13
|
setError: (error: string | undefined) => void;
|
|
11
14
|
};
|
|
12
15
|
type FormArrayProxy<T> = ArrayProxy<T> & FormCommon;
|
|
@@ -19,8 +22,13 @@ type FormDeepProxy<T> = NonNullable<T> extends readonly (infer U)[] ? FormArrayP
|
|
|
19
22
|
type DeepNonNullable<T> = NonNullable<T> extends readonly (infer U)[] ? U[] : NonNullable<T> extends FieldValues ? {
|
|
20
23
|
[K in keyof NonNullable<T>]-?: DeepNonNullable<NonNullable<T>[K]>;
|
|
21
24
|
} : NonNullable<T>;
|
|
25
|
+
/**
|
|
26
|
+
* The form store type, combining form state with validation and submission handling.
|
|
27
|
+
*/
|
|
22
28
|
type FormStore<T extends FieldValues> = FormDeepProxy<T> & {
|
|
29
|
+
/** Clears all validation errors from the form. */
|
|
23
30
|
clearErrors(): void;
|
|
31
|
+
/** Returns a form submit handler that validates and calls onSubmit with form values. */
|
|
24
32
|
handleSubmit(onSubmit: (values: T) => void): (e: React.FormEvent) => void;
|
|
25
33
|
};
|
|
26
34
|
type NoEmptyValidator = 'not-empty';
|
|
@@ -31,4 +39,28 @@ type FieldConfig<T extends FieldValues> = {
|
|
|
31
39
|
validate?: Validator<T>;
|
|
32
40
|
};
|
|
33
41
|
type CreateFormOptions<T extends FieldValues> = Partial<Record<FieldPath<T>, FieldConfig<T>>>;
|
|
42
|
+
/**
|
|
43
|
+
* React hook that creates a form store with validation support.
|
|
44
|
+
*
|
|
45
|
+
* The form store extends the memory store with error handling and validation.
|
|
46
|
+
* Fields can be configured with validators that run on every change.
|
|
47
|
+
*
|
|
48
|
+
* @param defaultValue - Initial form values
|
|
49
|
+
* @param fieldConfigs - Optional validation configuration per field
|
|
50
|
+
* @returns A form store with validation and submission handling
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const form = useForm(
|
|
54
|
+
* { email: '', password: '' },
|
|
55
|
+
* {
|
|
56
|
+
* email: { validate: 'not-empty' },
|
|
57
|
+
* password: { validate: v => v && v.length < 8 ? 'Too short' : undefined }
|
|
58
|
+
* }
|
|
59
|
+
* )
|
|
60
|
+
*
|
|
61
|
+
* <form onSubmit={form.handleSubmit(values => console.log(values))}>
|
|
62
|
+
* <input value={form.email.use() ?? ''} onChange={e => form.email.set(e.target.value)} />
|
|
63
|
+
* {form.email.useError() && <span>{form.email.error}</span>}
|
|
64
|
+
* </form>
|
|
65
|
+
*/
|
|
34
66
|
declare function useForm<T extends FieldValues>(defaultValue: T, fieldConfigs?: CreateFormOptions<T>): FormStore<T>;
|
package/dist/form.js
CHANGED
|
@@ -6,6 +6,30 @@ import { getSnapshot, produce } from './impl';
|
|
|
6
6
|
import { createNode } from './node';
|
|
7
7
|
import { createStoreRoot } from './root';
|
|
8
8
|
export { useForm };
|
|
9
|
+
/**
|
|
10
|
+
* React hook that creates a form store with validation support.
|
|
11
|
+
*
|
|
12
|
+
* The form store extends the memory store with error handling and validation.
|
|
13
|
+
* Fields can be configured with validators that run on every change.
|
|
14
|
+
*
|
|
15
|
+
* @param defaultValue - Initial form values
|
|
16
|
+
* @param fieldConfigs - Optional validation configuration per field
|
|
17
|
+
* @returns A form store with validation and submission handling
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const form = useForm(
|
|
21
|
+
* { email: '', password: '' },
|
|
22
|
+
* {
|
|
23
|
+
* email: { validate: 'not-empty' },
|
|
24
|
+
* password: { validate: v => v && v.length < 8 ? 'Too short' : undefined }
|
|
25
|
+
* }
|
|
26
|
+
* )
|
|
27
|
+
*
|
|
28
|
+
* <form onSubmit={form.handleSubmit(values => console.log(values))}>
|
|
29
|
+
* <input value={form.email.use() ?? ''} onChange={e => form.email.set(e.target.value)} />
|
|
30
|
+
* {form.email.useError() && <span>{form.email.error}</span>}
|
|
31
|
+
* </form>
|
|
32
|
+
*/
|
|
9
33
|
function useForm(defaultValue, fieldConfigs = {}) {
|
|
10
34
|
const formId = useId();
|
|
11
35
|
const namespace = `form:${formId}`;
|
|
@@ -53,6 +77,14 @@ function useForm(defaultValue, fieldConfigs = {}) {
|
|
|
53
77
|
}
|
|
54
78
|
return store;
|
|
55
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a form proxy node that extends the base node with error handling.
|
|
82
|
+
*
|
|
83
|
+
* @param storeApi - The form's value store
|
|
84
|
+
* @param errorStore - The form's error store
|
|
85
|
+
* @param path - The field path
|
|
86
|
+
* @returns A proxy with both state methods and error methods
|
|
87
|
+
*/
|
|
56
88
|
const createFormProxy = (storeApi, errorStore, path) => {
|
|
57
89
|
const proxyCache = new Map();
|
|
58
90
|
const useError = () => errorStore.use(path);
|
|
@@ -73,6 +105,13 @@ const createFormProxy = (storeApi, errorStore, path) => {
|
|
|
73
105
|
}
|
|
74
106
|
});
|
|
75
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Converts a validator configuration into a validation function.
|
|
110
|
+
*
|
|
111
|
+
* @param field - The field path (used for error messages)
|
|
112
|
+
* @param validator - The validator config ('not-empty', RegExp, or function)
|
|
113
|
+
* @returns A validation function, or undefined if no validator provided
|
|
114
|
+
*/
|
|
76
115
|
function getValidator(field, validator) {
|
|
77
116
|
if (!validator) {
|
|
78
117
|
return undefined;
|
|
@@ -85,18 +124,37 @@ function getValidator(field, validator) {
|
|
|
85
124
|
}
|
|
86
125
|
return validator;
|
|
87
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Validates that a field has a non-empty value.
|
|
129
|
+
*
|
|
130
|
+
* @param field - The field path (used for error message)
|
|
131
|
+
* @param value - The value to validate
|
|
132
|
+
* @returns Error message if empty, undefined if valid
|
|
133
|
+
*/
|
|
88
134
|
function validateNoEmpty(field, value) {
|
|
89
135
|
if (!stringValue(value)) {
|
|
90
136
|
return `${pascalCase(field)} is required`;
|
|
91
137
|
}
|
|
92
138
|
return undefined;
|
|
93
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Validates that a field matches a regular expression.
|
|
142
|
+
*
|
|
143
|
+
* @param field - The field path (used for error message)
|
|
144
|
+
* @param value - The value to validate
|
|
145
|
+
* @param regex - The pattern to match against
|
|
146
|
+
* @returns Error message if invalid, undefined if valid
|
|
147
|
+
*/
|
|
94
148
|
function validateRegex(field, value, regex) {
|
|
95
149
|
if (!regex.test(stringValue(value))) {
|
|
96
150
|
return `${pascalCase(field)} is invalid`;
|
|
97
151
|
}
|
|
98
152
|
return undefined;
|
|
99
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Converts a value to a string for validation purposes.
|
|
156
|
+
* Returns empty string for non-primitive values.
|
|
157
|
+
*/
|
|
100
158
|
function stringValue(v) {
|
|
101
159
|
if (typeof v === 'string') {
|
|
102
160
|
return v;
|
package/dist/impl.d.ts
CHANGED
|
@@ -1,19 +1,82 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
2
|
export { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe };
|
|
3
|
+
/**
|
|
4
|
+
* Joins a namespace and path into a full key string.
|
|
5
|
+
*
|
|
6
|
+
* @param namespace - The store namespace (root key)
|
|
7
|
+
* @param path - Optional dot-separated path within the namespace
|
|
8
|
+
* @returns Combined key string (e.g., "app.user.name")
|
|
9
|
+
*/
|
|
3
10
|
declare function joinPath(namespace: string, path?: string): string;
|
|
4
11
|
/** Get a nested value from an object/array using a dot-separated path. */
|
|
5
12
|
declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
6
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Notifies all relevant listeners when a value changes.
|
|
15
|
+
*
|
|
16
|
+
* Handles three types of listeners:
|
|
17
|
+
* 1. Exact match - listeners subscribed to the exact changed path
|
|
18
|
+
* 2. Root listeners - listeners on the namespace root (for full-store subscriptions)
|
|
19
|
+
* 3. Child listeners - listeners on nested paths that may be affected by the change
|
|
20
|
+
*
|
|
21
|
+
* Child listeners are only notified if their specific value actually changed,
|
|
22
|
+
* determined by deep equality comparison.
|
|
23
|
+
*/
|
|
7
24
|
declare function notifyListeners(key: string, oldValue: unknown, newValue: unknown, skipRoot?: boolean, skipChildren?: boolean): void;
|
|
8
25
|
/** Snapshot getter used by React's useSyncExternalStore. */
|
|
9
26
|
declare function getSnapshot(key: string): unknown;
|
|
10
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Core mutation function that updates the store and notifies listeners.
|
|
29
|
+
*
|
|
30
|
+
* Handles both setting and deleting values, with optimizations to skip
|
|
31
|
+
* unnecessary updates when the value hasn't changed.
|
|
32
|
+
*
|
|
33
|
+
* @param key - The full key path to update
|
|
34
|
+
* @param value - The new value, or undefined to delete
|
|
35
|
+
* @param skipUpdate - When true, skips notifying listeners
|
|
36
|
+
* @param memoryOnly - When true, skips localStorage persistence
|
|
37
|
+
*/
|
|
11
38
|
declare function produce(key: string, value: unknown, skipUpdate?: boolean, memoryOnly?: boolean): void;
|
|
12
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* React hook that subscribes to and reads a value at a path.
|
|
41
|
+
*
|
|
42
|
+
* Uses useSyncExternalStore for tear-free reads and automatic re-rendering
|
|
43
|
+
* when the subscribed value changes.
|
|
44
|
+
*
|
|
45
|
+
* @param key - The namespace or full key
|
|
46
|
+
* @param path - Optional path within the namespace
|
|
47
|
+
* @returns The current value at the path, or undefined if not set
|
|
48
|
+
*/
|
|
13
49
|
declare function useObject<T extends FieldValues, P extends FieldPath<T>>(key: string, path?: P): FieldPathValue<T, P> | undefined;
|
|
14
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* React hook that subscribes to a value with debounced updates.
|
|
52
|
+
*
|
|
53
|
+
* The returned value only updates after the specified delay has passed
|
|
54
|
+
* since the last change, useful for expensive operations like search.
|
|
55
|
+
*
|
|
56
|
+
* @param key - The namespace or full key
|
|
57
|
+
* @param path - Path within the namespace
|
|
58
|
+
* @param delay - Debounce delay in milliseconds
|
|
59
|
+
* @returns The debounced value at the path
|
|
60
|
+
*/
|
|
15
61
|
declare function useDebounce<T extends FieldValues, P extends FieldPath<T>>(key: string, path: P, delay: number): FieldPathValue<T, P> | undefined;
|
|
16
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* React hook for side effects when a value changes.
|
|
64
|
+
*
|
|
65
|
+
* Unlike `use()`, this doesn't cause re-renders. Instead, it calls the
|
|
66
|
+
* provided callback whenever the value changes, useful for syncing with
|
|
67
|
+
* external systems or triggering effects.
|
|
68
|
+
*
|
|
69
|
+
* @param key - The full key path to subscribe to
|
|
70
|
+
* @param onChange - Callback invoked with the new value on each change
|
|
71
|
+
*/
|
|
17
72
|
declare function useSubscribe<T>(key: string, onChange: (value: T) => void): void;
|
|
18
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Sets a value at a specific path within a namespace.
|
|
75
|
+
*
|
|
76
|
+
* @param key - The namespace
|
|
77
|
+
* @param path - Path within the namespace
|
|
78
|
+
* @param value - The value to set, or undefined to delete
|
|
79
|
+
* @param skipUpdate - When true, skips notifying listeners
|
|
80
|
+
* @param memoryOnly - When true, skips localStorage persistence
|
|
81
|
+
*/
|
|
19
82
|
declare function setLeaf<T extends FieldValues, P extends FieldPath<T>>(key: string, path: P, value: FieldPathValue<T, P> | undefined, skipUpdate?: boolean, memoryOnly?: boolean): void;
|