@signaltree/core 1.0.0 → 1.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/README.md +887 -4
- package/fesm2022/signaltree-core.mjs +120 -2
- package/fesm2022/signaltree-core.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,890 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🌳 SignalTree Core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The foundation package for SignalTree - a powerful, type-safe, modular signal-based state management solution for Angular applications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## ✨ What is @signaltree/core?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
SignalTree Core is the lightweight (5KB) foundation that provides:
|
|
8
|
+
|
|
9
|
+
- **Hierarchical signal trees** for organized state management
|
|
10
|
+
- **Type-safe updates and access** with full TypeScript inference
|
|
11
|
+
- **Basic entity management** with CRUD operations
|
|
12
|
+
- **Simple async actions** with loading states
|
|
13
|
+
- **Form integration basics** for reactive forms
|
|
14
|
+
- **Performance optimized** with lazy signal creation and structural sharing
|
|
15
|
+
|
|
16
|
+
## 🚀 Quick Start
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @signaltree/core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Basic Usage (Beginner)
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { signalTree } from '@signaltree/core';
|
|
28
|
+
|
|
29
|
+
// Create a simple reactive state tree
|
|
30
|
+
const tree = signalTree({
|
|
31
|
+
count: 0,
|
|
32
|
+
message: 'Hello World',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Read values (these are Angular signals)
|
|
36
|
+
console.log(tree.$.count()); // 0
|
|
37
|
+
console.log(tree.$.message()); // 'Hello World'
|
|
38
|
+
|
|
39
|
+
// Update values
|
|
40
|
+
tree.$.count.set(5);
|
|
41
|
+
tree.$.message.set('Updated!');
|
|
42
|
+
|
|
43
|
+
// Use in Angular components
|
|
44
|
+
@Component({
|
|
45
|
+
template: `
|
|
46
|
+
<div>Count: {{ tree.$.count() }}</div>
|
|
47
|
+
<div>Message: {{ tree.$.message() }}</div>
|
|
48
|
+
<button (click)="increment()">+1</button>
|
|
49
|
+
`,
|
|
50
|
+
})
|
|
51
|
+
class SimpleComponent {
|
|
52
|
+
tree = tree;
|
|
53
|
+
|
|
54
|
+
increment() {
|
|
55
|
+
this.tree.$.count.update((n) => n + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Intermediate Usage (Nested State)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Create hierarchical state
|
|
64
|
+
const tree = signalTree({
|
|
65
|
+
user: {
|
|
66
|
+
name: 'John Doe',
|
|
67
|
+
email: 'john@example.com',
|
|
68
|
+
preferences: {
|
|
69
|
+
theme: 'dark',
|
|
70
|
+
notifications: true,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
ui: {
|
|
74
|
+
loading: false,
|
|
75
|
+
errors: [] as string[],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Access nested signals with full type safety
|
|
80
|
+
tree.$.user.name.set('Jane Doe');
|
|
81
|
+
tree.$.user.preferences.theme.set('light');
|
|
82
|
+
tree.$.ui.loading.set(true);
|
|
83
|
+
|
|
84
|
+
// Computed values from nested state
|
|
85
|
+
const userDisplayName = computed(() => {
|
|
86
|
+
const user = tree.$.user();
|
|
87
|
+
return `${user.name} (${user.email})`;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Effects that respond to changes
|
|
91
|
+
effect(() => {
|
|
92
|
+
if (tree.$.ui.loading()) {
|
|
93
|
+
console.log('Loading started...');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Advanced Usage (Full State Tree)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface AppState {
|
|
102
|
+
auth: {
|
|
103
|
+
user: User | null;
|
|
104
|
+
token: string | null;
|
|
105
|
+
isAuthenticated: boolean;
|
|
106
|
+
};
|
|
107
|
+
data: {
|
|
108
|
+
users: User[];
|
|
109
|
+
posts: Post[];
|
|
110
|
+
cache: Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
ui: {
|
|
113
|
+
theme: 'light' | 'dark';
|
|
114
|
+
sidebar: {
|
|
115
|
+
open: boolean;
|
|
116
|
+
width: number;
|
|
117
|
+
};
|
|
118
|
+
notifications: Notification[];
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const tree = signalTree<AppState>({
|
|
123
|
+
auth: {
|
|
124
|
+
user: null,
|
|
125
|
+
token: null,
|
|
126
|
+
isAuthenticated: false
|
|
127
|
+
},
|
|
128
|
+
data: {
|
|
129
|
+
users: [],
|
|
130
|
+
posts: [],
|
|
131
|
+
cache: {}
|
|
132
|
+
},
|
|
133
|
+
ui: {
|
|
134
|
+
theme: 'light',
|
|
135
|
+
sidebar: { open: true, width: 250 },
|
|
136
|
+
notifications: []
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Complex updates with type safety
|
|
141
|
+
tree.update(state => ({
|
|
142
|
+
auth: {
|
|
143
|
+
...state.auth,
|
|
144
|
+
user: { id: '1', name: 'John' },
|
|
145
|
+
isAuthenticated: true
|
|
146
|
+
},
|
|
147
|
+
ui: {
|
|
148
|
+
...state.ui,
|
|
149
|
+
notifications: [
|
|
150
|
+
...state.ui.notifications,
|
|
151
|
+
// Get entire state as plain object
|
|
152
|
+
const currentState = tree.unwrap();
|
|
153
|
+
console.log('Current app state:', currentState);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 📦 Core Features
|
|
157
|
+
|
|
158
|
+
### 1. Hierarchical Signal Trees
|
|
159
|
+
|
|
160
|
+
Create deeply nested reactive state with automatic type inference:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const tree = signalTree({
|
|
164
|
+
user: { name: '', email: '' },
|
|
165
|
+
settings: { theme: 'dark', notifications: true },
|
|
166
|
+
todos: [] as Todo[],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Access nested signals with full type safety
|
|
170
|
+
tree.$.user.name(); // string signal
|
|
171
|
+
tree.$.settings.theme.set('light'); // type-checked value
|
|
172
|
+
tree.$.todos.update((todos) => [...todos, newTodo]); // array operations
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 2. TypeScript Inference Excellence
|
|
176
|
+
|
|
177
|
+
SignalTree provides complete type inference without manual typing:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Automatic inference from initial state
|
|
181
|
+
const tree = signalTree({
|
|
182
|
+
count: 0, // Inferred as WritableSignal<number>
|
|
183
|
+
name: 'John', // Inferred as WritableSignal<string>
|
|
184
|
+
active: true, // Inferred as WritableSignal<boolean>
|
|
185
|
+
items: [] as Item[], // Inferred as WritableSignal<Item[]>
|
|
186
|
+
config: {
|
|
187
|
+
theme: 'dark' as const, // Inferred as WritableSignal<'dark'>
|
|
188
|
+
settings: {
|
|
189
|
+
nested: true, // Deep nesting maintained
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Type-safe access and updates
|
|
195
|
+
tree.$.count.set(5); // ✅ number
|
|
196
|
+
tree.$.count.set('invalid'); // ❌ Type error
|
|
197
|
+
tree.$.config.theme.set('light'); // ❌ Type error ('dark' const)
|
|
198
|
+
tree.$.config.settings.nested.set(false); // ✅ boolean
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 3. Basic Entity Management
|
|
202
|
+
|
|
203
|
+
Built-in lightweight CRUD operations:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
interface User {
|
|
207
|
+
id: string;
|
|
208
|
+
name: string;
|
|
209
|
+
email: string;
|
|
210
|
+
active: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const tree = signalTree({
|
|
214
|
+
users: [] as User[],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const users = tree.asCrud<User>('users');
|
|
218
|
+
|
|
219
|
+
// Basic CRUD operations
|
|
220
|
+
users.add({ id: '1', name: 'Alice', email: 'alice@example.com', active: true });
|
|
221
|
+
users.update('1', { name: 'Alice Smith' });
|
|
222
|
+
users.remove('1');
|
|
223
|
+
users.upsert({ id: '2', name: 'Bob', email: 'bob@example.com', active: true });
|
|
224
|
+
|
|
225
|
+
// Basic queries
|
|
226
|
+
const userById = users.findById('1');
|
|
227
|
+
const allUsers = users.selectAll();
|
|
228
|
+
const userCount = users.selectTotal();
|
|
229
|
+
const activeUsers = users.selectWhere((user) => user.active);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 4. Simple Async Actions
|
|
233
|
+
|
|
234
|
+
Built-in async action helpers with loading states:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const tree = signalTree({
|
|
238
|
+
users: [] as User[],
|
|
239
|
+
loading: false,
|
|
240
|
+
error: null as string | null,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const loadUsers = tree.asyncAction(async () => await api.getUsers(), {
|
|
244
|
+
onStart: () => ({ loading: true, error: null }),
|
|
245
|
+
onSuccess: (users) => ({ users, loading: false }),
|
|
246
|
+
onError: (error) => ({ loading: false, error: error.message }),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Usage in component
|
|
250
|
+
@Component({
|
|
251
|
+
template: `
|
|
252
|
+
@if (tree.$.loading()) {
|
|
253
|
+
<div>Loading...</div>
|
|
254
|
+
} @else if (tree.$.error()) {
|
|
255
|
+
<div class="error">{{ tree.$.error() }}</div>
|
|
256
|
+
} @else { @for (user of tree.$.users(); track user.id) {
|
|
257
|
+
<user-card [user]="user" />
|
|
258
|
+
} }
|
|
259
|
+
<button (click)="loadUsers()">Refresh</button>
|
|
260
|
+
`,
|
|
261
|
+
})
|
|
262
|
+
class UsersComponent {
|
|
263
|
+
tree = tree;
|
|
264
|
+
loadUsers = loadUsers;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 5. Performance Optimizations
|
|
269
|
+
|
|
270
|
+
Core includes several performance optimizations:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Lazy signal creation (default)
|
|
274
|
+
const tree = signalTree(
|
|
275
|
+
{
|
|
276
|
+
largeObject: {
|
|
277
|
+
// Signals only created when accessed
|
|
278
|
+
level1: { level2: { level3: { data: 'value' } } },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
useLazySignals: true, // Default: true
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Custom equality function
|
|
287
|
+
const tree2 = signalTree(
|
|
288
|
+
{
|
|
289
|
+
items: [] as Item[],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
useShallowComparison: false, // Deep equality (default)
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Structural sharing for memory efficiency
|
|
297
|
+
tree.update((state) => ({
|
|
298
|
+
...state, // Reuses unchanged parts
|
|
299
|
+
newField: 'value',
|
|
300
|
+
}));
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 🚀 Error Handling Examples
|
|
304
|
+
|
|
305
|
+
### Async Error Handling
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const tree = signalTree({
|
|
309
|
+
data: null as ApiData | null,
|
|
310
|
+
loading: false,
|
|
311
|
+
error: null as Error | null,
|
|
312
|
+
retryCount: 0,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const loadDataWithRetry = tree.asyncAction(
|
|
316
|
+
async (attempt = 0) => {
|
|
317
|
+
try {
|
|
318
|
+
return await api.getData();
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (attempt < 3) {
|
|
321
|
+
// Retry logic
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
323
|
+
return loadDataWithRetry(attempt + 1);
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
onStart: () => ({ loading: true, error: null }),
|
|
330
|
+
onSuccess: (data) => ({ data, loading: false, retryCount: 0 }),
|
|
331
|
+
onError: (error, state) => ({
|
|
332
|
+
loading: false,
|
|
333
|
+
error,
|
|
334
|
+
retryCount: state.retryCount + 1,
|
|
335
|
+
}),
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Error boundary component
|
|
340
|
+
@Component({
|
|
341
|
+
template: `
|
|
342
|
+
@if (tree.$.error()) {
|
|
343
|
+
<div class="error-boundary">
|
|
344
|
+
<h3>Something went wrong</h3>
|
|
345
|
+
<p>{{ tree.$.error()?.message }}</p>
|
|
346
|
+
<p>Attempts: {{ tree.$.retryCount() }}</p>
|
|
347
|
+
<button (click)="retry()">Retry</button>
|
|
348
|
+
<button (click)="clear()">Clear Error</button>
|
|
349
|
+
</div>
|
|
350
|
+
} @else {
|
|
351
|
+
<!-- Normal content -->
|
|
352
|
+
}
|
|
353
|
+
`,
|
|
354
|
+
})
|
|
355
|
+
class ErrorHandlingComponent {
|
|
356
|
+
tree = tree;
|
|
357
|
+
|
|
358
|
+
retry() {
|
|
359
|
+
loadDataWithRetry();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
clear() {
|
|
363
|
+
this.tree.$.error.set(null);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### State Update Error Handling
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
const tree = signalTree({
|
|
372
|
+
items: [] as Item[],
|
|
373
|
+
validationErrors: [] as string[],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Safe update with validation
|
|
377
|
+
function safeUpdateItem(id: string, updates: Partial<Item>) {
|
|
378
|
+
try {
|
|
379
|
+
tree.update((state) => {
|
|
380
|
+
const itemIndex = state.items.findIndex((item) => item.id === id);
|
|
381
|
+
if (itemIndex === -1) {
|
|
382
|
+
throw new Error(`Item with id ${id} not found`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const updatedItem = { ...state.items[itemIndex], ...updates };
|
|
386
|
+
|
|
387
|
+
// Validation
|
|
388
|
+
if (!updatedItem.name?.trim()) {
|
|
389
|
+
throw new Error('Item name is required');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const newItems = [...state.items];
|
|
393
|
+
newItems[itemIndex] = updatedItem;
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
items: newItems,
|
|
397
|
+
validationErrors: [], // Clear errors on success
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
tree.$.validationErrors.update((errors) => [...errors, error instanceof Error ? error.message : 'Unknown error']);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## 🔗 Package Composition Patterns
|
|
407
|
+
|
|
408
|
+
### Basic Composition
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { signalTree } from '@signaltree/core';
|
|
412
|
+
|
|
413
|
+
// Core provides the foundation
|
|
414
|
+
const tree = signalTree({
|
|
415
|
+
state: 'initial',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Extend with additional packages via pipe
|
|
419
|
+
const enhancedTree = tree.pipe(
|
|
420
|
+
// Add features as needed
|
|
421
|
+
someFeatureFunction()
|
|
422
|
+
);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Modular Enhancement Pattern
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Start minimal, add features as needed
|
|
429
|
+
let tree = signalTree(initialState);
|
|
430
|
+
|
|
431
|
+
if (isDevelopment) {
|
|
432
|
+
tree = tree.pipe(withDevtools());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (needsPerformance) {
|
|
436
|
+
tree = tree.pipe(withBatching(), withMemoization());
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (needsTimeTravel) {
|
|
440
|
+
tree = tree.pipe(withTimeTravel());
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Service-Based Pattern
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
@Injectable()
|
|
448
|
+
class AppStateService {
|
|
449
|
+
private tree = signalTree({
|
|
450
|
+
user: null as User | null,
|
|
451
|
+
settings: { theme: 'light' as const },
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Expose specific parts
|
|
455
|
+
readonly user$ = this.tree.$.user;
|
|
456
|
+
readonly settings$ = this.tree.$.settings;
|
|
457
|
+
|
|
458
|
+
// Expose specific actions
|
|
459
|
+
setUser(user: User) {
|
|
460
|
+
this.tree.$.user.set(user);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
updateSettings(settings: Partial<Settings>) {
|
|
464
|
+
this.tree.$.settings.update((current) => ({
|
|
465
|
+
...current,
|
|
466
|
+
...settings,
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// For advanced features, return the tree
|
|
471
|
+
getTree() {
|
|
472
|
+
return this.tree;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## ⚡ Performance Benchmarks
|
|
478
|
+
|
|
479
|
+
### Memory Usage Comparison
|
|
480
|
+
|
|
481
|
+
| Operation | SignalTree Core | NgRx | Akita | Native Signals |
|
|
482
|
+
| ------------------------ | --------------- | ------ | ------ | -------------- |
|
|
483
|
+
| 1K entities | 1.2MB | 4.2MB | 3.5MB | 2.3MB |
|
|
484
|
+
| 10K entities | 8.1MB | 28.5MB | 22.1MB | 15.2MB |
|
|
485
|
+
| Deep nesting (10 levels) | 145KB | 890KB | 720KB | 340KB |
|
|
486
|
+
|
|
487
|
+
### Update Performance
|
|
488
|
+
|
|
489
|
+
| Operation | SignalTree Core | NgRx | Akita | Native Signals |
|
|
490
|
+
| ------------------------ | --------------- | ---- | ----- | -------------- |
|
|
491
|
+
| Single update | <1ms | 8ms | 6ms | 2ms |
|
|
492
|
+
| Nested update (5 levels) | 2ms | 12ms | 10ms | 3ms |
|
|
493
|
+
| Bulk update (100 items) | 14ms | 35ms | 28ms | 10ms |
|
|
494
|
+
|
|
495
|
+
### TypeScript Inference Speed
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// SignalTree: Instant inference
|
|
499
|
+
const tree = signalTree({
|
|
500
|
+
deeply: { nested: { state: { with: { types: 'instant' } } } }
|
|
501
|
+
});
|
|
502
|
+
tree.$.deeply.nested.state.with.types.set('updated'); // ✅ <1ms
|
|
503
|
+
|
|
504
|
+
// Manual typing required with other solutions
|
|
505
|
+
interface State { deeply: { nested: { state: { with: { types: string } } } } }
|
|
506
|
+
const store: Store<State> = ...; // Requires manual interface definition
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## 🎯 Real-World Example
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Complete user management component
|
|
513
|
+
@Component({
|
|
514
|
+
template: `
|
|
515
|
+
<div class="user-manager">
|
|
516
|
+
<!-- User List -->
|
|
517
|
+
<div class="user-list">
|
|
518
|
+
@if (userTree.$.loading()) {
|
|
519
|
+
<div class="loading">Loading users...</div>
|
|
520
|
+
} @else if (userTree.$.error()) {
|
|
521
|
+
<div class="error">
|
|
522
|
+
{{ userTree.$.error() }}
|
|
523
|
+
<button (click)="loadUsers()">Retry</button>
|
|
524
|
+
</div>
|
|
525
|
+
} @else { @for (user of users.selectAll()(); track user.id) {
|
|
526
|
+
<div class="user-card">
|
|
527
|
+
<h3>{{ user.name }}</h3>
|
|
528
|
+
<p>{{ user.email }}</p>
|
|
529
|
+
<button (click)="editUser(user)">Edit</button>
|
|
530
|
+
<button (click)="deleteUser(user.id)">Delete</button>
|
|
531
|
+
</div>
|
|
532
|
+
} }
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<!-- User Form -->
|
|
536
|
+
<form (ngSubmit)="saveUser()" #form="ngForm">
|
|
537
|
+
<input [(ngModel)]="userTree.$.form.name()" name="name" placeholder="Name" required />
|
|
538
|
+
<input [(ngModel)]="userTree.$.form.email()" name="email" type="email" placeholder="Email" required />
|
|
539
|
+
<button type="submit" [disabled]="form.invalid">{{ userTree.$.form.id() ? 'Update' : 'Create' }} User</button>
|
|
540
|
+
<button type="button" (click)="clearForm()">Clear</button>
|
|
541
|
+
</form>
|
|
542
|
+
</div>
|
|
543
|
+
`,
|
|
544
|
+
})
|
|
545
|
+
class UserManagerComponent implements OnInit {
|
|
546
|
+
userTree = signalTree({
|
|
547
|
+
users: [] as User[],
|
|
548
|
+
loading: false,
|
|
549
|
+
error: null as string | null,
|
|
550
|
+
form: { id: '', name: '', email: '' },
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
users = this.userTree.asCrud<User>('users');
|
|
554
|
+
|
|
555
|
+
loadUsers = this.userTree.asyncAction(async () => await this.userService.getUsers(), {
|
|
556
|
+
onStart: () => ({ loading: true, error: null }),
|
|
557
|
+
onSuccess: (users) => ({ users, loading: false }),
|
|
558
|
+
onError: (error) => ({ loading: false, error: error.message }),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
constructor(private userService: UserService) {}
|
|
562
|
+
|
|
563
|
+
ngOnInit() {
|
|
564
|
+
this.loadUsers();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
editUser(user: User) {
|
|
568
|
+
this.userTree.$.form.set(user);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async saveUser() {
|
|
572
|
+
try {
|
|
573
|
+
const form = this.userTree.$.form();
|
|
574
|
+
if (form.id) {
|
|
575
|
+
await this.userService.updateUser(form.id, form);
|
|
576
|
+
this.users.update(form.id, form);
|
|
577
|
+
} else {
|
|
578
|
+
const newUser = await this.userService.createUser(form);
|
|
579
|
+
this.users.add(newUser);
|
|
580
|
+
}
|
|
581
|
+
this.clearForm();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
this.userTree.$.error.set(error instanceof Error ? error.message : 'Save failed');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
deleteUser(id: string) {
|
|
588
|
+
if (confirm('Delete user?')) {
|
|
589
|
+
this.users.remove(id);
|
|
590
|
+
this.userService.deleteUser(id).catch((error) => {
|
|
591
|
+
this.userTree.$.error.set(error.message);
|
|
592
|
+
this.loadUsers(); // Reload on error
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
clearForm() {
|
|
598
|
+
this.userTree.$.form.set({ id: '', name: '', email: '' });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
}
|
|
606
|
+
}));
|
|
607
|
+
|
|
608
|
+
// Get entire state as plain object
|
|
609
|
+
const currentState = tree.unwrap();
|
|
610
|
+
console.log('Current app state:', currentState);
|
|
611
|
+
|
|
612
|
+
```
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## 📦 Core Features
|
|
617
|
+
|
|
618
|
+
### Hierarchical Signal Trees
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
const tree = signalTree({
|
|
622
|
+
user: { name: '', email: '' },
|
|
623
|
+
settings: { theme: 'dark', notifications: true },
|
|
624
|
+
todos: [] as Todo[],
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Access nested signals with full type safety
|
|
628
|
+
tree.$.user.name(); // string
|
|
629
|
+
tree.$.settings.theme.set('light');
|
|
630
|
+
tree.$.todos.update((todos) => [...todos, newTodo]);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Basic Entity Management
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// Built-in CRUD operations (lightweight)
|
|
637
|
+
const todos = tree.asCrud<Todo>('todos');
|
|
638
|
+
|
|
639
|
+
todos.add({ id: '1', text: 'Learn SignalTree', done: false });
|
|
640
|
+
todos.update('1', { done: true });
|
|
641
|
+
todos.remove('1');
|
|
642
|
+
todos.upsert({ id: '2', text: 'Build app', done: false });
|
|
643
|
+
|
|
644
|
+
// Basic queries
|
|
645
|
+
const todoById = todos.findById('1');
|
|
646
|
+
const allTodos = todos.selectAll();
|
|
647
|
+
const todoCount = todos.selectTotal();
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Simple Async Actions
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
const loadUsers = tree.asyncAction(async () => await api.getUsers(), {
|
|
654
|
+
onStart: () => ({ loading: true }),
|
|
655
|
+
onSuccess: (users) => ({ users, loading: false }),
|
|
656
|
+
onError: (error) => ({ loading: false, error: error.message }),
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Use in components
|
|
660
|
+
async function handleLoadUsers() {
|
|
661
|
+
await loadUsers();
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Reactive Effects
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Create reactive effects
|
|
669
|
+
tree.effect((state) => {
|
|
670
|
+
console.log(`User: ${state.user.name}, Theme: ${state.settings.theme}`);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Manual subscriptions
|
|
674
|
+
const unsubscribe = tree.subscribe((state) => {
|
|
675
|
+
// Handle state changes
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## 🎯 Core API Reference
|
|
680
|
+
|
|
681
|
+
### signalTree()
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
const tree = signalTree(initialState, config?);
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Tree Methods
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
// State access
|
|
691
|
+
tree.$.property(); // Read signal value
|
|
692
|
+
tree.$.property.set(value); // Update signal
|
|
693
|
+
tree.unwrap(); // Get plain object
|
|
694
|
+
|
|
695
|
+
// Tree operations
|
|
696
|
+
tree.update(updater); // Update entire tree
|
|
697
|
+
tree.effect(fn); // Create reactive effects
|
|
698
|
+
tree.subscribe(fn); // Manual subscriptions
|
|
699
|
+
tree.destroy(); // Cleanup resources
|
|
700
|
+
|
|
701
|
+
// Entity management
|
|
702
|
+
tree.asCrud<T>(key); // Get entity helpers
|
|
703
|
+
|
|
704
|
+
// Async actions
|
|
705
|
+
tree.asyncAction(fn, config?); // Create async action
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
## 🔌 Extending with Optional Packages
|
|
709
|
+
|
|
710
|
+
SignalTree Core can be extended with additional features:
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
import { signalTree } from '@signaltree/core';
|
|
714
|
+
import { withBatching } from '@signaltree/batching';
|
|
715
|
+
import { withMemoization } from '@signaltree/memoization';
|
|
716
|
+
import { withTimeTravel } from '@signaltree/time-travel';
|
|
717
|
+
|
|
718
|
+
// Compose features using pipe
|
|
719
|
+
const tree = signalTree(initialState).pipe(withBatching(), withMemoization(), withTimeTravel());
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Available Extensions
|
|
723
|
+
|
|
724
|
+
- **@signaltree/batching** (+1KB) - Batch multiple updates
|
|
725
|
+
- **@signaltree/memoization** (+2KB) - Intelligent caching & performance
|
|
726
|
+
- **@signaltree/middleware** (+1KB) - Middleware system & taps
|
|
727
|
+
- **@signaltree/async** (+2KB) - Advanced async actions & states
|
|
728
|
+
- **@signaltree/entities** (+2KB) - Advanced entity management
|
|
729
|
+
- **@signaltree/devtools** (+1KB) - Redux DevTools integration
|
|
730
|
+
- **@signaltree/time-travel** (+3KB) - Undo/redo functionality
|
|
731
|
+
- **@signaltree/ng-forms** (+3KB) - Complete Angular forms integration
|
|
732
|
+
- **@signaltree/presets** (+0.5KB) - Environment-based configurations
|
|
733
|
+
|
|
734
|
+
## 🎯 When to Use Core Only
|
|
735
|
+
|
|
736
|
+
Perfect for:
|
|
737
|
+
|
|
738
|
+
- ✅ Simple to medium applications
|
|
739
|
+
- ✅ Prototype and MVP development
|
|
740
|
+
- ✅ When bundle size is critical
|
|
741
|
+
- ✅ Learning signal-based state management
|
|
742
|
+
- ✅ Applications with basic state needs
|
|
743
|
+
|
|
744
|
+
Consider extensions when you need:
|
|
745
|
+
|
|
746
|
+
- ⚡ Performance optimization (batching, memoization)
|
|
747
|
+
- 🐛 Advanced debugging (devtools, time-travel)
|
|
748
|
+
- 📝 Complex forms (ng-forms)
|
|
749
|
+
- 🔧 Middleware patterns (middleware)
|
|
750
|
+
|
|
751
|
+
## 🔄 Migration from NgRx
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
// Step 1: Create parallel tree
|
|
755
|
+
const tree = signalTree(initialState);
|
|
756
|
+
|
|
757
|
+
// Step 2: Gradually migrate components
|
|
758
|
+
// Before (NgRx)
|
|
759
|
+
users$ = this.store.select(selectUsers);
|
|
760
|
+
|
|
761
|
+
// After (SignalTree)
|
|
762
|
+
users = this.tree.$.users;
|
|
763
|
+
|
|
764
|
+
// Step 3: Replace effects with async actions
|
|
765
|
+
// Before (NgRx)
|
|
766
|
+
loadUsers$ = createEffect(() =>
|
|
767
|
+
this.actions$.pipe(
|
|
768
|
+
ofType(loadUsers),
|
|
769
|
+
switchMap(() => this.api.getUsers())
|
|
770
|
+
)
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
// After (SignalTree)
|
|
774
|
+
loadUsers = tree.asyncAction(() => api.getUsers(), {
|
|
775
|
+
onSuccess: (users, tree) => tree.$.users.set(users),
|
|
776
|
+
});
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## 📖 Examples
|
|
780
|
+
|
|
781
|
+
### Simple Counter
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
const counter = signalTree({ count: 0 });
|
|
785
|
+
|
|
786
|
+
// In component
|
|
787
|
+
@Component({
|
|
788
|
+
template: ` <button (click)="increment()">{{ counter.$.count() }}</button> `,
|
|
789
|
+
})
|
|
790
|
+
class CounterComponent {
|
|
791
|
+
counter = counter;
|
|
792
|
+
|
|
793
|
+
increment() {
|
|
794
|
+
this.counter.$.count.update((n) => n + 1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### User Management
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
const userTree = signalTree({
|
|
803
|
+
users: [] as User[],
|
|
804
|
+
loading: false,
|
|
805
|
+
error: null as string | null,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const users = userTree.asCrud<User>('users');
|
|
809
|
+
|
|
810
|
+
const loadUsers = userTree.asyncAction(async () => await api.getUsers(), {
|
|
811
|
+
onStart: () => ({ loading: true }),
|
|
812
|
+
onSuccess: (users) => ({ users, loading: false, error: null }),
|
|
813
|
+
onError: (error) => ({ loading: false, error: error.message }),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// In component
|
|
817
|
+
@Component({
|
|
818
|
+
template: `
|
|
819
|
+
@if (userTree.$.loading()) {
|
|
820
|
+
<spinner />
|
|
821
|
+
} @else { @for (user of userTree.$.users(); track user.id) {
|
|
822
|
+
<user-card [user]="user" />
|
|
823
|
+
} }
|
|
824
|
+
`,
|
|
825
|
+
})
|
|
826
|
+
class UsersComponent {
|
|
827
|
+
userTree = userTree;
|
|
828
|
+
|
|
829
|
+
ngOnInit() {
|
|
830
|
+
loadUsers();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
addUser(userData: Partial<User>) {
|
|
834
|
+
users.add({ id: crypto.randomUUID(), ...userData });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## � Available Extension Packages
|
|
840
|
+
|
|
841
|
+
Extend the core with optional feature packages:
|
|
842
|
+
|
|
843
|
+
### Performance & Optimization
|
|
844
|
+
|
|
845
|
+
- **[@signaltree/batching](../batching)** (+1KB) - Batch multiple updates for better performance
|
|
846
|
+
- **[@signaltree/memoization](../memoization)** (+2KB) - Intelligent caching & performance optimization
|
|
847
|
+
|
|
848
|
+
### Advanced Features
|
|
849
|
+
|
|
850
|
+
- **[@signaltree/middleware](../middleware)** (+1KB) - Middleware system & state interceptors
|
|
851
|
+
- **[@signaltree/async](../async)** (+2KB) - Advanced async operations & loading states
|
|
852
|
+
- **[@signaltree/entities](../entities)** (+2KB) - Enhanced CRUD operations & entity management
|
|
853
|
+
|
|
854
|
+
### Development Tools
|
|
855
|
+
|
|
856
|
+
- **[@signaltree/devtools](../devtools)** (+1KB) - Development tools & Redux DevTools integration
|
|
857
|
+
- **[@signaltree/time-travel](../time-travel)** (+3KB) - Undo/redo functionality & state history
|
|
858
|
+
|
|
859
|
+
### Integration & Convenience
|
|
860
|
+
|
|
861
|
+
- **[@signaltree/presets](../presets)** (+0.5KB) - Pre-configured setups for common patterns
|
|
862
|
+
- **[@signaltree/ng-forms](../ng-forms)** (+3KB) - Complete Angular Forms integration
|
|
863
|
+
|
|
864
|
+
### Quick Start with Extensions
|
|
865
|
+
|
|
866
|
+
```bash
|
|
867
|
+
# Performance-focused setup
|
|
868
|
+
npm install @signaltree/core @signaltree/batching @signaltree/memoization
|
|
869
|
+
|
|
870
|
+
# Full development setup
|
|
871
|
+
npm install @signaltree/core @signaltree/batching @signaltree/memoization @signaltree/devtools @signaltree/time-travel
|
|
872
|
+
|
|
873
|
+
# All packages (full-featured)
|
|
874
|
+
npm install @signaltree/core @signaltree/batching @signaltree/memoization @signaltree/middleware @signaltree/async @signaltree/entities @signaltree/devtools @signaltree/time-travel @signaltree/presets @signaltree/ng-forms
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
## �🔗 Links
|
|
878
|
+
|
|
879
|
+
- [SignalTree Documentation](https://signaltree.io)
|
|
880
|
+
- [GitHub Repository](https://github.com/JBorgia/signaltree)
|
|
881
|
+
- [NPM Package](https://www.npmjs.com/package/@signaltree/core)
|
|
882
|
+
- [Interactive Examples](https://signaltree.io/examples)
|
|
883
|
+
|
|
884
|
+
## 📄 License
|
|
885
|
+
|
|
886
|
+
MIT License with AI Training Restriction - see the [LICENSE](../../LICENSE) file for details.
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
**Ready to get started?** This core package provides everything you need for most applications. Add extensions only when you need them! 🚀
|