cogsbox-state 0.5.476-canary.46 → 0.5.478
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 +543 -543
- package/dist/CogsState.js.map +1 -1
- package/dist/plugins.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +3706 -3706
- package/src/plugins.ts +835 -835
package/README.md
CHANGED
|
@@ -1,543 +1,543 @@
|
|
|
1
|
-
# Cogsbox State
|
|
2
|
-
|
|
3
|
-
> **🚨 DANGER: DO NOT USE - UNSTABLE & EXPERIMENTAL 🚨**
|
|
4
|
-
>
|
|
5
|
-
> This library is in early development and constantly changing.
|
|
6
|
-
>
|
|
7
|
-
> **DO NOT USE IN ANY PROJECT YET - ONLY FOR TESTING AND PROVIDING FEEDBACK.**
|
|
8
|
-
|
|
9
|
-
## What is Cogsbox State?
|
|
10
|
-
|
|
11
|
-
Cogsbox State is a React state management library that creates a **nested state builder** - a type-safe proxy that mimics your initial state structure. Every property in your state becomes a powerful state object with built-in methods for updates, arrays, forms, and more.
|
|
12
|
-
|
|
13
|
-
**Key Philosophy**: Instead of complex useState drilling and manual mapping, you directly access nested properties and use built-in methods.
|
|
14
|
-
|
|
15
|
-
## Getting Started
|
|
16
|
-
|
|
17
|
-
### Basic Setup
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
import { createCogsState } from 'cogsbox-state';
|
|
21
|
-
|
|
22
|
-
type Todo = {
|
|
23
|
-
id: number;
|
|
24
|
-
text: string;
|
|
25
|
-
done: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type AppState = {
|
|
29
|
-
user: {
|
|
30
|
-
name: string;
|
|
31
|
-
stats: {
|
|
32
|
-
counter: number;
|
|
33
|
-
lastUpdated: number | null; // Be specific with types
|
|
34
|
-
},
|
|
35
|
-
age: number;
|
|
36
|
-
online: boolean;
|
|
37
|
-
};
|
|
38
|
-
todos: Todo[]; // Use the interface here
|
|
39
|
-
settings: {
|
|
40
|
-
darkMode: boolean;
|
|
41
|
-
notifications: boolean;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// 1. Define your initial state structure
|
|
45
|
-
const initialState: AppState = {
|
|
46
|
-
user: {
|
|
47
|
-
name: "John",
|
|
48
|
-
stats: {
|
|
49
|
-
counter: 0,
|
|
50
|
-
lastUpdated: null
|
|
51
|
-
},
|
|
52
|
-
age: 30,
|
|
53
|
-
online:false
|
|
54
|
-
},
|
|
55
|
-
todos: [],
|
|
56
|
-
settings: {
|
|
57
|
-
darkMode: false,
|
|
58
|
-
notifications: true
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// 2. Create your state manager
|
|
63
|
-
const { useCogsState } = createCogsState(initialState);
|
|
64
|
-
|
|
65
|
-
// 3. Use in components - access specific state slices by their keys
|
|
66
|
-
function UserComponent() {
|
|
67
|
-
const user = useCogsState('user'); // Access the 'user' slice
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div>
|
|
71
|
-
<p>Name: {user.name.$get()}</p>
|
|
72
|
-
<p>Counter: {user.stats.counter.$get()}</p>
|
|
73
|
-
<button onClick={() => user.stats.counter.$update(prev => prev + 1)}>
|
|
74
|
-
Increment Counter
|
|
75
|
-
</button>
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function TodoComponent() {
|
|
81
|
-
const todos = useCogsState('todos'); // Access the 'todos' slice
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div>
|
|
85
|
-
<p>Todo count: {todos.$get().length}</p>
|
|
86
|
-
<button onClick={() => todos.$insert({ id: Date.now(), text: 'New todo', done: false })}>
|
|
87
|
-
Add Todo
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Core Concepts
|
|
95
|
-
|
|
96
|
-
### State Access Patterns
|
|
97
|
-
|
|
98
|
-
Every state property gets these core methods:
|
|
99
|
-
|
|
100
|
-
#### Primitives (strings, numbers, booleans)
|
|
101
|
-
|
|
102
|
-
- `.$get()` - read values reactively
|
|
103
|
-
- `.$update()` - set values
|
|
104
|
-
- `.$toggle()` - flip booleans
|
|
105
|
-
- `.$$get()` - non-reactive read (signals)
|
|
106
|
-
- `.$$derive()` - computed signals
|
|
107
|
-
|
|
108
|
-
#### Objects
|
|
109
|
-
|
|
110
|
-
- All primitive methods plus access to nested properties
|
|
111
|
-
- `.$update()` can do partial updates
|
|
112
|
-
|
|
113
|
-
#### Arrays
|
|
114
|
-
|
|
115
|
-
- All core methods plus array-specific operations
|
|
116
|
-
- Built-in selection tracking and metadata
|
|
117
|
-
|
|
118
|
-
### Reading State
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const user = useCogsState('user');
|
|
122
|
-
const todos = useCogsState('todos');
|
|
123
|
-
const settings = useCogsState('settings');
|
|
124
|
-
|
|
125
|
-
// Reactive reads (triggers re-renders)
|
|
126
|
-
const userName = user.name.$get();
|
|
127
|
-
const allTodos = todos.$get();
|
|
128
|
-
const isDarkMode = settings.darkMode.$get();
|
|
129
|
-
|
|
130
|
-
// Access nested properties
|
|
131
|
-
const counterValue = user.stats.counter.$get();
|
|
132
|
-
const firstTodo = todos.$index(0)?.$get();
|
|
133
|
-
|
|
134
|
-
// Non-reactive reads (no re-renders, for signals)
|
|
135
|
-
const userNameStatic = user.name.$$get();
|
|
136
|
-
|
|
137
|
-
// Computed signals (transforms value without re-renders)
|
|
138
|
-
const todoCount = todos.$$derive((todos) => todos.length);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Updating State
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
const user = useCogsState('user');
|
|
145
|
-
const settings = useCogsState('settings');
|
|
146
|
-
const todos = useCogsState('todos');
|
|
147
|
-
|
|
148
|
-
// Direct updates
|
|
149
|
-
user.name.$update('Jane');
|
|
150
|
-
settings.darkMode.$toggle();
|
|
151
|
-
|
|
152
|
-
// Functional updates
|
|
153
|
-
user.stats.counter.$update((prev) => prev + 1);
|
|
154
|
-
|
|
155
|
-
// Object updates
|
|
156
|
-
user.$update((prev) => ({ ...prev, name: 'Jane', age: 30 }));
|
|
157
|
-
|
|
158
|
-
// Deep nested updates
|
|
159
|
-
todos.$index(0).text.$update('Updated todo text');
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Working with Arrays
|
|
163
|
-
|
|
164
|
-
Arrays are first-class citizens with powerful built-in operations:
|
|
165
|
-
|
|
166
|
-
### Basic Array Operations
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
const todos = useCogsState('todos');
|
|
170
|
-
|
|
171
|
-
// Add items
|
|
172
|
-
todos.$insert({ id: 'uuid', text: 'New todo', done: false });
|
|
173
|
-
todos.$insert(({ uuid }) => ({
|
|
174
|
-
id: uuid,
|
|
175
|
-
text: 'Auto-generated ID',
|
|
176
|
-
done: false,
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
// Remove items
|
|
180
|
-
todos.$cut(2); // Remove at index 2
|
|
181
|
-
todos.$cutSelected(); // Remove currently selected item
|
|
182
|
-
|
|
183
|
-
// Access items
|
|
184
|
-
const firstTodo = todos.$index(0);
|
|
185
|
-
const lastTodo = todos.$last();
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Array Iteration and Rendering
|
|
189
|
-
|
|
190
|
-
#### `$map()` - Enhanced Array Mapping
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
const todos = useCogsState('todos');
|
|
194
|
-
|
|
195
|
-
// Returns transformed array, each item is a full state object
|
|
196
|
-
const todoElements = todos.$map((todoState, index, arrayState) => (
|
|
197
|
-
<TodoItem
|
|
198
|
-
key={todoState.id.$get()}
|
|
199
|
-
todo={todoState}
|
|
200
|
-
onToggle={() => todoState.done.$toggle()}
|
|
201
|
-
onDelete={() => arrayState.$cut(index)}
|
|
202
|
-
/>
|
|
203
|
-
));
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
#### `$list()` - JSX List Rendering
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
const todos = useCogsState('todos');
|
|
210
|
-
|
|
211
|
-
// Renders directly in place with automatic key management
|
|
212
|
-
{todos.$list((todoState, index, arrayState) => (
|
|
213
|
-
<div key={todoState.id.$get()}>
|
|
214
|
-
<span>{todoState.text.$get()}</span>
|
|
215
|
-
<button onClick={() => todoState.done.$toggle()}>Toggle</button>
|
|
216
|
-
<button onClick={() => arrayState.$cut(index)}>Delete</button>
|
|
217
|
-
</div>
|
|
218
|
-
))}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Advanced Array Methods
|
|
222
|
-
|
|
223
|
-
#### Filtering and Sorting
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
const todos = useCogsState('todos');
|
|
227
|
-
|
|
228
|
-
// Filter items (returns new state object with filtered view)
|
|
229
|
-
const completedTodos = todos.$filter((todo) => todo.done);
|
|
230
|
-
const incompleteTodos = todos.$filter((todo) => !todo.done);
|
|
231
|
-
|
|
232
|
-
// Sort items (returns new state object with sorted view)
|
|
233
|
-
const sortedTodos = todos.$sort((a, b) => a.text.localeCompare(b.text));
|
|
234
|
-
|
|
235
|
-
// Chain operations
|
|
236
|
-
const sortedCompletedTodos = todos
|
|
237
|
-
.$filter((todo) => todo.done)
|
|
238
|
-
.$sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### Finding and Searching
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
const todos = useCogsState('todos');
|
|
245
|
-
|
|
246
|
-
// Find by property value
|
|
247
|
-
const todoById = todos.$findWith('id', 'some-id');
|
|
248
|
-
if (todoById) {
|
|
249
|
-
todoById.text.$update('Updated text');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Find with custom function
|
|
253
|
-
const firstIncompleteTodo = todos.$find((todo) => !todo.done);
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
#### Unique Operations
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
const todos = useCogsState('todos');
|
|
260
|
-
|
|
261
|
-
// Insert only if unique (prevents duplicates)
|
|
262
|
-
todos.$uniqueInsert(
|
|
263
|
-
{ id: 'new-id', text: 'New todo', done: false },
|
|
264
|
-
['id'], // Fields to check for uniqueness
|
|
265
|
-
(existingItem) => {
|
|
266
|
-
// Optional: callback if match found
|
|
267
|
-
return { ...existingItem, text: 'Updated existing' };
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
#### Selection Management
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
const todos = useCogsState('todos');
|
|
276
|
-
|
|
277
|
-
// Built-in selection tracking
|
|
278
|
-
const selectedTodo = todos.$getSelected();
|
|
279
|
-
const selectedIndex = todos.$getSelectedIndex();
|
|
280
|
-
|
|
281
|
-
// Set selection on individual items
|
|
282
|
-
todos.$index(0).$setSelected(true);
|
|
283
|
-
todos.$index(0).$toggleSelected();
|
|
284
|
-
|
|
285
|
-
// Clear all selections
|
|
286
|
-
todos.$clearSelected();
|
|
287
|
-
|
|
288
|
-
// Check if item is selected
|
|
289
|
-
const isSelected = todos.$index(0).isSelected;
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## Plugin Chain Methods
|
|
293
|
-
|
|
294
|
-
Plugins can add custom methods to the state builder chain with `.methods(...)`.
|
|
295
|
-
This is useful for reusable behaviours such as uploads, persistence, analytics,
|
|
296
|
-
or domain-specific state actions.
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { createCogsState, createPluginContext } from 'cogsbox-state';
|
|
300
|
-
import { z } from 'zod';
|
|
301
|
-
|
|
302
|
-
const { createPlugin } = createPluginContext({
|
|
303
|
-
options: z.object({
|
|
304
|
-
bucketPrefix: z.string().optional(),
|
|
305
|
-
}),
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const s3Plugin = createPlugin('s3').methods(({ path, array }) => ({
|
|
309
|
-
sendToS3: path((state) => state.users.$.profile.image)(
|
|
310
|
-
async (ctx, bucket: string) => {
|
|
311
|
-
return upload(ctx.$get(), bucket);
|
|
312
|
-
}
|
|
313
|
-
),
|
|
314
|
-
|
|
315
|
-
sendManyToS3: array(async (ctx, bucket: string) => {
|
|
316
|
-
return uploadMany(ctx.$get(), bucket);
|
|
317
|
-
}),
|
|
318
|
-
|
|
319
|
-
sendGalleryToS3: path((state) => state.galleries.$.images).array(
|
|
320
|
-
async (ctx, bucket: string) => {
|
|
321
|
-
return uploadMany(ctx.$get(), bucket, ctx.options?.bucketPrefix);
|
|
322
|
-
}
|
|
323
|
-
),
|
|
324
|
-
}));
|
|
325
|
-
|
|
326
|
-
const { useCogsState } = createCogsState(initialState, {
|
|
327
|
-
plugins: [s3Plugin],
|
|
328
|
-
});
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Use the plugin by passing its options to `useCogsState`:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
const assets = useCogsState('assets', {
|
|
335
|
-
s3: { bucketPrefix: 'uploads' },
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
await assets.users.$index(0).profile.image.sendToS3('avatars');
|
|
339
|
-
await assets.galleries.$index(0).images.sendGalleryToS3('gallery');
|
|
340
|
-
await assets.looseImages.sendManyToS3('images');
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### Method Targets
|
|
344
|
-
|
|
345
|
-
Method helpers decide where a method can run:
|
|
346
|
-
|
|
347
|
-
- `field(fn)` - any state node
|
|
348
|
-
- `array(fn)` - arrays only
|
|
349
|
-
- `object(fn)` - plain objects only
|
|
350
|
-
- `primitive(fn)` - strings, numbers, booleans, `null`, etc.
|
|
351
|
-
- `boolean(fn)` - booleans only
|
|
352
|
-
- `path(selector)(fn)` - only a matching path
|
|
353
|
-
- `path(selector).array(fn)` - matching path and array value
|
|
354
|
-
|
|
355
|
-
The `path` helper receives a path-recorder proxy. The `$` segment means "one
|
|
356
|
-
path segment", which naturally matches array item ids internally:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
path((state) => state.users.$.profile.image);
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
matches calls like:
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
assets.users.$index(0).profile.image.sendToS3('avatars');
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
The first handler argument is a scoped plugin context. The remaining handler
|
|
369
|
-
arguments become the chain method arguments, and the return type is preserved:
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
const imagePlugin = createPlugin('images').methods(({ field }) => ({
|
|
373
|
-
resize: field((ctx, width: number, height: number) => {
|
|
374
|
-
return resizeImage(ctx.$get(), width, height);
|
|
375
|
-
}),
|
|
376
|
-
}));
|
|
377
|
-
|
|
378
|
-
// typed as resize(width: number, height: number): ReturnType<typeof resizeImage>
|
|
379
|
-
state.avatar.resize(200, 200);
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
If a real state field has the same name as a plugin method, the state field wins.
|
|
383
|
-
|
|
384
|
-
<!-- ### Virtualization for Large Lists
|
|
385
|
-
|
|
386
|
-
For performance with large datasets:
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
function MessageList() {
|
|
390
|
-
const messages = useCogsState('messages', { reactiveType: 'none' });
|
|
391
|
-
|
|
392
|
-
const { virtualState, virtualizerProps, scrollToBottom } =
|
|
393
|
-
messages.useVirtualView({
|
|
394
|
-
itemHeight: 65, // Height per item
|
|
395
|
-
overscan: 10, // Items to render outside viewport
|
|
396
|
-
stickToBottom: true, // Auto-scroll to bottom
|
|
397
|
-
scrollStickTolerance: 75 // Distance tolerance for bottom detection
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
return (
|
|
401
|
-
<div {...virtualizerProps.outer} className="h-96 overflow-auto">
|
|
402
|
-
<div style={virtualizerProps.inner.style}>
|
|
403
|
-
<div style={virtualizerProps.list.style}>
|
|
404
|
-
{virtualState.list((messageState, index) => (
|
|
405
|
-
<MessageItem key={messageState.id.$get()} message={messageState} />
|
|
406
|
-
))}
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
``` -->
|
|
413
|
-
|
|
414
|
-
## Reactivity Control
|
|
415
|
-
|
|
416
|
-
Cogsbox offers different reactivity modes for performance optimization:
|
|
417
|
-
|
|
418
|
-
### Component Reactivity (Default)
|
|
419
|
-
|
|
420
|
-
```typescript
|
|
421
|
-
// Re-renders when any accessed state changes
|
|
422
|
-
const user = useCogsState('user');
|
|
423
|
-
// or explicitly:
|
|
424
|
-
const user = useCogsState('user', { reactiveType: 'component' });
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Dependency-Based Reactivity
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
// Only re-renders when specified dependencies change
|
|
431
|
-
const user = useCogsState('user', {
|
|
432
|
-
reactiveType: 'deps',
|
|
433
|
-
reactiveDeps: (state) => [state.name, state.stats.counter],
|
|
434
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Full Reactivity
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
// Re-renders on ANY change to the state slice
|
|
441
|
-
const user = useCogsState('user', { reactiveType: 'all' });
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### No Reactivity
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
// Never re-renders (useful with signals)
|
|
448
|
-
const todos = useCogsState('todos', { reactiveType: 'none' });
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### Multiple Reactivity Types
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
// Combine multiple reactivity modes
|
|
455
|
-
const user = useCogsState('user', {
|
|
456
|
-
reactiveType: ['component', 'deps'],
|
|
457
|
-
reactiveDeps: (state) => [state.online],
|
|
458
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
## Form Management
|
|
462
|
-
|
|
463
|
-
Cogsbox excels at form handling with automatic debouncing and validation:
|
|
464
|
-
|
|
465
|
-
### Basic Form Elements
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
import { createCogsState } from 'cogsbox-state';
|
|
469
|
-
import { z } from 'zod';
|
|
470
|
-
|
|
471
|
-
// 1. Define the validation schema. This is your single source of truth.
|
|
472
|
-
const userSchema = z.object({
|
|
473
|
-
name: z.string().min(1, "Name is required"),
|
|
474
|
-
email: z.string().email("Invalid email"),
|
|
475
|
-
age: z.number().min(18, "Must be 18+")
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
// 2. Best Practice: Infer the TypeScript type directly from the schema.
|
|
479
|
-
type UserFormData = z.infer<typeof userSchema>;
|
|
480
|
-
|
|
481
|
-
// 3. Define the initial state for the form using the inferred type.
|
|
482
|
-
const initialUserFormState: UserFormData = {
|
|
483
|
-
name: "",
|
|
484
|
-
email: "",
|
|
485
|
-
age: 18
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
// 4. Create the state manager.
|
|
489
|
-
const { useCogsState } = createCogsState({
|
|
490
|
-
userForm: {
|
|
491
|
-
initialState: initialUserFormState,
|
|
492
|
-
validation: {
|
|
493
|
-
key: "userValidation",
|
|
494
|
-
zodSchemaV4: userSchema, // Pass the schema for runtime validation
|
|
495
|
-
onBlur: 'error'
|
|
496
|
-
},
|
|
497
|
-
formElements: {
|
|
498
|
-
validation: ({ children, hasErrors, message }) => (
|
|
499
|
-
<div className="form-field">
|
|
500
|
-
{children}
|
|
501
|
-
{hasErrors && <span className="error">{message}</span>}
|
|
502
|
-
</div>
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
function UserForm() {
|
|
509
|
-
const userForm = useCogsState('userForm');
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<form>
|
|
513
|
-
{/* Auto-debounced input with validation wrapper */}
|
|
514
|
-
{userForm.name.$formElement(({ $inputProps }) => (
|
|
515
|
-
<>
|
|
516
|
-
<label>Name</label>
|
|
517
|
-
<input {...$inputProps} />
|
|
518
|
-
</>
|
|
519
|
-
))}
|
|
520
|
-
|
|
521
|
-
{/* Custom debounce time */}
|
|
522
|
-
{userForm.email.$formElement(({ $inputProps }) => (
|
|
523
|
-
<>
|
|
524
|
-
<label>Email</label>
|
|
525
|
-
<input {...$inputProps} />
|
|
526
|
-
</>
|
|
527
|
-
), { debounceTime: 500 })}
|
|
528
|
-
|
|
529
|
-
{/* Custom form control */}
|
|
530
|
-
{userForm.age.$formElement(({ $get, $update }) => (
|
|
531
|
-
<>
|
|
532
|
-
<label>Age</label>
|
|
533
|
-
<input
|
|
534
|
-
type="number"
|
|
535
|
-
value={$get()}
|
|
536
|
-
onChange={e => $update(parseInt(e.target.value))}
|
|
537
|
-
/>
|
|
538
|
-
</>
|
|
539
|
-
))}
|
|
540
|
-
</form>
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
```
|
|
1
|
+
# Cogsbox State
|
|
2
|
+
|
|
3
|
+
> **🚨 DANGER: DO NOT USE - UNSTABLE & EXPERIMENTAL 🚨**
|
|
4
|
+
>
|
|
5
|
+
> This library is in early development and constantly changing.
|
|
6
|
+
>
|
|
7
|
+
> **DO NOT USE IN ANY PROJECT YET - ONLY FOR TESTING AND PROVIDING FEEDBACK.**
|
|
8
|
+
|
|
9
|
+
## What is Cogsbox State?
|
|
10
|
+
|
|
11
|
+
Cogsbox State is a React state management library that creates a **nested state builder** - a type-safe proxy that mimics your initial state structure. Every property in your state becomes a powerful state object with built-in methods for updates, arrays, forms, and more.
|
|
12
|
+
|
|
13
|
+
**Key Philosophy**: Instead of complex useState drilling and manual mapping, you directly access nested properties and use built-in methods.
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Basic Setup
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createCogsState } from 'cogsbox-state';
|
|
21
|
+
|
|
22
|
+
type Todo = {
|
|
23
|
+
id: number;
|
|
24
|
+
text: string;
|
|
25
|
+
done: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type AppState = {
|
|
29
|
+
user: {
|
|
30
|
+
name: string;
|
|
31
|
+
stats: {
|
|
32
|
+
counter: number;
|
|
33
|
+
lastUpdated: number | null; // Be specific with types
|
|
34
|
+
},
|
|
35
|
+
age: number;
|
|
36
|
+
online: boolean;
|
|
37
|
+
};
|
|
38
|
+
todos: Todo[]; // Use the interface here
|
|
39
|
+
settings: {
|
|
40
|
+
darkMode: boolean;
|
|
41
|
+
notifications: boolean;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 1. Define your initial state structure
|
|
45
|
+
const initialState: AppState = {
|
|
46
|
+
user: {
|
|
47
|
+
name: "John",
|
|
48
|
+
stats: {
|
|
49
|
+
counter: 0,
|
|
50
|
+
lastUpdated: null
|
|
51
|
+
},
|
|
52
|
+
age: 30,
|
|
53
|
+
online:false
|
|
54
|
+
},
|
|
55
|
+
todos: [],
|
|
56
|
+
settings: {
|
|
57
|
+
darkMode: false,
|
|
58
|
+
notifications: true
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 2. Create your state manager
|
|
63
|
+
const { useCogsState } = createCogsState(initialState);
|
|
64
|
+
|
|
65
|
+
// 3. Use in components - access specific state slices by their keys
|
|
66
|
+
function UserComponent() {
|
|
67
|
+
const user = useCogsState('user'); // Access the 'user' slice
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div>
|
|
71
|
+
<p>Name: {user.name.$get()}</p>
|
|
72
|
+
<p>Counter: {user.stats.counter.$get()}</p>
|
|
73
|
+
<button onClick={() => user.stats.counter.$update(prev => prev + 1)}>
|
|
74
|
+
Increment Counter
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function TodoComponent() {
|
|
81
|
+
const todos = useCogsState('todos'); // Access the 'todos' slice
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
<p>Todo count: {todos.$get().length}</p>
|
|
86
|
+
<button onClick={() => todos.$insert({ id: Date.now(), text: 'New todo', done: false })}>
|
|
87
|
+
Add Todo
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Core Concepts
|
|
95
|
+
|
|
96
|
+
### State Access Patterns
|
|
97
|
+
|
|
98
|
+
Every state property gets these core methods:
|
|
99
|
+
|
|
100
|
+
#### Primitives (strings, numbers, booleans)
|
|
101
|
+
|
|
102
|
+
- `.$get()` - read values reactively
|
|
103
|
+
- `.$update()` - set values
|
|
104
|
+
- `.$toggle()` - flip booleans
|
|
105
|
+
- `.$$get()` - non-reactive read (signals)
|
|
106
|
+
- `.$$derive()` - computed signals
|
|
107
|
+
|
|
108
|
+
#### Objects
|
|
109
|
+
|
|
110
|
+
- All primitive methods plus access to nested properties
|
|
111
|
+
- `.$update()` can do partial updates
|
|
112
|
+
|
|
113
|
+
#### Arrays
|
|
114
|
+
|
|
115
|
+
- All core methods plus array-specific operations
|
|
116
|
+
- Built-in selection tracking and metadata
|
|
117
|
+
|
|
118
|
+
### Reading State
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const user = useCogsState('user');
|
|
122
|
+
const todos = useCogsState('todos');
|
|
123
|
+
const settings = useCogsState('settings');
|
|
124
|
+
|
|
125
|
+
// Reactive reads (triggers re-renders)
|
|
126
|
+
const userName = user.name.$get();
|
|
127
|
+
const allTodos = todos.$get();
|
|
128
|
+
const isDarkMode = settings.darkMode.$get();
|
|
129
|
+
|
|
130
|
+
// Access nested properties
|
|
131
|
+
const counterValue = user.stats.counter.$get();
|
|
132
|
+
const firstTodo = todos.$index(0)?.$get();
|
|
133
|
+
|
|
134
|
+
// Non-reactive reads (no re-renders, for signals)
|
|
135
|
+
const userNameStatic = user.name.$$get();
|
|
136
|
+
|
|
137
|
+
// Computed signals (transforms value without re-renders)
|
|
138
|
+
const todoCount = todos.$$derive((todos) => todos.length);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Updating State
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const user = useCogsState('user');
|
|
145
|
+
const settings = useCogsState('settings');
|
|
146
|
+
const todos = useCogsState('todos');
|
|
147
|
+
|
|
148
|
+
// Direct updates
|
|
149
|
+
user.name.$update('Jane');
|
|
150
|
+
settings.darkMode.$toggle();
|
|
151
|
+
|
|
152
|
+
// Functional updates
|
|
153
|
+
user.stats.counter.$update((prev) => prev + 1);
|
|
154
|
+
|
|
155
|
+
// Object updates
|
|
156
|
+
user.$update((prev) => ({ ...prev, name: 'Jane', age: 30 }));
|
|
157
|
+
|
|
158
|
+
// Deep nested updates
|
|
159
|
+
todos.$index(0).text.$update('Updated todo text');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Working with Arrays
|
|
163
|
+
|
|
164
|
+
Arrays are first-class citizens with powerful built-in operations:
|
|
165
|
+
|
|
166
|
+
### Basic Array Operations
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const todos = useCogsState('todos');
|
|
170
|
+
|
|
171
|
+
// Add items
|
|
172
|
+
todos.$insert({ id: 'uuid', text: 'New todo', done: false });
|
|
173
|
+
todos.$insert(({ uuid }) => ({
|
|
174
|
+
id: uuid,
|
|
175
|
+
text: 'Auto-generated ID',
|
|
176
|
+
done: false,
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
// Remove items
|
|
180
|
+
todos.$cut(2); // Remove at index 2
|
|
181
|
+
todos.$cutSelected(); // Remove currently selected item
|
|
182
|
+
|
|
183
|
+
// Access items
|
|
184
|
+
const firstTodo = todos.$index(0);
|
|
185
|
+
const lastTodo = todos.$last();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Array Iteration and Rendering
|
|
189
|
+
|
|
190
|
+
#### `$map()` - Enhanced Array Mapping
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const todos = useCogsState('todos');
|
|
194
|
+
|
|
195
|
+
// Returns transformed array, each item is a full state object
|
|
196
|
+
const todoElements = todos.$map((todoState, index, arrayState) => (
|
|
197
|
+
<TodoItem
|
|
198
|
+
key={todoState.id.$get()}
|
|
199
|
+
todo={todoState}
|
|
200
|
+
onToggle={() => todoState.done.$toggle()}
|
|
201
|
+
onDelete={() => arrayState.$cut(index)}
|
|
202
|
+
/>
|
|
203
|
+
));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `$list()` - JSX List Rendering
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const todos = useCogsState('todos');
|
|
210
|
+
|
|
211
|
+
// Renders directly in place with automatic key management
|
|
212
|
+
{todos.$list((todoState, index, arrayState) => (
|
|
213
|
+
<div key={todoState.id.$get()}>
|
|
214
|
+
<span>{todoState.text.$get()}</span>
|
|
215
|
+
<button onClick={() => todoState.done.$toggle()}>Toggle</button>
|
|
216
|
+
<button onClick={() => arrayState.$cut(index)}>Delete</button>
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Advanced Array Methods
|
|
222
|
+
|
|
223
|
+
#### Filtering and Sorting
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const todos = useCogsState('todos');
|
|
227
|
+
|
|
228
|
+
// Filter items (returns new state object with filtered view)
|
|
229
|
+
const completedTodos = todos.$filter((todo) => todo.done);
|
|
230
|
+
const incompleteTodos = todos.$filter((todo) => !todo.done);
|
|
231
|
+
|
|
232
|
+
// Sort items (returns new state object with sorted view)
|
|
233
|
+
const sortedTodos = todos.$sort((a, b) => a.text.localeCompare(b.text));
|
|
234
|
+
|
|
235
|
+
// Chain operations
|
|
236
|
+
const sortedCompletedTodos = todos
|
|
237
|
+
.$filter((todo) => todo.done)
|
|
238
|
+
.$sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Finding and Searching
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const todos = useCogsState('todos');
|
|
245
|
+
|
|
246
|
+
// Find by property value
|
|
247
|
+
const todoById = todos.$findWith('id', 'some-id');
|
|
248
|
+
if (todoById) {
|
|
249
|
+
todoById.text.$update('Updated text');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Find with custom function
|
|
253
|
+
const firstIncompleteTodo = todos.$find((todo) => !todo.done);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Unique Operations
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const todos = useCogsState('todos');
|
|
260
|
+
|
|
261
|
+
// Insert only if unique (prevents duplicates)
|
|
262
|
+
todos.$uniqueInsert(
|
|
263
|
+
{ id: 'new-id', text: 'New todo', done: false },
|
|
264
|
+
['id'], // Fields to check for uniqueness
|
|
265
|
+
(existingItem) => {
|
|
266
|
+
// Optional: callback if match found
|
|
267
|
+
return { ...existingItem, text: 'Updated existing' };
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Selection Management
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const todos = useCogsState('todos');
|
|
276
|
+
|
|
277
|
+
// Built-in selection tracking
|
|
278
|
+
const selectedTodo = todos.$getSelected();
|
|
279
|
+
const selectedIndex = todos.$getSelectedIndex();
|
|
280
|
+
|
|
281
|
+
// Set selection on individual items
|
|
282
|
+
todos.$index(0).$setSelected(true);
|
|
283
|
+
todos.$index(0).$toggleSelected();
|
|
284
|
+
|
|
285
|
+
// Clear all selections
|
|
286
|
+
todos.$clearSelected();
|
|
287
|
+
|
|
288
|
+
// Check if item is selected
|
|
289
|
+
const isSelected = todos.$index(0).isSelected;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Plugin Chain Methods
|
|
293
|
+
|
|
294
|
+
Plugins can add custom methods to the state builder chain with `.methods(...)`.
|
|
295
|
+
This is useful for reusable behaviours such as uploads, persistence, analytics,
|
|
296
|
+
or domain-specific state actions.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { createCogsState, createPluginContext } from 'cogsbox-state';
|
|
300
|
+
import { z } from 'zod';
|
|
301
|
+
|
|
302
|
+
const { createPlugin } = createPluginContext({
|
|
303
|
+
options: z.object({
|
|
304
|
+
bucketPrefix: z.string().optional(),
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const s3Plugin = createPlugin('s3').methods(({ path, array }) => ({
|
|
309
|
+
sendToS3: path((state) => state.users.$.profile.image)(
|
|
310
|
+
async (ctx, bucket: string) => {
|
|
311
|
+
return upload(ctx.$get(), bucket);
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
|
|
315
|
+
sendManyToS3: array(async (ctx, bucket: string) => {
|
|
316
|
+
return uploadMany(ctx.$get(), bucket);
|
|
317
|
+
}),
|
|
318
|
+
|
|
319
|
+
sendGalleryToS3: path((state) => state.galleries.$.images).array(
|
|
320
|
+
async (ctx, bucket: string) => {
|
|
321
|
+
return uploadMany(ctx.$get(), bucket, ctx.options?.bucketPrefix);
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
}));
|
|
325
|
+
|
|
326
|
+
const { useCogsState } = createCogsState(initialState, {
|
|
327
|
+
plugins: [s3Plugin],
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Use the plugin by passing its options to `useCogsState`:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const assets = useCogsState('assets', {
|
|
335
|
+
s3: { bucketPrefix: 'uploads' },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await assets.users.$index(0).profile.image.sendToS3('avatars');
|
|
339
|
+
await assets.galleries.$index(0).images.sendGalleryToS3('gallery');
|
|
340
|
+
await assets.looseImages.sendManyToS3('images');
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Method Targets
|
|
344
|
+
|
|
345
|
+
Method helpers decide where a method can run:
|
|
346
|
+
|
|
347
|
+
- `field(fn)` - any state node
|
|
348
|
+
- `array(fn)` - arrays only
|
|
349
|
+
- `object(fn)` - plain objects only
|
|
350
|
+
- `primitive(fn)` - strings, numbers, booleans, `null`, etc.
|
|
351
|
+
- `boolean(fn)` - booleans only
|
|
352
|
+
- `path(selector)(fn)` - only a matching path
|
|
353
|
+
- `path(selector).array(fn)` - matching path and array value
|
|
354
|
+
|
|
355
|
+
The `path` helper receives a path-recorder proxy. The `$` segment means "one
|
|
356
|
+
path segment", which naturally matches array item ids internally:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
path((state) => state.users.$.profile.image);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
matches calls like:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
assets.users.$index(0).profile.image.sendToS3('avatars');
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The first handler argument is a scoped plugin context. The remaining handler
|
|
369
|
+
arguments become the chain method arguments, and the return type is preserved:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
const imagePlugin = createPlugin('images').methods(({ field }) => ({
|
|
373
|
+
resize: field((ctx, width: number, height: number) => {
|
|
374
|
+
return resizeImage(ctx.$get(), width, height);
|
|
375
|
+
}),
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
// typed as resize(width: number, height: number): ReturnType<typeof resizeImage>
|
|
379
|
+
state.avatar.resize(200, 200);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
If a real state field has the same name as a plugin method, the state field wins.
|
|
383
|
+
|
|
384
|
+
<!-- ### Virtualization for Large Lists
|
|
385
|
+
|
|
386
|
+
For performance with large datasets:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
function MessageList() {
|
|
390
|
+
const messages = useCogsState('messages', { reactiveType: 'none' });
|
|
391
|
+
|
|
392
|
+
const { virtualState, virtualizerProps, scrollToBottom } =
|
|
393
|
+
messages.useVirtualView({
|
|
394
|
+
itemHeight: 65, // Height per item
|
|
395
|
+
overscan: 10, // Items to render outside viewport
|
|
396
|
+
stickToBottom: true, // Auto-scroll to bottom
|
|
397
|
+
scrollStickTolerance: 75 // Distance tolerance for bottom detection
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<div {...virtualizerProps.outer} className="h-96 overflow-auto">
|
|
402
|
+
<div style={virtualizerProps.inner.style}>
|
|
403
|
+
<div style={virtualizerProps.list.style}>
|
|
404
|
+
{virtualState.list((messageState, index) => (
|
|
405
|
+
<MessageItem key={messageState.id.$get()} message={messageState} />
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
``` -->
|
|
413
|
+
|
|
414
|
+
## Reactivity Control
|
|
415
|
+
|
|
416
|
+
Cogsbox offers different reactivity modes for performance optimization:
|
|
417
|
+
|
|
418
|
+
### Component Reactivity (Default)
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// Re-renders when any accessed state changes
|
|
422
|
+
const user = useCogsState('user');
|
|
423
|
+
// or explicitly:
|
|
424
|
+
const user = useCogsState('user', { reactiveType: 'component' });
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Dependency-Based Reactivity
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// Only re-renders when specified dependencies change
|
|
431
|
+
const user = useCogsState('user', {
|
|
432
|
+
reactiveType: 'deps',
|
|
433
|
+
reactiveDeps: (state) => [state.name, state.stats.counter],
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Full Reactivity
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// Re-renders on ANY change to the state slice
|
|
441
|
+
const user = useCogsState('user', { reactiveType: 'all' });
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### No Reactivity
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Never re-renders (useful with signals)
|
|
448
|
+
const todos = useCogsState('todos', { reactiveType: 'none' });
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Multiple Reactivity Types
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// Combine multiple reactivity modes
|
|
455
|
+
const user = useCogsState('user', {
|
|
456
|
+
reactiveType: ['component', 'deps'],
|
|
457
|
+
reactiveDeps: (state) => [state.online],
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Form Management
|
|
462
|
+
|
|
463
|
+
Cogsbox excels at form handling with automatic debouncing and validation:
|
|
464
|
+
|
|
465
|
+
### Basic Form Elements
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { createCogsState } from 'cogsbox-state';
|
|
469
|
+
import { z } from 'zod';
|
|
470
|
+
|
|
471
|
+
// 1. Define the validation schema. This is your single source of truth.
|
|
472
|
+
const userSchema = z.object({
|
|
473
|
+
name: z.string().min(1, "Name is required"),
|
|
474
|
+
email: z.string().email("Invalid email"),
|
|
475
|
+
age: z.number().min(18, "Must be 18+")
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// 2. Best Practice: Infer the TypeScript type directly from the schema.
|
|
479
|
+
type UserFormData = z.infer<typeof userSchema>;
|
|
480
|
+
|
|
481
|
+
// 3. Define the initial state for the form using the inferred type.
|
|
482
|
+
const initialUserFormState: UserFormData = {
|
|
483
|
+
name: "",
|
|
484
|
+
email: "",
|
|
485
|
+
age: 18
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// 4. Create the state manager.
|
|
489
|
+
const { useCogsState } = createCogsState({
|
|
490
|
+
userForm: {
|
|
491
|
+
initialState: initialUserFormState,
|
|
492
|
+
validation: {
|
|
493
|
+
key: "userValidation",
|
|
494
|
+
zodSchemaV4: userSchema, // Pass the schema for runtime validation
|
|
495
|
+
onBlur: 'error'
|
|
496
|
+
},
|
|
497
|
+
formElements: {
|
|
498
|
+
validation: ({ children, hasErrors, message }) => (
|
|
499
|
+
<div className="form-field">
|
|
500
|
+
{children}
|
|
501
|
+
{hasErrors && <span className="error">{message}</span>}
|
|
502
|
+
</div>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
function UserForm() {
|
|
509
|
+
const userForm = useCogsState('userForm');
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<form>
|
|
513
|
+
{/* Auto-debounced input with validation wrapper */}
|
|
514
|
+
{userForm.name.$formElement(({ $inputProps }) => (
|
|
515
|
+
<>
|
|
516
|
+
<label>Name</label>
|
|
517
|
+
<input {...$inputProps} />
|
|
518
|
+
</>
|
|
519
|
+
))}
|
|
520
|
+
|
|
521
|
+
{/* Custom debounce time */}
|
|
522
|
+
{userForm.email.$formElement(({ $inputProps }) => (
|
|
523
|
+
<>
|
|
524
|
+
<label>Email</label>
|
|
525
|
+
<input {...$inputProps} />
|
|
526
|
+
</>
|
|
527
|
+
), { debounceTime: 500 })}
|
|
528
|
+
|
|
529
|
+
{/* Custom form control */}
|
|
530
|
+
{userForm.age.$formElement(({ $get, $update }) => (
|
|
531
|
+
<>
|
|
532
|
+
<label>Age</label>
|
|
533
|
+
<input
|
|
534
|
+
type="number"
|
|
535
|
+
value={$get()}
|
|
536
|
+
onChange={e => $update(parseInt(e.target.value))}
|
|
537
|
+
/>
|
|
538
|
+
</>
|
|
539
|
+
))}
|
|
540
|
+
</form>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
```
|