juststore 1.1.1 → 1.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/LICENSE +21 -661
- package/README.md +143 -166
- package/dist/atom.d.ts +1 -1
- package/dist/form.d.ts +1 -1
- package/dist/form.js +3 -1
- package/dist/impl.d.ts +1 -1
- package/dist/impl.js +1 -1
- package/dist/kv_store.d.ts +1 -1
- package/dist/kv_store.js +10 -2
- package/dist/memory.d.ts +1 -1
- package/dist/src/atom.d.ts +45 -0
- package/dist/src/atom.js +141 -0
- package/dist/src/form.d.ts +97 -0
- package/dist/src/form.js +176 -0
- package/dist/src/impl.d.ts +128 -0
- package/dist/src/impl.js +644 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +7 -0
- package/dist/src/kv_store.d.ts +29 -0
- package/dist/src/kv_store.js +127 -0
- package/dist/src/local_storage.d.ts +7 -0
- package/dist/src/local_storage.js +43 -0
- package/dist/src/memory.d.ts +54 -0
- package/dist/src/memory.js +55 -0
- package/dist/src/mixed_state.d.ts +20 -0
- package/dist/src/mixed_state.js +45 -0
- package/dist/src/node.d.ts +41 -0
- package/dist/src/node.js +374 -0
- package/dist/src/path.d.ts +136 -0
- package/dist/src/path.js +26 -0
- package/dist/src/root.d.ts +23 -0
- package/dist/src/root.js +81 -0
- package/dist/src/stable_keys.d.ts +4 -0
- package/dist/src/stable_keys.js +31 -0
- package/dist/src/store.d.ts +42 -0
- package/dist/src/store.js +40 -0
- package/dist/src/types.d.ts +143 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +72 -0
- package/dist/src/utils.js +76 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +61 -61
package/README.md
CHANGED
|
@@ -21,56 +21,56 @@ bun add juststore
|
|
|
21
21
|
## Quick Start
|
|
22
22
|
|
|
23
23
|
```tsx
|
|
24
|
-
import { createStore } from
|
|
25
|
-
import { toast } from
|
|
24
|
+
import { createStore } from 'juststore'
|
|
25
|
+
import { toast } from 'sonner'
|
|
26
26
|
|
|
27
27
|
type AppState = {
|
|
28
28
|
user: {
|
|
29
|
-
name: string
|
|
29
|
+
name: string
|
|
30
30
|
preferences: {
|
|
31
|
-
theme:
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
todos: { id: number; text: string; done: boolean }[]
|
|
35
|
-
}
|
|
31
|
+
theme: 'light' | 'dark'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
todos: { id: number; text: string; done: boolean }[]
|
|
35
|
+
}
|
|
36
36
|
|
|
37
|
-
const store = createStore<AppState>(
|
|
37
|
+
const store = createStore<AppState>('app', {
|
|
38
38
|
user: {
|
|
39
|
-
name:
|
|
40
|
-
preferences: { theme:
|
|
39
|
+
name: 'Guest',
|
|
40
|
+
preferences: { theme: 'light' }
|
|
41
41
|
},
|
|
42
|
-
todos: []
|
|
43
|
-
})
|
|
42
|
+
todos: []
|
|
43
|
+
})
|
|
44
44
|
|
|
45
45
|
async function initUserDetails() {
|
|
46
|
-
const response = await fetch(
|
|
47
|
-
const data = (await response.json()) as AppState[
|
|
48
|
-
store.user.set(data)
|
|
46
|
+
const response = await fetch('/api/user/details')
|
|
47
|
+
const data = (await response.json()) as AppState['user']
|
|
48
|
+
store.user.set(data)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function ThemeToggle() {
|
|
52
|
-
const theme = store.user.preferences.theme.use()
|
|
53
|
-
const nextTheme = theme ===
|
|
52
|
+
const theme = store.user.preferences.theme.use()
|
|
53
|
+
const nextTheme = theme === 'light' ? 'dark' : 'light'
|
|
54
54
|
|
|
55
55
|
const updateTheme = async () => {
|
|
56
56
|
try {
|
|
57
|
-
const response = await fetch(
|
|
58
|
-
method:
|
|
59
|
-
headers: {
|
|
60
|
-
body: JSON.stringify({ theme: nextTheme })
|
|
61
|
-
})
|
|
57
|
+
const response = await fetch('/api/user/preferences/theme', {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ theme: nextTheme })
|
|
61
|
+
})
|
|
62
62
|
|
|
63
63
|
if (!response.ok) {
|
|
64
|
-
throw new Error(
|
|
64
|
+
throw new Error('Theme update failed')
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
store.user.preferences.theme.set(nextTheme)
|
|
67
|
+
store.user.preferences.theme.set(nextTheme)
|
|
68
68
|
} catch {
|
|
69
|
-
toast.error(
|
|
69
|
+
toast.error('Failed to update theme')
|
|
70
70
|
}
|
|
71
|
-
}
|
|
71
|
+
}
|
|
72
72
|
|
|
73
|
-
return <button onClick={updateTheme}>Theme: {theme}</button
|
|
73
|
+
return <button onClick={updateTheme}>Theme: {theme}</button>
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -80,70 +80,65 @@ function ThemeToggle() {
|
|
|
80
80
|
|
|
81
81
|
```tsx
|
|
82
82
|
type SearchState = {
|
|
83
|
-
query: string
|
|
84
|
-
category:
|
|
85
|
-
services: { id: string; name: string; status:
|
|
86
|
-
}
|
|
83
|
+
query: string
|
|
84
|
+
category: 'all' | 'running' | 'stopped'
|
|
85
|
+
services: { id: string; name: string; status: 'running' | 'stopped' }[]
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
const searchStore = createStore<SearchState>(
|
|
89
|
-
query:
|
|
90
|
-
category:
|
|
91
|
-
services: []
|
|
92
|
-
})
|
|
88
|
+
const searchStore = createStore<SearchState>('services-search', {
|
|
89
|
+
query: '',
|
|
90
|
+
category: 'all',
|
|
91
|
+
services: []
|
|
92
|
+
})
|
|
93
93
|
|
|
94
94
|
function SearchQueryInput() {
|
|
95
|
-
const query = searchStore.query.use() ??
|
|
95
|
+
const query = searchStore.query.use() ?? ''
|
|
96
96
|
return (
|
|
97
97
|
<input
|
|
98
98
|
value={query}
|
|
99
|
-
onChange={
|
|
99
|
+
onChange={e => searchStore.query.set(e.target.value)}
|
|
100
100
|
placeholder="Search services"
|
|
101
101
|
/>
|
|
102
|
-
)
|
|
102
|
+
)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function SearchCategoryFilter() {
|
|
106
|
-
const category = searchStore.category.use()
|
|
106
|
+
const category = searchStore.category.use()
|
|
107
107
|
return (
|
|
108
108
|
<select
|
|
109
109
|
value={category}
|
|
110
|
-
onChange={(e)
|
|
111
|
-
searchStore.category.set(e.target.value as SearchState["category"])
|
|
112
|
-
}
|
|
110
|
+
onChange={e => searchStore.category.set(e.target.value as SearchState['category'])}
|
|
113
111
|
>
|
|
114
112
|
<option value="all">All</option>
|
|
115
113
|
<option value="running">Running</option>
|
|
116
114
|
<option value="stopped">Stopped</option>
|
|
117
115
|
</select>
|
|
118
|
-
)
|
|
116
|
+
)
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
function SearchResults() {
|
|
122
|
-
const query = searchStore.query.useDebounce(150) ??
|
|
123
|
-
const category = searchStore.category.use()
|
|
120
|
+
const query = searchStore.query.useDebounce(150) ?? ''
|
|
121
|
+
const category = searchStore.category.use()
|
|
124
122
|
|
|
125
123
|
const visible = searchStore.services.useCompute(
|
|
126
|
-
|
|
127
|
-
const list = services ?? []
|
|
128
|
-
return list.filter(
|
|
129
|
-
const nameMatch = service.name
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
category === "all" ? true : service.status === category;
|
|
134
|
-
return nameMatch && categoryMatch;
|
|
135
|
-
});
|
|
124
|
+
services => {
|
|
125
|
+
const list = services ?? []
|
|
126
|
+
return list.filter(service => {
|
|
127
|
+
const nameMatch = service.name.toLowerCase().includes(query.toLowerCase())
|
|
128
|
+
const categoryMatch = category === 'all' ? true : service.status === category
|
|
129
|
+
return nameMatch && categoryMatch
|
|
130
|
+
})
|
|
136
131
|
},
|
|
137
|
-
[query, category]
|
|
138
|
-
)
|
|
132
|
+
[query, category]
|
|
133
|
+
)
|
|
139
134
|
|
|
140
135
|
return (
|
|
141
136
|
<ul>
|
|
142
|
-
{visible.map(
|
|
137
|
+
{visible.map(service => (
|
|
143
138
|
<li key={service.id}>{service.name}</li>
|
|
144
139
|
))}
|
|
145
140
|
</ul>
|
|
146
|
-
)
|
|
141
|
+
)
|
|
147
142
|
}
|
|
148
143
|
|
|
149
144
|
function ServiceSearchPage() {
|
|
@@ -153,40 +148,40 @@ function ServiceSearchPage() {
|
|
|
153
148
|
<SearchCategoryFilter />
|
|
154
149
|
<SearchResults />
|
|
155
150
|
</>
|
|
156
|
-
)
|
|
151
|
+
)
|
|
157
152
|
}
|
|
158
153
|
```
|
|
159
154
|
|
|
160
155
|
### 2) WebSocket ingestion into normalized state
|
|
161
156
|
|
|
162
157
|
```tsx
|
|
163
|
-
type RouteUptime = { alias: string; uptime: number }
|
|
158
|
+
type RouteUptime = { alias: string; uptime: number }
|
|
164
159
|
type UptimeState = {
|
|
165
|
-
routeKeys: string[]
|
|
166
|
-
uptimeByAlias: Record<string, RouteUptime
|
|
167
|
-
}
|
|
160
|
+
routeKeys: string[]
|
|
161
|
+
uptimeByAlias: Record<string, RouteUptime>
|
|
162
|
+
}
|
|
168
163
|
|
|
169
|
-
const uptimeStore = createStore<UptimeState>(
|
|
164
|
+
const uptimeStore = createStore<UptimeState>('uptime', {
|
|
170
165
|
routeKeys: [],
|
|
171
|
-
uptimeByAlias: {}
|
|
172
|
-
})
|
|
166
|
+
uptimeByAlias: {}
|
|
167
|
+
})
|
|
173
168
|
|
|
174
169
|
function onUptimeMessage(rows: RouteUptime[]) {
|
|
175
|
-
const keys = rows.map(
|
|
176
|
-
uptimeStore.routeKeys.set(keys)
|
|
170
|
+
const keys = rows.map(row => row.alias).toSorted()
|
|
171
|
+
uptimeStore.routeKeys.set(keys)
|
|
177
172
|
|
|
178
173
|
uptimeStore.uptimeByAlias.set(
|
|
179
174
|
rows.reduce<Record<string, RouteUptime>>((acc, row) => {
|
|
180
|
-
acc[row.alias] = row
|
|
181
|
-
return acc
|
|
182
|
-
}, {})
|
|
183
|
-
)
|
|
175
|
+
acc[row.alias] = row
|
|
176
|
+
return acc
|
|
177
|
+
}, {})
|
|
178
|
+
)
|
|
184
179
|
}
|
|
185
180
|
|
|
186
181
|
// fine grained subscription
|
|
187
182
|
function UptimeComponent({ alias }: { alias: string }) {
|
|
188
|
-
const uptime = uptimeStore.uptimeByAlias[alias]?.uptime.use()
|
|
189
|
-
return <div>Uptime: {uptime ??
|
|
183
|
+
const uptime = uptimeStore.uptimeByAlias[alias]?.uptime.use()
|
|
184
|
+
return <div>Uptime: {uptime ?? 'Unknown'}</div>
|
|
190
185
|
}
|
|
191
186
|
```
|
|
192
187
|
|
|
@@ -194,72 +189,62 @@ function UptimeComponent({ alias }: { alias: string }) {
|
|
|
194
189
|
|
|
195
190
|
```tsx
|
|
196
191
|
type HeaderState = {
|
|
197
|
-
headers: Record<string, string
|
|
198
|
-
}
|
|
192
|
+
headers: Record<string, string>
|
|
193
|
+
}
|
|
199
194
|
|
|
200
|
-
const headerStore = createStore<HeaderState>(
|
|
201
|
-
headers: {}
|
|
202
|
-
})
|
|
195
|
+
const headerStore = createStore<HeaderState>('route-headers', {
|
|
196
|
+
headers: {}
|
|
197
|
+
})
|
|
203
198
|
|
|
204
199
|
function HeadersEditor() {
|
|
205
200
|
// keys is a virtual property that returns a state proxy for the keys array
|
|
206
201
|
// it only recomputes when the keys array changes
|
|
207
|
-
const keys = headerStore.headers.keys.use()
|
|
202
|
+
const keys = headerStore.headers.keys.use()
|
|
208
203
|
|
|
209
204
|
return (
|
|
210
205
|
<div>
|
|
211
|
-
{keys.map(
|
|
206
|
+
{keys.map(key => (
|
|
212
207
|
<div key={key}>
|
|
213
208
|
<input
|
|
214
209
|
value={key}
|
|
215
|
-
onChange={(e)
|
|
216
|
-
headerStore.headers.rename(key, e.target.value.trim())
|
|
217
|
-
}
|
|
210
|
+
onChange={e => headerStore.headers.rename(key, e.target.value.trim())}
|
|
218
211
|
/>
|
|
219
212
|
{/* Render and update without cascade rerendering the entire HeadersEditor */}
|
|
220
213
|
<RenderWithUpdate state={headerStore.headers[key]}>
|
|
221
|
-
{(value, update) => (
|
|
222
|
-
<input value={value} onChange={(e) => update(e.target.value)} />
|
|
223
|
-
)}
|
|
214
|
+
{(value, update) => <input value={value} onChange={e => update(e.target.value)} />}
|
|
224
215
|
</RenderWithUpdate>
|
|
225
|
-
<button onClick={() => headerStore.headers[key].reset()}>
|
|
226
|
-
remove
|
|
227
|
-
</button>
|
|
216
|
+
<button onClick={() => headerStore.headers[key].reset()}>remove</button>
|
|
228
217
|
</div>
|
|
229
218
|
))}
|
|
230
219
|
</div>
|
|
231
|
-
)
|
|
220
|
+
)
|
|
232
221
|
}
|
|
233
222
|
```
|
|
234
223
|
|
|
235
224
|
### 4) Typed form with validation and submit gating
|
|
236
225
|
|
|
237
226
|
```tsx
|
|
238
|
-
import { useForm } from
|
|
239
|
-
import {
|
|
240
|
-
StoreFormInputField,
|
|
241
|
-
StoreFormPasswordField,
|
|
242
|
-
} from "@/components/store/Input"; // from juststore-shadcn
|
|
227
|
+
import { useForm } from 'juststore'
|
|
228
|
+
import { StoreFormInputField, StoreFormPasswordField } from '@/components/store/Input' // from juststore-shadcn
|
|
243
229
|
|
|
244
230
|
type LoginForm = {
|
|
245
|
-
email: string
|
|
246
|
-
password: string
|
|
247
|
-
}
|
|
231
|
+
email: string
|
|
232
|
+
password: string
|
|
233
|
+
}
|
|
248
234
|
|
|
249
235
|
function LoginPage() {
|
|
250
236
|
const form = useForm<LoginForm>(
|
|
251
|
-
{ email:
|
|
237
|
+
{ email: '', password: '' },
|
|
252
238
|
{
|
|
253
239
|
email: { validate: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
|
|
254
240
|
password: {
|
|
255
|
-
validate: (value)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
241
|
+
validate: value => (value && value.length < 8 ? 'Password too short' : undefined)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
)
|
|
260
245
|
|
|
261
246
|
return (
|
|
262
|
-
<form onSubmit={form.handleSubmit(
|
|
247
|
+
<form onSubmit={form.handleSubmit(values => console.log(values))}>
|
|
263
248
|
<StoreFormInputField
|
|
264
249
|
state={form.email}
|
|
265
250
|
type="email"
|
|
@@ -273,54 +258,51 @@ function LoginPage() {
|
|
|
273
258
|
/>
|
|
274
259
|
<button type="submit">Sign in</button>
|
|
275
260
|
</form>
|
|
276
|
-
)
|
|
261
|
+
)
|
|
277
262
|
}
|
|
278
263
|
```
|
|
279
264
|
|
|
280
265
|
### 5) Mixed read model for unified UI flags
|
|
281
266
|
|
|
282
267
|
```tsx
|
|
283
|
-
import { createMixedState, createStore } from
|
|
268
|
+
import { createMixedState, createStore } from 'juststore'
|
|
284
269
|
|
|
285
270
|
type OpsState = {
|
|
286
|
-
syncingConfig: boolean
|
|
287
|
-
savingRoute: boolean
|
|
288
|
-
reloadingAgent: boolean
|
|
289
|
-
}
|
|
271
|
+
syncingConfig: boolean
|
|
272
|
+
savingRoute: boolean
|
|
273
|
+
reloadingAgent: boolean
|
|
274
|
+
}
|
|
290
275
|
|
|
291
|
-
const opsStore = createStore<OpsState>(
|
|
276
|
+
const opsStore = createStore<OpsState>('ops', {
|
|
292
277
|
syncingConfig: false,
|
|
293
278
|
savingRoute: false,
|
|
294
|
-
reloadingAgent: false
|
|
295
|
-
})
|
|
279
|
+
reloadingAgent: false
|
|
280
|
+
})
|
|
296
281
|
|
|
297
282
|
const busyState = createMixedState(
|
|
298
283
|
opsStore.syncingConfig,
|
|
299
284
|
opsStore.savingRoute,
|
|
300
|
-
opsStore.reloadingAgent
|
|
301
|
-
)
|
|
285
|
+
opsStore.reloadingAgent
|
|
286
|
+
)
|
|
302
287
|
|
|
303
288
|
function GlobalBusyOverlay() {
|
|
304
289
|
const isBusy = busyState.useCompute(
|
|
305
|
-
([syncingConfig, savingRoute, reloadingAgent]) =>
|
|
306
|
-
|
|
307
|
-
);
|
|
290
|
+
([syncingConfig, savingRoute, reloadingAgent]) => syncingConfig || savingRoute || reloadingAgent
|
|
291
|
+
)
|
|
308
292
|
|
|
309
|
-
if (!isBusy) return null
|
|
310
|
-
return <div className="overlay">Loading...</div
|
|
293
|
+
if (!isBusy) return null
|
|
294
|
+
return <div className="overlay">Loading...</div>
|
|
311
295
|
}
|
|
312
296
|
|
|
313
297
|
function BusyLabel() {
|
|
314
|
-
const label = busyState.useCompute(
|
|
315
|
-
(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return <span>{label}</span>;
|
|
298
|
+
const label = busyState.useCompute(([syncingConfig, savingRoute, reloadingAgent]) => {
|
|
299
|
+
if (syncingConfig) return 'Syncing config...'
|
|
300
|
+
if (savingRoute) return 'Saving route...'
|
|
301
|
+
if (reloadingAgent) return 'Reloading agent...'
|
|
302
|
+
return 'Idle'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return <span>{label}</span>
|
|
324
306
|
}
|
|
325
307
|
```
|
|
326
308
|
|
|
@@ -329,64 +311,59 @@ function BusyLabel() {
|
|
|
329
311
|
### Read and write state
|
|
330
312
|
|
|
331
313
|
```tsx
|
|
332
|
-
const name = store.user.name.use()
|
|
333
|
-
const current = store.user.name.value
|
|
334
|
-
store.user.name.set(
|
|
335
|
-
store.user.name.set(
|
|
314
|
+
const name = store.user.name.use() // subscribe
|
|
315
|
+
const current = store.user.name.value // read without subscribe
|
|
316
|
+
store.user.name.set('Alice')
|
|
317
|
+
store.user.name.set(prev => prev.toUpperCase())
|
|
336
318
|
```
|
|
337
319
|
|
|
338
320
|
### Path-based dynamic API
|
|
339
321
|
|
|
340
322
|
```tsx
|
|
341
|
-
store.set(
|
|
342
|
-
const name = store.use(
|
|
343
|
-
const value = store.value(
|
|
323
|
+
store.set('user.name', 'Alice')
|
|
324
|
+
const name = store.use('user.name')
|
|
325
|
+
const value = store.value('user.name')
|
|
344
326
|
```
|
|
345
327
|
|
|
346
328
|
### Arrays
|
|
347
329
|
|
|
348
330
|
```tsx
|
|
349
|
-
store.todos.push({ id: Date.now(), text:
|
|
350
|
-
store.todos.at(0).done.set(true)
|
|
331
|
+
store.todos.push({ id: Date.now(), text: 'new', done: false })
|
|
332
|
+
store.todos.at(0).done.set(true)
|
|
351
333
|
store.todos.sortedInsert((a, b) => a.id - b.id, {
|
|
352
334
|
id: 2,
|
|
353
|
-
text:
|
|
354
|
-
done: false
|
|
355
|
-
})
|
|
335
|
+
text: 'x',
|
|
336
|
+
done: false
|
|
337
|
+
})
|
|
356
338
|
|
|
357
|
-
const len = store.todos.length
|
|
358
|
-
const liveLen = store.todos.useLength()
|
|
339
|
+
const len = store.todos.length
|
|
340
|
+
const liveLen = store.todos.useLength()
|
|
359
341
|
```
|
|
360
342
|
|
|
361
343
|
### Computed and derived values
|
|
362
344
|
|
|
363
345
|
```tsx
|
|
364
346
|
const total = store.cart.items.useCompute(
|
|
365
|
-
|
|
366
|
-
)
|
|
347
|
+
items => items?.reduce((sum, item) => sum + item.price * item.qty, 0) ?? 0
|
|
348
|
+
)
|
|
367
349
|
|
|
368
350
|
const fahrenheit = store.temperature.derived({
|
|
369
|
-
from:
|
|
370
|
-
to:
|
|
371
|
-
})
|
|
351
|
+
from: celsius => ((celsius ?? 0) * 9) / 5 + 32,
|
|
352
|
+
to: f => ((f - 32) * 5) / 9
|
|
353
|
+
})
|
|
372
354
|
```
|
|
373
355
|
|
|
374
356
|
### Render helpers
|
|
375
357
|
|
|
376
358
|
```tsx
|
|
377
|
-
import { Conditional, Render, RenderWithUpdate } from
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<button onClick={() => update((value ?? 0) + 1)}>{value}</button>
|
|
384
|
-
)}
|
|
385
|
-
</RenderWithUpdate>;
|
|
386
|
-
|
|
387
|
-
<Conditional state={store.user.role} on={(role) => role === "admin"}>
|
|
359
|
+
import { Conditional, Render, RenderWithUpdate } from 'juststore'
|
|
360
|
+
;<Render state={store.counter}>{value => <span>{value}</span>}</Render>
|
|
361
|
+
;<RenderWithUpdate state={store.counter}>
|
|
362
|
+
{(value, update) => <button onClick={() => update((value ?? 0) + 1)}>{value}</button>}
|
|
363
|
+
</RenderWithUpdate>
|
|
364
|
+
;<Conditional state={store.user.role} on={role => role === 'admin'}>
|
|
388
365
|
<AdminPage />
|
|
389
|
-
</Conditional
|
|
366
|
+
</Conditional>
|
|
390
367
|
```
|
|
391
368
|
|
|
392
369
|
## API Reference
|
package/dist/atom.d.ts
CHANGED
package/dist/form.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues, IsEqual } from './path';
|
|
2
2
|
import type { ArrayProxy, DerivedStateProps, ObjectMutationMethods, Prettify, ValueState } from './types';
|
|
3
|
-
export {
|
|
3
|
+
export { type CreateFormOptions, createForm, type DeepNonNullable, type FormArrayState, type FormObjectState, type FormState, type FormStore, type FormValueState, useForm };
|
|
4
4
|
/**
|
|
5
5
|
* Common form field methods available on every form state node.
|
|
6
6
|
*/
|
package/dist/form.js
CHANGED
|
@@ -46,7 +46,9 @@ function useForm(defaultValue, fieldConfigs = {}) {
|
|
|
46
46
|
function createForm(namespace, defaultValue, fieldConfigs = {}) {
|
|
47
47
|
const errorNamespace = `_juststore_form_errors.${namespace}`;
|
|
48
48
|
const errorStore = createStoreRoot(errorNamespace, {}, { memoryOnly: true });
|
|
49
|
-
const storeApi = createStoreRoot(namespace, defaultValue, {
|
|
49
|
+
const storeApi = createStoreRoot(namespace, defaultValue, {
|
|
50
|
+
memoryOnly: true
|
|
51
|
+
});
|
|
50
52
|
const formApi = {
|
|
51
53
|
clearErrors: () => produce(errorNamespace, undefined, false, true),
|
|
52
54
|
handleSubmit: (onSubmit) => (e) => {
|
package/dist/impl.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
2
|
import { getStableKeys, setExternalKeyOrder } from './stable_keys';
|
|
3
|
-
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject
|
|
3
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useCompute, useDebounce, useObject };
|
|
4
4
|
declare function testReset(): void;
|
|
5
5
|
declare function isClass(value: unknown): boolean;
|
|
6
6
|
declare function isRecord(value: unknown): boolean;
|
package/dist/impl.js
CHANGED
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from '
|
|
|
2
2
|
import rfcIsEqual from 'react-fast-compare';
|
|
3
3
|
import { KVStore } from './kv_store';
|
|
4
4
|
import { getExternalKeyOrder, getStableKeys, setExternalKeyOrder } from './stable_keys';
|
|
5
|
-
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject
|
|
5
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useCompute, useDebounce, useObject };
|
|
6
6
|
const inMemStorage = new Map();
|
|
7
7
|
const listeners = new Map();
|
|
8
8
|
const descendantListenerKeysByPrefix = new Map();
|
package/dist/kv_store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNestedValue, setNestedValue } from './impl';
|
|
2
|
-
export { getNestedValue, KVStore, setNestedValue
|
|
2
|
+
export { getNestedValue, type KeyValueStore, KVStore, setNestedValue };
|
|
3
3
|
type KeyValueStore = {
|
|
4
4
|
getBroadcastChannel: () => BroadcastChannel | undefined;
|
|
5
5
|
setBroadcastChannel: (broadcastChannel: BroadcastChannel) => void;
|
package/dist/kv_store.js
CHANGED
|
@@ -57,7 +57,11 @@ class KVStore {
|
|
|
57
57
|
localStorageSet(rootKey, rootValue);
|
|
58
58
|
// Broadcast change to other tabs
|
|
59
59
|
if (this.broadcastChannel) {
|
|
60
|
-
this.broadcastChannel.postMessage({
|
|
60
|
+
this.broadcastChannel.postMessage({
|
|
61
|
+
type: 'set',
|
|
62
|
+
key: rootKey,
|
|
63
|
+
value: rootValue
|
|
64
|
+
});
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
@@ -82,7 +86,11 @@ class KVStore {
|
|
|
82
86
|
if (!this.memoryOnly && typeof window !== 'undefined') {
|
|
83
87
|
localStorageSet(rootKey, updatedRoot);
|
|
84
88
|
if (this.broadcastChannel) {
|
|
85
|
-
this.broadcastChannel.postMessage({
|
|
89
|
+
this.broadcastChannel.postMessage({
|
|
90
|
+
type: 'set',
|
|
91
|
+
key: rootKey,
|
|
92
|
+
value: updatedRoot
|
|
93
|
+
});
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
}
|
package/dist/memory.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldValues } from './path';
|
|
2
2
|
import type { State, ValueState } from './types';
|
|
3
|
-
export { createMemoryStore,
|
|
3
|
+
export { createMemoryStore, type MemoryStore, useMemoryStore };
|
|
4
4
|
/**
|
|
5
5
|
* A component local store with React bindings.
|
|
6
6
|
*
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { type Atom, createAtom };
|
|
2
|
+
/**
|
|
3
|
+
* An atom is a value that can be subscribed to and updated.
|
|
4
|
+
*
|
|
5
|
+
* @param T - The type of the value
|
|
6
|
+
* @returns The atom
|
|
7
|
+
*/
|
|
8
|
+
type Atom<T> = {
|
|
9
|
+
/** The current value. */
|
|
10
|
+
readonly value: T;
|
|
11
|
+
/** Subscribe to the value. */
|
|
12
|
+
use: () => T;
|
|
13
|
+
/** Set the value. */
|
|
14
|
+
set: AtomSetState<T>;
|
|
15
|
+
/** Reset the value to the default value. */
|
|
16
|
+
reset: () => void;
|
|
17
|
+
/** Subscribe to the value.with a callback function. */
|
|
18
|
+
subscribe: (listener: (value: T) => void) => () => void;
|
|
19
|
+
/** Compute a derived value from the current value, similar to useState + useMemo */
|
|
20
|
+
useCompute: <R>(fn: (value: T) => R, deps?: readonly unknown[]) => R;
|
|
21
|
+
};
|
|
22
|
+
type AtomSetState<T> = (value: T | ((prev: T) => T)) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Creates an atom with a given id and default value.
|
|
25
|
+
*
|
|
26
|
+
* @param id - The id of the atom
|
|
27
|
+
* @param defaultValue - The default value of the atom
|
|
28
|
+
* @returns The atom
|
|
29
|
+
* @example
|
|
30
|
+
* const stateA = createAtom(useId(), false)
|
|
31
|
+
* return (
|
|
32
|
+
* <>
|
|
33
|
+
* <ComponentA/>
|
|
34
|
+
* <ComponentB/>
|
|
35
|
+
* <Render state={stateA}>
|
|
36
|
+
* {(value, setValue) => (
|
|
37
|
+
* <button onClick={() => setValue(!value)}>{value ? 'Hide' : 'Show'}</button>
|
|
38
|
+
* )}
|
|
39
|
+
* </Render>
|
|
40
|
+
* <ComponentC/>
|
|
41
|
+
* <ComponentD/>
|
|
42
|
+
* </>
|
|
43
|
+
* )
|
|
44
|
+
*/
|
|
45
|
+
declare function createAtom<T>(id: string, defaultValue: T, persistent?: boolean): Atom<T>;
|