cogsbox-state 0.5.7 → 0.5.8
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 +474 -280
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,386 +1,580 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Cogsbox State: A Practical Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Getting Started
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Cogsbox State is a React state management library that provides a fluent interface for managing complex state.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Basic Setup
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```typescript
|
|
10
|
+
// 1. Define your initial state
|
|
11
|
+
const InitialState = {
|
|
12
|
+
users: [],
|
|
13
|
+
settings: {
|
|
14
|
+
darkMode: false,
|
|
15
|
+
notifications: true
|
|
16
|
+
},
|
|
17
|
+
cart: {
|
|
18
|
+
items: [],
|
|
19
|
+
total: 0
|
|
20
|
+
}
|
|
21
|
+
};
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
23
|
+
// 2. Create the state hook
|
|
24
|
+
export const { useCogsState } = createCogsState(InitialState);
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
// 3. Use in your component
|
|
27
|
+
function MyComponent() {
|
|
28
|
+
const [state, updater] = useCogsState("cart");
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
// Access values
|
|
31
|
+
const cartItems = updater.items.get();
|
|
32
|
+
const total = updater.total.get();
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
updater.
|
|
22
|
-
|
|
34
|
+
// Update values
|
|
35
|
+
const addItem = (item) => {
|
|
36
|
+
updater.items.insert(item);
|
|
37
|
+
updater.total.update(total + item.price);
|
|
38
|
+
};
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
return (
|
|
41
|
+
// Your component JSX
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
// Find, filter, and update in one chain
|
|
28
|
-
updater.items.findWith("id", "123").status.update("complete");
|
|
29
|
-
```
|
|
46
|
+
## Core Concepts
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
### Accessing State
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
```typescript
|
|
51
|
+
// Get the entire state object
|
|
52
|
+
const entireCart = updater.get();
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
// Access a specific property
|
|
55
|
+
const cartItems = updater.items.get();
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- Map with updaters
|
|
57
|
+
// Access nested properties
|
|
58
|
+
const firstItemPrice = updater.items[0].price.get();
|
|
59
|
+
```
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
### Updating State
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
```typescript
|
|
64
|
+
// Direct update
|
|
65
|
+
updater.settings.darkMode.update(true);
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
// Functional update (based on previous value)
|
|
68
|
+
updater.cart.total.update((prev) => prev + 10);
|
|
51
69
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
// Deep update
|
|
71
|
+
updater.users.findWith("id", "123").name.update("New Name");
|
|
72
|
+
```
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
- Granular updates
|
|
58
|
-
- Automatic optimization
|
|
59
|
-
- Path-based subscriptions
|
|
74
|
+
## Working with Arrays
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
### Basic Array Operations
|
|
62
77
|
|
|
63
78
|
```typescript
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
id: string;
|
|
67
|
-
name: string;
|
|
68
|
-
price: number;
|
|
69
|
-
categories: string[];
|
|
70
|
-
variants: {
|
|
71
|
-
id: string;
|
|
72
|
-
color: string;
|
|
73
|
-
size: string;
|
|
74
|
-
stock: number;
|
|
75
|
-
}[];
|
|
76
|
-
}
|
|
79
|
+
// Add an item
|
|
80
|
+
updater.cart.items.insert({ id: "prod1", name: "Product 1", price: 29.99 });
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
variantId: string;
|
|
81
|
-
quantity: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const InitialState = {
|
|
85
|
-
catalog: {
|
|
86
|
-
products: [] as Product[],
|
|
87
|
-
categories: [] as string[],
|
|
88
|
-
filters: {
|
|
89
|
-
priceRange: { min: 0, max: 100 },
|
|
90
|
-
selectedCategories: [] as string[],
|
|
91
|
-
search: "",
|
|
92
|
-
},
|
|
93
|
-
sortBy: "price_asc" as "price_asc" | "price_desc" | "name",
|
|
94
|
-
},
|
|
95
|
-
cart: {
|
|
96
|
-
items: [] as CartItem[],
|
|
97
|
-
couponCode: "",
|
|
98
|
-
shipping: {
|
|
99
|
-
address: "",
|
|
100
|
-
method: "standard" as "standard" | "express",
|
|
101
|
-
cost: 0,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
ui: {
|
|
105
|
-
sidebarOpen: false,
|
|
106
|
-
activeProductId: null as string | null,
|
|
107
|
-
notifications: [] as {
|
|
108
|
-
id: string;
|
|
109
|
-
message: string;
|
|
110
|
-
type: "success" | "error";
|
|
111
|
-
}[],
|
|
112
|
-
},
|
|
113
|
-
};
|
|
82
|
+
// Remove an item at index
|
|
83
|
+
updater.cart.items.cut(2);
|
|
114
84
|
|
|
115
|
-
//
|
|
116
|
-
|
|
85
|
+
// Find and update an item
|
|
86
|
+
updater.cart.items.findWith("id", "prod1").quantity.update((prev) => prev + 1);
|
|
117
87
|
|
|
118
|
-
//
|
|
119
|
-
|
|
88
|
+
// Update item at specific index
|
|
89
|
+
updater.cart.items.index(0).price.update(19.99);
|
|
120
90
|
```
|
|
121
91
|
|
|
122
|
-
|
|
92
|
+
### Advanced Array Methods
|
|
123
93
|
|
|
124
|
-
|
|
94
|
+
```typescript
|
|
95
|
+
// Map with access to updaters
|
|
96
|
+
updater.cart.items.stateMap((item, itemUpdater) => (
|
|
97
|
+
<CartItem
|
|
98
|
+
key={item.id}
|
|
99
|
+
item={item}
|
|
100
|
+
onQuantityChange={qty => itemUpdater.quantity.update(qty)}
|
|
101
|
+
/>
|
|
102
|
+
));
|
|
103
|
+
|
|
104
|
+
// Filter items while maintaining updater capabilities
|
|
105
|
+
const inStockItems = updater.products.stateFilter(product => product.stock > 0);
|
|
106
|
+
|
|
107
|
+
// Insert only if the item doesn't exist
|
|
108
|
+
updater.cart.items.uniqueInsert(
|
|
109
|
+
{ id: "prod1", quantity: 1 },
|
|
110
|
+
["id"] // fields to check for uniqueness
|
|
111
|
+
);
|
|
125
112
|
|
|
126
|
-
|
|
113
|
+
// Flatten nested arrays by property
|
|
114
|
+
const allVariants = updater.products.stateFlattenOn("variants");
|
|
115
|
+
```
|
|
127
116
|
|
|
128
|
-
|
|
117
|
+
## Reactivity Control
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
// Direct update
|
|
132
|
-
updater.catalog.filters.search.update("blue shoes");
|
|
119
|
+
Cogsbox offers different ways to control when components re-render:
|
|
133
120
|
|
|
134
|
-
|
|
135
|
-
updater.cart.items[0].quantity.update((prev) => prev + 1);
|
|
121
|
+
### Component Reactivity (Default)
|
|
136
122
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
123
|
+
Re-renders when any accessed value changes.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Default behavior - re-renders when cart.items or cart.total changes
|
|
127
|
+
const cart = useCogsState("cart");
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<div>Items: {cart.items.get().length}</div>
|
|
132
|
+
<div>Total: {cart.total.get()}</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
141
135
|
```
|
|
142
136
|
|
|
143
|
-
|
|
137
|
+
### Dependency-Based Reactivity
|
|
144
138
|
|
|
145
|
-
|
|
139
|
+
Re-renders only when specified dependencies change.
|
|
146
140
|
|
|
147
141
|
```typescript
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
// Only re-renders when items array or status changes
|
|
143
|
+
const cart = useCogsState("cart", {
|
|
144
|
+
reactiveType: ["deps"],
|
|
145
|
+
reactiveDeps: (state) => [state.items, state.status],
|
|
146
|
+
});
|
|
152
147
|
```
|
|
153
148
|
|
|
154
|
-
###
|
|
155
|
-
|
|
156
|
-
#### `.insert()`
|
|
149
|
+
### Full Reactivity
|
|
157
150
|
|
|
158
|
-
|
|
151
|
+
Re-renders on any state change, even for unused properties.
|
|
159
152
|
|
|
160
153
|
```typescript
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
name: "Running Shoes",
|
|
165
|
-
price: 99.99,
|
|
166
|
-
categories: ["shoes", "sports"],
|
|
167
|
-
variants: [],
|
|
154
|
+
// Re-renders on any change to cart state
|
|
155
|
+
const cart = useCogsState("cart", {
|
|
156
|
+
reactiveType: ["all"],
|
|
168
157
|
});
|
|
169
|
-
|
|
170
|
-
// Add to cart
|
|
171
|
-
updater.cart.items.insert((prev) => ({
|
|
172
|
-
productId: "123",
|
|
173
|
-
variantId: "v1",
|
|
174
|
-
quantity: 1,
|
|
175
|
-
}));
|
|
176
158
|
```
|
|
177
159
|
|
|
178
|
-
|
|
160
|
+
### Signal-Based Reactivity
|
|
179
161
|
|
|
180
|
-
|
|
162
|
+
Updates only the DOM elements that depend on changed values.
|
|
181
163
|
|
|
182
164
|
```typescript
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
id: generateId(),
|
|
190
|
-
message: "Item added to cart",
|
|
191
|
-
type: "success",
|
|
192
|
-
},
|
|
193
|
-
["id"],
|
|
165
|
+
// Most efficient - updates just the specific DOM elements
|
|
166
|
+
return (
|
|
167
|
+
<div>
|
|
168
|
+
<div>Items: {cart.items.$derive(items => items.length)}</div>
|
|
169
|
+
<div>Total: {cart.total.$get()}</div>
|
|
170
|
+
</div>
|
|
194
171
|
);
|
|
195
172
|
```
|
|
196
173
|
|
|
197
|
-
|
|
174
|
+
## Form Integration
|
|
198
175
|
|
|
199
|
-
|
|
176
|
+
Cogsbox State provides an intuitive form system to connect your state to form controls with built-in validation, error handling, and array support.
|
|
200
177
|
|
|
201
|
-
|
|
202
|
-
// Remove from cart
|
|
203
|
-
updater.cart.items.cut(index);
|
|
178
|
+
### Basic Form Element Usage
|
|
204
179
|
|
|
205
|
-
|
|
206
|
-
updater.ui.notifications.findWith("id", "notif-123").cut();
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### `.findWith()`
|
|
210
|
-
|
|
211
|
-
Finds an item in array by property comparison.
|
|
180
|
+
The `formElement` method serves as the bridge between state and UI:
|
|
212
181
|
|
|
213
182
|
```typescript
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
183
|
+
// Direct value/onChange pattern for complete control
|
|
184
|
+
user.firstName.formElement((params) => (
|
|
185
|
+
<div>
|
|
186
|
+
<label className="block text-sm font-medium">First Name</label>
|
|
187
|
+
<input
|
|
188
|
+
type="text"
|
|
189
|
+
className="mt-1 block w-full rounded-md border-2 p-2"
|
|
190
|
+
value={params.get()}
|
|
191
|
+
onChange={(e) => params.set(e.target.value)}
|
|
192
|
+
onBlur={params.inputProps.onBlur}
|
|
193
|
+
ref={params.inputProps.ref}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
));
|
|
197
|
+
|
|
198
|
+
// Using inputProps shorthand for simpler binding
|
|
199
|
+
user.lastName.formElement((params) => (
|
|
200
|
+
<div>
|
|
201
|
+
<label className="block text-sm font-medium">Last Name</label>
|
|
202
|
+
<input
|
|
203
|
+
type="text"
|
|
204
|
+
className="mt-1 block w-full rounded-md border-2 p-2"
|
|
205
|
+
{...params.inputProps}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
));
|
|
221
209
|
```
|
|
222
210
|
|
|
223
|
-
|
|
211
|
+
### Form Validation Options
|
|
224
212
|
|
|
225
|
-
|
|
213
|
+
Cogsbox provides several approaches to validation:
|
|
226
214
|
|
|
227
215
|
```typescript
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
216
|
+
// Custom validation message
|
|
217
|
+
user.email.formElement(
|
|
218
|
+
(params) => (
|
|
219
|
+
<div>
|
|
220
|
+
<label>Email Address</label>
|
|
221
|
+
<input {...params.inputProps} type="email" />
|
|
222
|
+
</div>
|
|
223
|
+
),
|
|
224
|
+
{
|
|
225
|
+
validation: {
|
|
226
|
+
message: "Please enter a valid email address"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
233
230
|
|
|
234
|
-
|
|
231
|
+
// Hidden validation (show border but no message)
|
|
232
|
+
user.lastName.formElement(
|
|
233
|
+
(params) => (
|
|
234
|
+
<div>
|
|
235
|
+
<label>Last Name</label>
|
|
236
|
+
<input
|
|
237
|
+
{...params.inputProps}
|
|
238
|
+
className={`input ${params.validationErrors().length > 0 ? 'border-red-500' : ''}`}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
),
|
|
242
|
+
{
|
|
243
|
+
validation: {
|
|
244
|
+
hideMessage: true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
);
|
|
235
248
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
249
|
+
// Custom validation with onBlur
|
|
250
|
+
user.phone.formElement((params) => (
|
|
251
|
+
<div>
|
|
252
|
+
<label>Phone Number</label>
|
|
253
|
+
<input
|
|
254
|
+
{...params.inputProps}
|
|
255
|
+
onBlur={(e) => {
|
|
256
|
+
if (e.target.value.length == 0 || isNaN(Number(e.target.value))) {
|
|
257
|
+
params.addValidationError("Please enter a valid phone number");
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
placeholder="(555) 123-4567"
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
));
|
|
241
264
|
```
|
|
242
265
|
|
|
243
|
-
|
|
266
|
+
### Working with Form Arrays
|
|
244
267
|
|
|
245
|
-
|
|
268
|
+
For managing collections like addresses:
|
|
246
269
|
|
|
247
270
|
```typescript
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
function AddressesManager() {
|
|
272
|
+
const [currentAddressIndex, setCurrentAddressIndex] = useState(0);
|
|
273
|
+
const user = useCogsState("user");
|
|
274
|
+
|
|
275
|
+
// Add new address
|
|
276
|
+
const addNewAddress = () => {
|
|
277
|
+
user.addresses.insert({
|
|
278
|
+
street: "",
|
|
279
|
+
city: "",
|
|
280
|
+
state: "",
|
|
281
|
+
zipCode: "",
|
|
282
|
+
country: "USA",
|
|
283
|
+
isDefault: false,
|
|
284
|
+
});
|
|
285
|
+
setCurrentAddressIndex(user.addresses.get().length - 1);
|
|
286
|
+
};
|
|
252
287
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
288
|
+
return (
|
|
289
|
+
<div>
|
|
290
|
+
{/* Address tabs with validation indicators */}
|
|
291
|
+
<div className="flex space-x-2 mt-2">
|
|
292
|
+
{user.addresses.stateMap((_, setter, index) => {
|
|
293
|
+
const errorCount = setter.showValidationErrors().length;
|
|
294
|
+
return (
|
|
295
|
+
<button
|
|
296
|
+
key={index}
|
|
297
|
+
onClick={() => setCurrentAddressIndex(index)}
|
|
298
|
+
className={`rounded-lg flex items-center justify-center ${
|
|
299
|
+
errorCount > 0
|
|
300
|
+
? "border-red-500 bg-red-400"
|
|
301
|
+
: currentAddressIndex === index
|
|
302
|
+
? "bg-blue-500 text-white"
|
|
303
|
+
: "bg-gray-200"
|
|
304
|
+
}`}
|
|
305
|
+
>
|
|
306
|
+
{index + 1}
|
|
307
|
+
{errorCount > 0 && (
|
|
308
|
+
<div className="bg-red-500 text-white rounded-full">
|
|
309
|
+
{errorCount}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</button>
|
|
313
|
+
);
|
|
314
|
+
})}
|
|
315
|
+
<button onClick={addNewAddress}>Add</button>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Current address form */}
|
|
319
|
+
{user.addresses.get().length > 0 && (
|
|
320
|
+
<div className="grid grid-cols-1 gap-4">
|
|
321
|
+
{/* Access fields with index() method */}
|
|
322
|
+
{user.addresses.index(currentAddressIndex).street.formElement(
|
|
323
|
+
(params) => (
|
|
324
|
+
<div>
|
|
325
|
+
<label>Street</label>
|
|
326
|
+
<input value={params.get()} onChange={(e) => params.set(e.target.value)} />
|
|
327
|
+
</div>
|
|
328
|
+
),
|
|
329
|
+
{
|
|
330
|
+
validation: {
|
|
331
|
+
message: "Street address is required"
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* City and State in a row */}
|
|
337
|
+
<div className="grid grid-cols-2 gap-4">
|
|
338
|
+
{user.addresses.index(currentAddressIndex).city.formElement((params) => (
|
|
339
|
+
<div>
|
|
340
|
+
<label>City</label>
|
|
341
|
+
<input {...params.inputProps} />
|
|
342
|
+
</div>
|
|
343
|
+
))}
|
|
344
|
+
|
|
345
|
+
{user.addresses.index(currentAddressIndex).state.formElement((params) => (
|
|
346
|
+
<div>
|
|
347
|
+
<label>State</label>
|
|
348
|
+
<input {...params.inputProps} />
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
{/* Boolean field handling */}
|
|
354
|
+
{user.addresses.index(currentAddressIndex).isDefault.formElement((params) => (
|
|
355
|
+
<div className="flex items-center">
|
|
356
|
+
<input
|
|
357
|
+
type="checkbox"
|
|
358
|
+
checked={params.get()}
|
|
359
|
+
onChange={(e) => params.set(e.target.checked)}
|
|
360
|
+
id={`default-address-${currentAddressIndex}`}
|
|
361
|
+
/>
|
|
362
|
+
<label htmlFor={`default-address-${currentAddressIndex}`}>
|
|
363
|
+
Set as default address
|
|
364
|
+
</label>
|
|
365
|
+
</div>
|
|
366
|
+
))}
|
|
367
|
+
|
|
368
|
+
{/* Remove address button */}
|
|
369
|
+
{user.addresses.get().length > 1 && (
|
|
370
|
+
<button
|
|
371
|
+
onClick={() => {
|
|
372
|
+
user.addresses.cut(currentAddressIndex);
|
|
373
|
+
setCurrentAddressIndex(Math.max(0, currentAddressIndex - 1));
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
Remove Selected Address
|
|
377
|
+
</button>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
257
384
|
```
|
|
258
385
|
|
|
259
|
-
|
|
386
|
+
### Form Actions
|
|
260
387
|
|
|
261
|
-
|
|
388
|
+
Cogsbox provides methods to manage form state:
|
|
262
389
|
|
|
263
390
|
```typescript
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
### Form Integration
|
|
391
|
+
// Reset form to initial state
|
|
392
|
+
const handleReset = () => {
|
|
393
|
+
user.revertToInitialState();
|
|
394
|
+
};
|
|
269
395
|
|
|
270
|
-
|
|
396
|
+
// Validate all fields using Zod schema
|
|
397
|
+
const handleSubmit = () => {
|
|
398
|
+
if (user.validateZodSchema()) {
|
|
399
|
+
// All valid, proceed with submission
|
|
400
|
+
submitData(user.get());
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
```
|
|
271
404
|
|
|
272
|
-
|
|
405
|
+
### Setting Up Zod Validation
|
|
273
406
|
|
|
274
407
|
```typescript
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
408
|
+
// Setting up validation at initialization
|
|
409
|
+
export const { useCogsState } = createCogsState({
|
|
410
|
+
user: {
|
|
411
|
+
initialState: {
|
|
412
|
+
firstName: "",
|
|
413
|
+
lastName: "",
|
|
414
|
+
email: "",
|
|
415
|
+
phone: "",
|
|
416
|
+
addresses: [
|
|
417
|
+
{
|
|
418
|
+
street: "",
|
|
419
|
+
city: "",
|
|
420
|
+
state: "",
|
|
421
|
+
zipCode: "",
|
|
422
|
+
country: "USA",
|
|
423
|
+
isDefault: false,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
validation: {
|
|
428
|
+
key: "userForm", // Used for error tracking
|
|
429
|
+
zodSchema: z.object({
|
|
430
|
+
firstName: z.string().min(1, "First name is required"),
|
|
431
|
+
lastName: z.string().min(1, "Last name is required"),
|
|
432
|
+
email: z.string().email("Please enter a valid email"),
|
|
433
|
+
phone: z.string().min(10, "Phone number must be at least 10 digits"),
|
|
434
|
+
addresses: z.array(
|
|
435
|
+
z.object({
|
|
436
|
+
street: z.string().min(1, "Street is required"),
|
|
437
|
+
city: z.string().min(1, "City is required"),
|
|
438
|
+
state: z.string().min(1, "State is required"),
|
|
439
|
+
zipCode: z
|
|
440
|
+
.string()
|
|
441
|
+
.min(5, "Zip code must be at least 5 characters"),
|
|
442
|
+
country: z.string(),
|
|
443
|
+
isDefault: z.boolean(),
|
|
444
|
+
})
|
|
445
|
+
),
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
281
448
|
},
|
|
282
|
-
|
|
283
|
-
})
|
|
449
|
+
});
|
|
284
450
|
```
|
|
285
451
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
#### `.setSelected()`
|
|
289
|
-
|
|
290
|
-
Marks an item as selected in a list.
|
|
452
|
+
## Server Synchronization
|
|
291
453
|
|
|
292
454
|
```typescript
|
|
293
|
-
//
|
|
294
|
-
|
|
455
|
+
// Setting up server sync
|
|
456
|
+
const products = useCogsState("products", {
|
|
457
|
+
serverSync: {
|
|
458
|
+
syncKey: "products",
|
|
459
|
+
syncFunction: ({ state }) => api.updateProducts(state),
|
|
460
|
+
debounce: 1000, // ms
|
|
461
|
+
mutation: useMutation(api.updateProducts),
|
|
462
|
+
},
|
|
463
|
+
});
|
|
295
464
|
|
|
296
|
-
//
|
|
297
|
-
|
|
465
|
+
// State is automatically synced with server after changes
|
|
466
|
+
products.items[0].stock.update((prev) => prev - 1);
|
|
298
467
|
```
|
|
299
468
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
Gets currently selected item from a list.
|
|
469
|
+
## Local Storage Persistence
|
|
303
470
|
|
|
304
471
|
```typescript
|
|
305
|
-
|
|
472
|
+
// Automatically save state to localStorage
|
|
473
|
+
const cart = useCogsState("cart", {
|
|
474
|
+
localStorage: {
|
|
475
|
+
key: "shopping-cart",
|
|
476
|
+
},
|
|
477
|
+
});
|
|
306
478
|
```
|
|
307
479
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
## Examples
|
|
311
|
-
|
|
312
|
-
### Product Catalog Management
|
|
480
|
+
## Example: Shopping Cart
|
|
313
481
|
|
|
314
482
|
```typescript
|
|
315
|
-
function
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
483
|
+
function ShoppingCart() {
|
|
484
|
+
const cart = useCogsState("cart");
|
|
485
|
+
const products = useCogsState("products");
|
|
486
|
+
|
|
487
|
+
const addToCart = (productId) => {
|
|
488
|
+
const product = products.items.findWith("id", productId).get();
|
|
489
|
+
|
|
490
|
+
cart.items.uniqueInsert(
|
|
491
|
+
{
|
|
492
|
+
productId,
|
|
493
|
+
name: product.name,
|
|
494
|
+
price: product.price,
|
|
495
|
+
quantity: 1
|
|
496
|
+
},
|
|
497
|
+
["productId"],
|
|
498
|
+
// If product exists, update quantity instead
|
|
499
|
+
(existingItem) => ({
|
|
500
|
+
...existingItem,
|
|
501
|
+
quantity: existingItem.quantity + 1
|
|
502
|
+
})
|
|
331
503
|
);
|
|
332
504
|
|
|
505
|
+
// Update total
|
|
506
|
+
cart.total.update(prev => prev + product.price);
|
|
507
|
+
};
|
|
508
|
+
|
|
333
509
|
return (
|
|
334
510
|
<div>
|
|
335
|
-
|
|
336
|
-
<ProductCard
|
|
337
|
-
key={product.id}
|
|
338
|
-
product={product}
|
|
339
|
-
onUpdateStock={(variantId, newStock) =>
|
|
340
|
-
productUpdater
|
|
341
|
-
.variants
|
|
342
|
-
.findWith("id", variantId)
|
|
343
|
-
.stock
|
|
344
|
-
.update(newStock)
|
|
345
|
-
}
|
|
346
|
-
/>
|
|
347
|
-
))}
|
|
348
|
-
</div>
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
```
|
|
511
|
+
<h2>Your Cart</h2>
|
|
352
512
|
|
|
353
|
-
|
|
513
|
+
{cart.items.stateMap((item, itemUpdater) => (
|
|
514
|
+
<div key={item.productId} className="cart-item">
|
|
515
|
+
<div>{item.name}</div>
|
|
516
|
+
<div>${item.price}</div>
|
|
354
517
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
key: "shopping-cart"
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const addToCart = (product: Product, variantId: string) => {
|
|
364
|
-
updater.items.uniqueInsert({
|
|
365
|
-
productId: product.id,
|
|
366
|
-
variantId,
|
|
367
|
-
quantity: 1
|
|
368
|
-
}, ['productId', 'variantId']);
|
|
369
|
-
};
|
|
518
|
+
<div className="quantity">
|
|
519
|
+
<button onClick={() =>
|
|
520
|
+
itemUpdater.quantity.update(prev => Math.max(prev - 1, 0))
|
|
521
|
+
}>-</button>
|
|
370
522
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
523
|
+
<span>{item.quantity}</span>
|
|
524
|
+
|
|
525
|
+
<button onClick={() =>
|
|
526
|
+
itemUpdater.quantity.update(prev => prev + 1)
|
|
527
|
+
}>+</button>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<button onClick={() => itemUpdater.cut()}>Remove</button>
|
|
531
|
+
</div>
|
|
380
532
|
))}
|
|
533
|
+
|
|
534
|
+
<div className="cart-total">
|
|
535
|
+
<strong>Total:</strong> ${cart.total.get()}
|
|
536
|
+
</div>
|
|
381
537
|
</div>
|
|
382
538
|
);
|
|
383
539
|
}
|
|
384
540
|
```
|
|
385
541
|
|
|
386
|
-
|
|
542
|
+
## Common Patterns and Tips
|
|
543
|
+
|
|
544
|
+
1. **Path-based Updates**: Always use the fluent API to update nested properties.
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Good
|
|
548
|
+
updater.users[0].address.city.update("New York");
|
|
549
|
+
|
|
550
|
+
// Avoid
|
|
551
|
+
updater.update({ ...state, users: [...] });
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
2. **Working with Arrays**: Use the built-in array methods instead of manually updating array state.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
// Good
|
|
558
|
+
updater.users.insert(newUser);
|
|
559
|
+
updater.users.findWith("id", 123).active.update(true);
|
|
560
|
+
|
|
561
|
+
// Avoid
|
|
562
|
+
updater.users.update([...users, newUser]);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
3. **Optimization**: Use the appropriate reactivity type for your needs.
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// For lists where only specific items change frequently
|
|
569
|
+
updater.items.stateMap((item) => (
|
|
570
|
+
<div>{item.$get()}</div> // Only this item re-renders when changed
|
|
571
|
+
));
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
4. **Form Management**: Use formElement for all form inputs to get automatic validation and debouncing.
|
|
575
|
+
```typescript
|
|
576
|
+
profile.name.formElement(
|
|
577
|
+
({ inputProps }) => <input {...inputProps} />,
|
|
578
|
+
{ debounceTime: 300 }
|
|
579
|
+
);
|
|
580
|
+
```
|