ngx-signal-plus 2.0.1 → 2.0.2

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 CHANGED
@@ -1,597 +1,597 @@
1
- # ngx-signal-plus
2
-
3
- A powerful utility library that enhances Angular Signals with additional features for robust state management.
4
-
5
- ## Features
6
-
7
- - Enhanced signal operations with built-in state tracking
8
- - Type-safe validations and transformations
9
- - Persistent storage with automatic serialization
10
- - Time-based operations (debounce, throttle, delay)
11
- - Signal operators for transformation and combination
12
- - Built-in undo/redo functionality
13
- - Form handling with validation
14
- - Form groups with aggregated state and validation
15
- - Async state management with loading, error, and retry logic
16
- - Reactive Queries for server state (TanStack Query style)
17
- - Collection management with ID-based CRUD operations
18
- - Automatic cleanup and memory management
19
- - Performance optimizations
20
- - Transactions and batching for atomic operations
21
-
22
- ## Installation
23
-
24
- ```bash
25
- npm install ngx-signal-plus
26
- ```
27
-
28
- ## Requirements
29
-
30
- - Angular >= 16.0.0 (fully compatible with Angular 16-20)
31
- - TypeScript >= 5.0.0
32
-
33
- ## Basic Usage
34
-
35
- ```typescript
36
- import { Component } from "@angular/core";
37
- import { sp, enhance, spMap, spFilter } from "ngx-signal-plus";
38
- import { signal, computed } from "@angular/core";
39
-
40
- @Component({
41
- standalone: true,
42
- selector: "app-counter",
43
- template: `
44
- <div>Count: {{ counter.value() }}</div>
45
- <div>Doubled: {{ doubled() }}</div>
46
- <button (click)="increment()">Increment</button>
47
- <button (click)="decrement()">Decrement</button>
48
-
49
- @if (counter.history().length > 0) {
50
- <button (click)="counter.undo()">Undo</button>
51
- }
52
- `,
53
- })
54
- export class CounterComponent {
55
- // Create an enhanced signal with persistence and history
56
- counter = sp(0)
57
- .persist("counter")
58
- .withHistory(10)
59
- .validate((value) => value >= 0)
60
- .build();
61
-
62
- // Use signal operators
63
- doubled = computed(() => this.counter.value() * 2);
64
-
65
- increment() {
66
- this.counter.setValue(this.counter.value() + 1);
67
- }
68
-
69
- decrement() {
70
- if (this.counter.value() > 0) {
71
- this.counter.setValue(this.counter.value() - 1);
72
- }
73
- }
74
- }
75
- ```
76
-
77
- ## Core Features
78
-
79
- ### Signal Creation
80
-
81
- ```typescript
82
- import { sp, spCounter, spToggle, spForm } from "ngx-signal-plus";
83
-
84
- // Simple enhanced signal
85
- const name = sp("John").build();
86
-
87
- // Counter with min/max validation
88
- const counter = spCounter(0, { min: 0, max: 100 });
89
-
90
- // Toggle (boolean) with persistence
91
- const darkMode = spToggle(false, "theme-mode");
92
-
93
- // Form input with validation
94
- const username = spForm.text("", {
95
- minLength: 3,
96
- maxLength: 20,
97
- debounce: 300,
98
- });
99
- ```
100
-
101
- ### Signal Enhancement
102
-
103
- Enhance existing signals with additional features:
104
-
105
- ```typescript
106
- import { enhance } from "ngx-signal-plus";
107
- import { signal } from "@angular/core";
108
-
109
- const enhanced = enhance(signal(0))
110
- .persist("counter")
111
- .validate((n) => n >= 0)
112
- .transform(Math.round)
113
- .withHistory(5)
114
- .debounce(300)
115
- .distinctUntilChanged()
116
- .build();
117
- ```
118
-
119
- ### Signal Operators
120
-
121
- ```typescript
122
- import { spMap, spFilter, spDebounceTime, spCombineLatest } from "ngx-signal-plus";
123
- import { signal } from "@angular/core";
124
-
125
- // Transform values
126
- const price = signal(100);
127
- const withTax = price.pipe(
128
- spMap((n) => n * 1.2),
129
- spMap((n) => Math.round(n * 100) / 100),
130
- );
131
-
132
- // Combine signals
133
- const firstName = signal("John");
134
- const lastName = signal("Doe");
135
- const fullName = spCombineLatest([firstName, lastName]).pipe(spMap(([first, last]) => `${first} ${last}`));
136
- ```
137
-
138
- ### Form Handling
139
-
140
- ```typescript
141
- import { spForm } from "ngx-signal-plus";
142
- import { computed } from "@angular/core";
143
-
144
- // Form inputs with validation
145
- const username = spForm.text("", { minLength: 3, maxLength: 20 });
146
- const email = spForm.email("");
147
- const age = spForm.number({ min: 18, max: 99, initial: 30 });
148
-
149
- // Form validation
150
- const isFormValid = computed(() => username.isValid() && email.isValid() && age.isValid());
151
- ```
152
-
153
- ### Form Groups
154
-
155
- Group multiple form controls together with aggregated state, validation, and persistence:
156
-
157
- ```typescript
158
- import { spFormGroup, spForm } from "ngx-signal-plus";
159
-
160
- // Basic form group
161
- const loginForm = spFormGroup({
162
- email: spForm.email(""),
163
- password: spForm.text("", { minLength: 8 }),
164
- });
165
-
166
- // Access aggregated state
167
- loginForm.isValid(); // false if password < 8 chars
168
- loginForm.isDirty(); // true if any field changed
169
- loginForm.isTouched(); // true if any field touched
170
- loginForm.value(); // { email: '', password: '' }
171
- loginForm.errors(); // { email: [...], password: [...] }
172
-
173
- // Update values
174
- loginForm.setValue({ email: "user@example.com", password: "secret123" });
175
- loginForm.patchValue({ email: "new@example.com" }); // Partial update
176
-
177
- // Form actions
178
- loginForm.reset(); // Reset all fields to initial values
179
- loginForm.markAsTouched(); // Mark all fields as touched
180
- loginForm.submit(); // Returns values if valid, null otherwise
181
-
182
- // Nested form groups
183
- const credentials = spFormGroup({
184
- email: spForm.email(""),
185
- password: spForm.text("", { minLength: 8 }),
186
- });
187
-
188
- const profile = spFormGroup({
189
- name: spForm.text(""),
190
- age: spForm.number({ min: 18 }),
191
- });
192
-
193
- const registrationForm = spFormGroup({
194
- credentials,
195
- profile,
196
- });
197
-
198
- // Group-level validation
199
- const passwordForm = spFormGroup(
200
- {
201
- password: spForm.text("password123"),
202
- confirmPassword: spForm.text("password123"),
203
- },
204
- {
205
- validators: [(values) => values.password === values.confirmPassword || "Passwords must match"],
206
- },
207
- );
208
-
209
- // Persistence
210
- const persistedForm = spFormGroup(
211
- {
212
- email: spForm.email(""),
213
- preferences: spForm.text(""),
214
- },
215
- {
216
- persistKey: "user-form", // Automatically saves/restores from localStorage
217
- },
218
- );
219
- ```
220
-
221
- ### Async State Management
222
-
223
- Manage asynchronous operations with built-in loading, error, and data states:
224
-
225
- ```typescript
226
- import { spAsync } from "ngx-signal-plus";
227
-
228
- const userData = spAsync<User>({
229
- fetcher: () => fetch("/api/user").then((r) => r.json()),
230
- initialValue: null,
231
- retryCount: 3,
232
- retryDelay: 1000,
233
- cacheTime: 5000,
234
- autoFetch: true,
235
- onSuccess: (data) => console.log("Loaded:", data),
236
- onError: (error) => console.error("Failed:", error),
237
- });
238
-
239
- // Reactive state signals
240
- userData.data(); // Signal<User | null>
241
- userData.loading(); // Signal<boolean>
242
- userData.error(); // Signal<Error | null>
243
- userData.isSuccess(); // Signal<boolean>
244
- userData.isError(); // Signal<boolean>
245
-
246
- // Methods
247
- await userData.refetch(); // Manually refetch data
248
- userData.invalidate(); // Mark cache as stale
249
- userData.reset(); // Reset to initial state
250
- userData.mutate(newData); // Optimistic update
251
- ```
252
-
253
- ### Reactive Queries
254
-
255
- ```typescript
256
- import { QueryClient, setGlobalQueryClient } from "ngx-signal-plus";
257
- import { spQuery, spMutation } from "ngx-signal-plus";
258
-
259
- const qc = new QueryClient();
260
- setGlobalQueryClient(qc);
261
-
262
- const todosQuery = spQuery({
263
- queryKey: ["todos"],
264
- queryFn: async () => fetch("/api/todos").then((r) => r.json()),
265
- staleTime: 5000,
266
- refetchOnWindowFocus: true,
267
- });
268
-
269
- const addTodo = spMutation({
270
- mutationFn: async (title: string) => postTodo(title),
271
- onMutate: (title) => {
272
- qc.setQueryData(["todos"], (prev) => [...((prev as { title: string }[] | undefined) ?? []), { title }], true);
273
- },
274
- onSuccess: () => qc.refetchQueries(["todos"]),
275
- });
276
- ```
277
-
278
- Highlights:
279
-
280
- - Cache-aware queries with invalidation and refetch
281
- - Mutations with optimistic updates
282
- - Interval/focus/reconnect refetch strategies
283
-
284
- ### Collection Management
285
-
286
- Manage arrays of entities with ID-based operations, optimized updates, and history support:
287
-
288
- ```typescript
289
- import { spCollection } from "ngx-signal-plus";
290
-
291
- interface Todo {
292
- id: string;
293
- title: string;
294
- completed: boolean;
295
- }
296
-
297
- const todos = spCollection<Todo>({
298
- idField: "id",
299
- initialValue: [],
300
- persist: "todos-key",
301
- withHistory: true,
302
- });
303
-
304
- // CRUD operations
305
- todos.add({ id: "1", title: "Learn Angular", completed: false });
306
- todos.addMany([todo1, todo2, todo3]);
307
- todos.update("1", { completed: true });
308
- todos.updateMany([
309
- { id: "1", changes: { completed: true } },
310
- { id: "2", changes: { title: "Updated" } },
311
- ]);
312
- todos.remove("1");
313
- todos.removeMany(["1", "2"]);
314
- todos.clear();
315
-
316
- // Query operations
317
- const todo = todos.findById("1");
318
- const completed = todos.filter((t) => t.completed);
319
- const firstCompleted = todos.find((t) => t.completed);
320
- const hasCompleted = todos.some((t) => t.completed);
321
- const allCompleted = todos.every((t) => t.completed);
322
-
323
- // Transform operations
324
- const sorted = todos.sort((a, b) => a.title.localeCompare(b.title));
325
- const titles = todos.map((t) => t.title);
326
- const totalCompleted = todos.reduce((acc, t) => acc + (t.completed ? 1 : 0), 0);
327
-
328
- // History operations
329
- todos.undo(); // Undo last operation
330
- todos.redo(); // Redo last undone operation
331
- todos.canUndo(); // Check if undo is available
332
- todos.canRedo(); // Check if redo is available
333
-
334
- // Reactive signals
335
- todos.value(); // Signal<Todo[]>
336
- todos.count(); // Signal<number>
337
- todos.isEmpty(); // Signal<boolean>
338
- ```
339
-
340
- ### Validation and Presets
341
-
342
- ```typescript
343
- import { spValidators, spPresets } from "ngx-signal-plus";
344
-
345
- // Use built-in validators
346
- const email = sp("").validate(spValidators.string.required).validate(spValidators.string.email).build();
347
-
348
- // Use presets for common patterns
349
- const counter = spPresets.counter({
350
- initial: 0,
351
- min: 0,
352
- max: 100,
353
- step: 1,
354
- withHistory: true,
355
- });
356
-
357
- const darkMode = spPresets.toggle({
358
- initial: false,
359
- persistent: true,
360
- storageKey: "theme-mode",
361
- });
362
- ```
363
-
364
- ### State Management
365
-
366
- ```typescript
367
- import { spStorageManager, sp } from "ngx-signal-plus";
368
-
369
- // Storage management (saves to localStorage with namespace prefix)
370
- spStorageManager.save("app-settings", { theme: "dark", language: "en" });
371
- const settings = spStorageManager.load<{ theme: string; language: string }>("app-settings");
372
-
373
- // Remove when no longer needed
374
- spStorageManager.remove("app-settings");
375
-
376
- // History management through signals
377
- const counter = sp(0)
378
- .withHistory(10) // Keep last 10 values
379
- .build();
380
-
381
- counter.setValue(1);
382
- counter.setValue(2);
383
- counter.setValue(3);
384
-
385
- // Navigate history
386
- counter.undo(); // Back to 2
387
- counter.undo(); // Back to 1
388
- counter.redo(); // Forward to 2
389
-
390
- // Check history
391
- console.log(counter.history()); // Array of past values
392
- ```
393
-
394
- ### Cleanup and Memory Management
395
-
396
- **ngx-signal-plus** provides automatic and manual cleanup to prevent memory leaks:
397
-
398
- ```typescript
399
- import { sp } from "ngx-signal-plus";
400
-
401
- // Automatic cleanup when all subscribers unsubscribe
402
- const signal = sp(0).persist("counter").debounce(300).build();
403
- const unsubscribe = signal.subscribe((value) => console.log(value));
404
-
405
- // When you're done with the signal
406
- unsubscribe(); // Automatically cleans up when last subscriber unsubscribes
407
-
408
- // Manual cleanup with destroy()
409
- const signal2 = sp(0).persist("data").withHistory(10).build();
410
- signal2.setValue(42);
411
-
412
- // Explicitly destroy and clean up all resources
413
- signal2.destroy(); // Removes event listeners, clears timers, frees memory
414
- ```
415
-
416
- **What gets cleaned up:**
417
-
418
- - ✅ Storage event listeners (for `localStorage` synchronization)
419
- - ✅ Debounce/throttle timers
420
- - ✅ All subscribers
421
- - ✅ Pending operations
422
-
423
- **SSR-Safe:** All cleanup operations work safely in server-side rendering environments.
424
-
425
- ### Transactions and Batching
426
-
427
- Group multiple updates together with automatic rollback on errors:
428
-
429
- ```typescript
430
- import { spTransaction, spBatch } from "ngx-signal-plus";
431
-
432
- const balance = sp(100).build();
433
- const cart = sp<string[]>([]).build();
434
-
435
- // Transaction with automatic rollback
436
- try {
437
- spTransaction(() => {
438
- balance.setValue(balance.value() - 50);
439
- cart.update((items) => [...items, "premium-item"]);
440
-
441
- if (balance.value() < 0) {
442
- throw new Error("Insufficient funds");
443
- }
444
- // Success - changes are committed
445
- });
446
- } catch (error) {
447
- // Error - all changes automatically rolled back
448
- console.log(balance.value()); // 100 (original value)
449
- console.log(cart.value()); // [] (original value)
450
- }
451
-
452
- // Batch updates for performance (no rollback)
453
- spBatch(() => {
454
- signal1.setValue(1);
455
- signal2.setValue(2);
456
- signal3.setValue(3);
457
- // All changes applied together efficiently
458
- });
459
- ```
460
-
461
- ### Server-Side Rendering
462
-
463
- The library works seamlessly with Angular Universal:
464
-
465
- ```typescript
466
- // This code works in both SSR and browser
467
- const userPrefs = sp({ theme: "dark" }).persist("user-preferences").build();
468
-
469
- // In SSR: works in-memory, localStorage calls are safely skipped
470
- // In browser: full persistence with localStorage
471
- ```
472
-
473
- What happens during SSR:
474
-
475
- - Signals work normally with in-memory state
476
- - localStorage operations are safely skipped (no errors)
477
- - State automatically persists once the app runs in the browser
478
-
479
- ## Available Features
480
-
481
- | Category | Features |
482
- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
483
- | **Signal Creation** | `sp`, `spCounter`, `spToggle`, `spForm` |
484
- | **Signal Enhancement** | `enhance`, validation, transformation, persistence, history |
485
- | **Signal Operators** | `spMap`, `spFilter`, `spDebounceTime`, `spThrottleTime`, `spDelay`, `spDistinctUntilChanged`, `spSkip`, `spTake`, `spMerge`, `spCombineLatest` |
486
- | **Form Groups** | `spFormGroup` - Group multiple controls with aggregated state, validation, and persistence |
487
- | **Async State Management** | `spAsync` - Manage asynchronous operations with loading, error, retry, and caching |
488
- | **Collection Management** | `spCollection` - Manage arrays of entities with ID-based CRUD, queries, transforms, and history |
489
- | **Transactions & Batching** | `spTransaction`, `spBatch`, `spIsTransactionActive`, `spIsInTransaction`, `spIsInBatch`, `spGetModifiedSignals` |
490
- | **Utilities** | `spValidators`, `spPresets` |
491
- | **State Management** | `spHistoryManager`, `spStorageManager` |
492
- | **Components** | `spSignalPlusComponent`, `spSignalPlusService`, `spSignalBuilder` |
493
-
494
- ## Bundle Size Optimization
495
-
496
- The library is built with tree-shaking and optimization in mind. You only pay for what you use.
497
-
498
- ### Modern Package Exports
499
-
500
- The package provides **modular exports** for selective importing:
501
-
502
- ```typescript
503
- // Import only what you need - tree-shaking removes unused code
504
-
505
- // Core signals only (~3KB gzipped)
506
- import { sp, spCounter, spToggle } from "ngx-signal-plus/core";
507
-
508
- // Operators only (~2KB gzipped)
509
- import { spMap, spFilter, spDebounceTime } from "ngx-signal-plus/operators";
510
-
511
- // Utilities only (~2KB gzipped)
512
- import { enhance, spValidators, spPresets } from "ngx-signal-plus/utils";
513
-
514
- // State managers (~1KB gzipped)
515
- import { spHistoryManager, spStorageManager } from "ngx-signal-plus";
516
-
517
- // Everything (~8KB gzipped)
518
- import { sp, spMap, spFilter, enhance, spValidators } from "ngx-signal-plus";
519
- ```
520
-
521
- ### Tree-Shaking Configuration
522
-
523
- The package is optimized for tree-shaking:
524
-
525
- - ✅ **`sideEffects: false`** in package.json - marks the library as side-effect free
526
- - ✅ **Modular exports** - separate entry points for each feature category
527
- - ✅ **ES2022 modules** - modern JavaScript with full tree-shaking support
528
- - ✅ **FESM bundles** - Flat ESM bundles for better optimization
529
- - ✅ **Individual entry points** for granular control:
530
- - `ngx-signal-plus/core` - Core signal creation
531
- - `ngx-signal-plus/operators` - Signal operators
532
- - `ngx-signal-plus/utils` - Utilities and validators
533
- - `ngx-signal-plus/models` - TypeScript types
534
-
535
- ### Best Practices for Minimal Bundle
536
-
537
- **1. Import only what you need:**
538
-
539
- ```typescript
540
- // ✅ Good - imports only used features
541
- import { sp, spCounter } from "ngx-signal-plus";
542
-
543
- // ❌ Avoid - imports everything even if unused
544
- import * as SignalPlus from "ngx-signal-plus";
545
- ```
546
-
547
- **2. Use named imports:**
548
-
549
- ```typescript
550
- // ✅ Good - tree-shaking can remove unused exports
551
- import { sp, spMap } from "ngx-signal-plus";
552
-
553
- // ❌ Less optimal - may import more than needed
554
- import SignalPlus from "ngx-signal-plus";
555
- ```
556
-
557
- **3. Import from specific entry points:**
558
-
559
- ```typescript
560
- // ✅ Good - direct import from feature module
561
- import { spMap, spFilter } from "ngx-signal-plus/operators";
562
-
563
- // ✅ Also good - barrel export handles tree-shaking
564
- import { spMap, spFilter } from "ngx-signal-plus";
565
- ```
566
-
567
- ### Typical Bundle Sizes
568
-
569
- | Feature Set | Size (gzipped) | Savings vs Full |
570
- | --------------- | -------------- | --------------- |
571
- | Just `sp()` | ~1.5 KB | -87% |
572
- | Core signals | ~3 KB | -62% |
573
- | + Operators | ~5 KB | -38% |
574
- | + All utilities | ~8 KB | 0% |
575
-
576
- ### Performance Impact
577
-
578
- - **Tree-shaking enabled**: Webpack, Vite, Rollup automatically remove unused code
579
- - **No performance penalty**: Modern bundlers handle optimization automatically
580
- - **Zero runtime overhead**: Only loaded features are included
581
-
582
- ## Documentation
583
-
584
- For detailed documentation including all features, API reference, and examples, see our [API Documentation](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md).
585
-
586
- ## Contributing
587
-
588
- Please read our [Contributing Guide](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/CONTRIBUTING.md).
589
-
590
- ## Support
591
-
592
- - [Documentation](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md)
593
- - [Issue Tracker](https://github.com/milad-hub/ngx-signal-plus/issues)
594
-
595
- ## License
596
-
597
- MIT
1
+ # ngx-signal-plus
2
+
3
+ A powerful utility library that enhances Angular Signals with additional features for robust state management.
4
+
5
+ ## Features
6
+
7
+ - Enhanced signal operations with built-in state tracking
8
+ - Type-safe validations and transformations
9
+ - Persistent storage with automatic serialization
10
+ - Time-based operations (debounce, throttle, delay)
11
+ - Signal operators for transformation and combination
12
+ - Built-in undo/redo functionality
13
+ - Form handling with validation
14
+ - Form groups with aggregated state and validation
15
+ - Async state management with loading, error, and retry logic
16
+ - Reactive Queries for server state (TanStack Query style)
17
+ - Collection management with ID-based CRUD operations
18
+ - Automatic cleanup and memory management
19
+ - Performance optimizations
20
+ - Transactions and batching for atomic operations
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install ngx-signal-plus
26
+ ```
27
+
28
+ ## Requirements
29
+
30
+ - Angular >= 16.0.0 (fully compatible with Angular 16-20)
31
+ - TypeScript >= 5.0.0
32
+
33
+ ## Basic Usage
34
+
35
+ ```typescript
36
+ import { Component } from "@angular/core";
37
+ import { sp, enhance, spMap, spFilter } from "ngx-signal-plus";
38
+ import { signal, computed } from "@angular/core";
39
+
40
+ @Component({
41
+ standalone: true,
42
+ selector: "app-counter",
43
+ template: `
44
+ <div>Count: {{ counter.value() }}</div>
45
+ <div>Doubled: {{ doubled() }}</div>
46
+ <button (click)="increment()">Increment</button>
47
+ <button (click)="decrement()">Decrement</button>
48
+
49
+ @if (counter.history().length > 0) {
50
+ <button (click)="counter.undo()">Undo</button>
51
+ }
52
+ `,
53
+ })
54
+ export class CounterComponent {
55
+ // Create an enhanced signal with persistence and history
56
+ counter = sp(0)
57
+ .persist("counter")
58
+ .withHistory(10)
59
+ .validate((value) => value >= 0)
60
+ .build();
61
+
62
+ // Use signal operators
63
+ doubled = computed(() => this.counter.value() * 2);
64
+
65
+ increment() {
66
+ this.counter.setValue(this.counter.value() + 1);
67
+ }
68
+
69
+ decrement() {
70
+ if (this.counter.value() > 0) {
71
+ this.counter.setValue(this.counter.value() - 1);
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Core Features
78
+
79
+ ### Signal Creation
80
+
81
+ ```typescript
82
+ import { sp, spCounter, spToggle, spForm } from "ngx-signal-plus";
83
+
84
+ // Simple enhanced signal
85
+ const name = sp("John").build();
86
+
87
+ // Counter with min/max validation
88
+ const counter = spCounter(0, { min: 0, max: 100 });
89
+
90
+ // Toggle (boolean) with persistence
91
+ const darkMode = spToggle(false, "theme-mode");
92
+
93
+ // Form input with validation
94
+ const username = spForm.text("", {
95
+ minLength: 3,
96
+ maxLength: 20,
97
+ debounce: 300,
98
+ });
99
+ ```
100
+
101
+ ### Signal Enhancement
102
+
103
+ Enhance existing signals with additional features:
104
+
105
+ ```typescript
106
+ import { enhance } from "ngx-signal-plus";
107
+ import { signal } from "@angular/core";
108
+
109
+ const enhanced = enhance(signal(0))
110
+ .persist("counter")
111
+ .validate((n) => n >= 0)
112
+ .transform(Math.round)
113
+ .withHistory(5)
114
+ .debounce(300)
115
+ .distinctUntilChanged()
116
+ .build();
117
+ ```
118
+
119
+ ### Signal Operators
120
+
121
+ ```typescript
122
+ import { spMap, spFilter, spDebounceTime, spCombineLatest } from "ngx-signal-plus";
123
+ import { signal } from "@angular/core";
124
+
125
+ // Transform values
126
+ const price = signal(100);
127
+ const withTax = price.pipe(
128
+ spMap((n) => n * 1.2),
129
+ spMap((n) => Math.round(n * 100) / 100),
130
+ );
131
+
132
+ // Combine signals
133
+ const firstName = signal("John");
134
+ const lastName = signal("Doe");
135
+ const fullName = spCombineLatest([firstName, lastName]).pipe(spMap(([first, last]) => `${first} ${last}`));
136
+ ```
137
+
138
+ ### Form Handling
139
+
140
+ ```typescript
141
+ import { spForm } from "ngx-signal-plus";
142
+ import { computed } from "@angular/core";
143
+
144
+ // Form inputs with validation
145
+ const username = spForm.text("", { minLength: 3, maxLength: 20 });
146
+ const email = spForm.email("");
147
+ const age = spForm.number({ min: 18, max: 99, initial: 30 });
148
+
149
+ // Form validation
150
+ const isFormValid = computed(() => username.isValid() && email.isValid() && age.isValid());
151
+ ```
152
+
153
+ ### Form Groups
154
+
155
+ Group multiple form controls together with aggregated state, validation, and persistence:
156
+
157
+ ```typescript
158
+ import { spFormGroup, spForm } from "ngx-signal-plus";
159
+
160
+ // Basic form group
161
+ const loginForm = spFormGroup({
162
+ email: spForm.email(""),
163
+ password: spForm.text("", { minLength: 8 }),
164
+ });
165
+
166
+ // Access aggregated state
167
+ loginForm.isValid(); // false if password < 8 chars
168
+ loginForm.isDirty(); // true if any field changed
169
+ loginForm.isTouched(); // true if any field touched
170
+ loginForm.value(); // { email: '', password: '' }
171
+ loginForm.errors(); // { email: [...], password: [...] }
172
+
173
+ // Update values
174
+ loginForm.setValue({ email: "user@example.com", password: "secret123" });
175
+ loginForm.patchValue({ email: "new@example.com" }); // Partial update
176
+
177
+ // Form actions
178
+ loginForm.reset(); // Reset all fields to initial values
179
+ loginForm.markAsTouched(); // Mark all fields as touched
180
+ loginForm.submit(); // Returns values if valid, null otherwise
181
+
182
+ // Nested form groups
183
+ const credentials = spFormGroup({
184
+ email: spForm.email(""),
185
+ password: spForm.text("", { minLength: 8 }),
186
+ });
187
+
188
+ const profile = spFormGroup({
189
+ name: spForm.text(""),
190
+ age: spForm.number({ min: 18 }),
191
+ });
192
+
193
+ const registrationForm = spFormGroup({
194
+ credentials,
195
+ profile,
196
+ });
197
+
198
+ // Group-level validation
199
+ const passwordForm = spFormGroup(
200
+ {
201
+ password: spForm.text("password123"),
202
+ confirmPassword: spForm.text("password123"),
203
+ },
204
+ {
205
+ validators: [(values) => values.password === values.confirmPassword || "Passwords must match"],
206
+ },
207
+ );
208
+
209
+ // Persistence
210
+ const persistedForm = spFormGroup(
211
+ {
212
+ email: spForm.email(""),
213
+ preferences: spForm.text(""),
214
+ },
215
+ {
216
+ persistKey: "user-form", // Automatically saves/restores from localStorage
217
+ },
218
+ );
219
+ ```
220
+
221
+ ### Async State Management
222
+
223
+ Manage asynchronous operations with built-in loading, error, and data states:
224
+
225
+ ```typescript
226
+ import { spAsync } from "ngx-signal-plus";
227
+
228
+ const userData = spAsync<User>({
229
+ fetcher: () => fetch("/api/user").then((r) => r.json()),
230
+ initialValue: null,
231
+ retryCount: 3,
232
+ retryDelay: 1000,
233
+ cacheTime: 5000,
234
+ autoFetch: true,
235
+ onSuccess: (data) => console.log("Loaded:", data),
236
+ onError: (error) => console.error("Failed:", error),
237
+ });
238
+
239
+ // Reactive state signals
240
+ userData.data(); // Signal<User | null>
241
+ userData.loading(); // Signal<boolean>
242
+ userData.error(); // Signal<Error | null>
243
+ userData.isSuccess(); // Signal<boolean>
244
+ userData.isError(); // Signal<boolean>
245
+
246
+ // Methods
247
+ await userData.refetch(); // Manually refetch data
248
+ userData.invalidate(); // Mark cache as stale
249
+ userData.reset(); // Reset to initial state
250
+ userData.mutate(newData); // Optimistic update
251
+ ```
252
+
253
+ ### Reactive Queries
254
+
255
+ ```typescript
256
+ import { QueryClient, setGlobalQueryClient } from "ngx-signal-plus";
257
+ import { spQuery, spMutation } from "ngx-signal-plus";
258
+
259
+ const qc = new QueryClient();
260
+ setGlobalQueryClient(qc);
261
+
262
+ const todosQuery = spQuery({
263
+ queryKey: ["todos"],
264
+ queryFn: async () => fetch("/api/todos").then((r) => r.json()),
265
+ staleTime: 5000,
266
+ refetchOnWindowFocus: true,
267
+ });
268
+
269
+ const addTodo = spMutation({
270
+ mutationFn: async (title: string) => postTodo(title),
271
+ onMutate: (title) => {
272
+ qc.setQueryData(["todos"], (prev) => [...((prev as { title: string }[] | undefined) ?? []), { title }], true);
273
+ },
274
+ onSuccess: () => qc.refetchQueries(["todos"]),
275
+ });
276
+ ```
277
+
278
+ Highlights:
279
+
280
+ - Cache-aware queries with invalidation and refetch
281
+ - Mutations with optimistic updates
282
+ - Interval/focus/reconnect refetch strategies
283
+
284
+ ### Collection Management
285
+
286
+ Manage arrays of entities with ID-based operations, optimized updates, and history support:
287
+
288
+ ```typescript
289
+ import { spCollection } from "ngx-signal-plus";
290
+
291
+ interface Todo {
292
+ id: string;
293
+ title: string;
294
+ completed: boolean;
295
+ }
296
+
297
+ const todos = spCollection<Todo>({
298
+ idField: "id",
299
+ initialValue: [],
300
+ persist: "todos-key",
301
+ withHistory: true,
302
+ });
303
+
304
+ // CRUD operations
305
+ todos.add({ id: "1", title: "Learn Angular", completed: false });
306
+ todos.addMany([todo1, todo2, todo3]);
307
+ todos.update("1", { completed: true });
308
+ todos.updateMany([
309
+ { id: "1", changes: { completed: true } },
310
+ { id: "2", changes: { title: "Updated" } },
311
+ ]);
312
+ todos.remove("1");
313
+ todos.removeMany(["1", "2"]);
314
+ todos.clear();
315
+
316
+ // Query operations
317
+ const todo = todos.findById("1");
318
+ const completed = todos.filter((t) => t.completed);
319
+ const firstCompleted = todos.find((t) => t.completed);
320
+ const hasCompleted = todos.some((t) => t.completed);
321
+ const allCompleted = todos.every((t) => t.completed);
322
+
323
+ // Transform operations
324
+ const sorted = todos.sort((a, b) => a.title.localeCompare(b.title));
325
+ const titles = todos.map((t) => t.title);
326
+ const totalCompleted = todos.reduce((acc, t) => acc + (t.completed ? 1 : 0), 0);
327
+
328
+ // History operations
329
+ todos.undo(); // Undo last operation
330
+ todos.redo(); // Redo last undone operation
331
+ todos.canUndo(); // Check if undo is available
332
+ todos.canRedo(); // Check if redo is available
333
+
334
+ // Reactive signals
335
+ todos.value(); // Signal<Todo[]>
336
+ todos.count(); // Signal<number>
337
+ todos.isEmpty(); // Signal<boolean>
338
+ ```
339
+
340
+ ### Validation and Presets
341
+
342
+ ```typescript
343
+ import { spValidators, spPresets } from "ngx-signal-plus";
344
+
345
+ // Use built-in validators
346
+ const email = sp("").validate(spValidators.string.required).validate(spValidators.string.email).build();
347
+
348
+ // Use presets for common patterns
349
+ const counter = spPresets.counter({
350
+ initial: 0,
351
+ min: 0,
352
+ max: 100,
353
+ step: 1,
354
+ withHistory: true,
355
+ });
356
+
357
+ const darkMode = spPresets.toggle({
358
+ initial: false,
359
+ persistent: true,
360
+ storageKey: "theme-mode",
361
+ });
362
+ ```
363
+
364
+ ### State Management
365
+
366
+ ```typescript
367
+ import { spStorageManager, sp } from "ngx-signal-plus";
368
+
369
+ // Storage management (saves to localStorage with namespace prefix)
370
+ spStorageManager.save("app-settings", { theme: "dark", language: "en" });
371
+ const settings = spStorageManager.load<{ theme: string; language: string }>("app-settings");
372
+
373
+ // Remove when no longer needed
374
+ spStorageManager.remove("app-settings");
375
+
376
+ // History management through signals
377
+ const counter = sp(0)
378
+ .withHistory(10) // Keep last 10 values
379
+ .build();
380
+
381
+ counter.setValue(1);
382
+ counter.setValue(2);
383
+ counter.setValue(3);
384
+
385
+ // Navigate history
386
+ counter.undo(); // Back to 2
387
+ counter.undo(); // Back to 1
388
+ counter.redo(); // Forward to 2
389
+
390
+ // Check history
391
+ console.log(counter.history()); // Array of past values
392
+ ```
393
+
394
+ ### Cleanup and Memory Management
395
+
396
+ **ngx-signal-plus** provides automatic and manual cleanup to prevent memory leaks:
397
+
398
+ ```typescript
399
+ import { sp } from "ngx-signal-plus";
400
+
401
+ // Automatic cleanup when all subscribers unsubscribe
402
+ const signal = sp(0).persist("counter").debounce(300).build();
403
+ const unsubscribe = signal.subscribe((value) => console.log(value));
404
+
405
+ // When you're done with the signal
406
+ unsubscribe(); // Automatically cleans up when last subscriber unsubscribes
407
+
408
+ // Manual cleanup with destroy()
409
+ const signal2 = sp(0).persist("data").withHistory(10).build();
410
+ signal2.setValue(42);
411
+
412
+ // Explicitly destroy and clean up all resources
413
+ signal2.destroy(); // Removes event listeners, clears timers, frees memory
414
+ ```
415
+
416
+ **What gets cleaned up:**
417
+
418
+ - ✅ Storage event listeners (for `localStorage` synchronization)
419
+ - ✅ Debounce/throttle timers
420
+ - ✅ All subscribers
421
+ - ✅ Pending operations
422
+
423
+ **SSR-Safe:** All cleanup operations work safely in server-side rendering environments.
424
+
425
+ ### Transactions and Batching
426
+
427
+ Group multiple updates together with automatic rollback on errors:
428
+
429
+ ```typescript
430
+ import { spTransaction, spBatch } from "ngx-signal-plus";
431
+
432
+ const balance = sp(100).build();
433
+ const cart = sp<string[]>([]).build();
434
+
435
+ // Transaction with automatic rollback
436
+ try {
437
+ spTransaction(() => {
438
+ balance.setValue(balance.value() - 50);
439
+ cart.update((items) => [...items, "premium-item"]);
440
+
441
+ if (balance.value() < 0) {
442
+ throw new Error("Insufficient funds");
443
+ }
444
+ // Success - changes are committed
445
+ });
446
+ } catch (error) {
447
+ // Error - all changes automatically rolled back
448
+ console.log(balance.value()); // 100 (original value)
449
+ console.log(cart.value()); // [] (original value)
450
+ }
451
+
452
+ // Batch updates for performance (no rollback)
453
+ spBatch(() => {
454
+ signal1.setValue(1);
455
+ signal2.setValue(2);
456
+ signal3.setValue(3);
457
+ // All changes applied together efficiently
458
+ });
459
+ ```
460
+
461
+ ### Server-Side Rendering
462
+
463
+ The library works seamlessly with Angular Universal:
464
+
465
+ ```typescript
466
+ // This code works in both SSR and browser
467
+ const userPrefs = sp({ theme: "dark" }).persist("user-preferences").build();
468
+
469
+ // In SSR: works in-memory, localStorage calls are safely skipped
470
+ // In browser: full persistence with localStorage
471
+ ```
472
+
473
+ What happens during SSR:
474
+
475
+ - Signals work normally with in-memory state
476
+ - localStorage operations are safely skipped (no errors)
477
+ - State automatically persists once the app runs in the browser
478
+
479
+ ## Available Features
480
+
481
+ | Category | Features |
482
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
483
+ | **Signal Creation** | `sp`, `spCounter`, `spToggle`, `spForm` |
484
+ | **Signal Enhancement** | `enhance`, validation, transformation, persistence, history |
485
+ | **Signal Operators** | `spMap`, `spFilter`, `spDebounceTime`, `spThrottleTime`, `spDelay`, `spDistinctUntilChanged`, `spSkip`, `spTake`, `spMerge`, `spCombineLatest` |
486
+ | **Form Groups** | `spFormGroup` - Group multiple controls with aggregated state, validation, and persistence |
487
+ | **Async State Management** | `spAsync` - Manage asynchronous operations with loading, error, retry, and caching |
488
+ | **Collection Management** | `spCollection` - Manage arrays of entities with ID-based CRUD, queries, transforms, and history |
489
+ | **Transactions & Batching** | `spTransaction`, `spBatch`, `spIsTransactionActive`, `spIsInTransaction`, `spIsInBatch`, `spGetModifiedSignals` |
490
+ | **Utilities** | `spValidators`, `spPresets` |
491
+ | **State Management** | `spHistoryManager`, `spStorageManager` |
492
+ | **Components** | `spSignalPlusComponent`, `spSignalPlusService`, `spSignalBuilder` |
493
+
494
+ ## Bundle Size Optimization
495
+
496
+ The library is built with tree-shaking and optimization in mind. You only pay for what you use.
497
+
498
+ ### Modern Package Exports
499
+
500
+ The package provides **modular exports** for selective importing:
501
+
502
+ ```typescript
503
+ // Import only what you need - tree-shaking removes unused code
504
+
505
+ // Core signals only (~3KB gzipped)
506
+ import { sp, spCounter, spToggle } from "ngx-signal-plus/core";
507
+
508
+ // Operators only (~2KB gzipped)
509
+ import { spMap, spFilter, spDebounceTime } from "ngx-signal-plus/operators";
510
+
511
+ // Utilities only (~2KB gzipped)
512
+ import { enhance, spValidators, spPresets } from "ngx-signal-plus/utils";
513
+
514
+ // State managers (~1KB gzipped)
515
+ import { spHistoryManager, spStorageManager } from "ngx-signal-plus";
516
+
517
+ // Everything (~8KB gzipped)
518
+ import { sp, spMap, spFilter, enhance, spValidators } from "ngx-signal-plus";
519
+ ```
520
+
521
+ ### Tree-Shaking Configuration
522
+
523
+ The package is optimized for tree-shaking:
524
+
525
+ - ✅ **`sideEffects: false`** in package.json - marks the library as side-effect free
526
+ - ✅ **Modular exports** - separate entry points for each feature category
527
+ - ✅ **ES2022 modules** - modern JavaScript with full tree-shaking support
528
+ - ✅ **FESM bundles** - Flat ESM bundles for better optimization
529
+ - ✅ **Individual entry points** for granular control:
530
+ - `ngx-signal-plus/core` - Core signal creation
531
+ - `ngx-signal-plus/operators` - Signal operators
532
+ - `ngx-signal-plus/utils` - Utilities and validators
533
+ - `ngx-signal-plus/models` - TypeScript types
534
+
535
+ ### Best Practices for Minimal Bundle
536
+
537
+ **1. Import only what you need:**
538
+
539
+ ```typescript
540
+ // ✅ Good - imports only used features
541
+ import { sp, spCounter } from "ngx-signal-plus";
542
+
543
+ // ❌ Avoid - imports everything even if unused
544
+ import * as SignalPlus from "ngx-signal-plus";
545
+ ```
546
+
547
+ **2. Use named imports:**
548
+
549
+ ```typescript
550
+ // ✅ Good - tree-shaking can remove unused exports
551
+ import { sp, spMap } from "ngx-signal-plus";
552
+
553
+ // ❌ Less optimal - may import more than needed
554
+ import SignalPlus from "ngx-signal-plus";
555
+ ```
556
+
557
+ **3. Import from specific entry points:**
558
+
559
+ ```typescript
560
+ // ✅ Good - direct import from feature module
561
+ import { spMap, spFilter } from "ngx-signal-plus/operators";
562
+
563
+ // ✅ Also good - barrel export handles tree-shaking
564
+ import { spMap, spFilter } from "ngx-signal-plus";
565
+ ```
566
+
567
+ ### Typical Bundle Sizes
568
+
569
+ | Feature Set | Size (gzipped) | Savings vs Full |
570
+ | --------------- | -------------- | --------------- |
571
+ | Just `sp()` | ~1.5 KB | -87% |
572
+ | Core signals | ~3 KB | -62% |
573
+ | + Operators | ~5 KB | -38% |
574
+ | + All utilities | ~8 KB | 0% |
575
+
576
+ ### Performance Impact
577
+
578
+ - **Tree-shaking enabled**: Webpack, Vite, Rollup automatically remove unused code
579
+ - **No performance penalty**: Modern bundlers handle optimization automatically
580
+ - **Zero runtime overhead**: Only loaded features are included
581
+
582
+ ## Documentation
583
+
584
+ For detailed documentation including all features, API reference, and examples, see our [API Documentation](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md).
585
+
586
+ ## Contributing
587
+
588
+ Please read our [Contributing Guide](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/CONTRIBUTING.md).
589
+
590
+ ## Support
591
+
592
+ - [Documentation](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md)
593
+ - [Issue Tracker](https://github.com/milad-hub/ngx-signal-plus/issues)
594
+
595
+ ## License
596
+
597
+ MIT