ngx-signal-plus 2.4.0 → 2.4.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 +65 -621
- package/fesm2022/ngx-signal-plus.mjs +103 -3
- package/fesm2022/ngx-signal-plus.mjs.map +1 -1
- package/index.d.ts +114 -15
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
# ngx-signal-plus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://angular.dev/)
|
|
4
|
+
[](https://www.npmjs.com/package/ngx-signal-plus)
|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Bring validation, persistence, undo/redo, and reactive queries to Angular Signals on Angular 16+.
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
|
9
|
+
- Interactive playground: https://stackblitz.com/github/milad-hub/ngx-signal-plus
|
|
10
|
+
- Full API docs: https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md
|
|
11
|
+
- Repository README (contributors): https://github.com/milad-hub/ngx-signal-plus/blob/main/README.md
|
|
21
12
|
|
|
22
13
|
## Installation
|
|
23
14
|
|
|
@@ -27,643 +18,96 @@ npm install ngx-signal-plus
|
|
|
27
18
|
|
|
28
19
|
## Requirements
|
|
29
20
|
|
|
30
|
-
- Angular
|
|
31
|
-
- TypeScript
|
|
21
|
+
- Angular `>=16.0.0 <=21.0.0`
|
|
22
|
+
- TypeScript `>=5.0.0`
|
|
32
23
|
|
|
33
|
-
##
|
|
24
|
+
## Why this library?
|
|
25
|
+
|
|
26
|
+
| Capability | Angular native | ngx-signal-plus |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Signal validation and validation helpers | Limited | `sp().validate()`, presets, schema helpers |
|
|
29
|
+
| localStorage persistence | Manual | `sp().persist()` |
|
|
30
|
+
| Undo/redo history | Manual | `sp().withHistory()` |
|
|
31
|
+
| Transaction rollback | Manual | `spTransaction()` |
|
|
32
|
+
| Middleware/interceptors | No built-in | `spUseMiddleware()` |
|
|
33
|
+
| Query cache/retry/invalidation | `resource/httpResource` (basic) | `spQuery()`, `spMutation()`, `QueryClient` |
|
|
34
|
+
| Collection CRUD helpers | Manual | `spCollection()` |
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
34
37
|
|
|
35
38
|
```typescript
|
|
36
|
-
import { Component } from "@angular/core";
|
|
37
|
-
import { sp
|
|
38
|
-
import { signal, computed } from "@angular/core";
|
|
39
|
+
import { Component, computed } from "@angular/core";
|
|
40
|
+
import { sp } from "ngx-signal-plus";
|
|
39
41
|
|
|
40
42
|
@Component({
|
|
41
43
|
standalone: true,
|
|
42
44
|
selector: "app-counter",
|
|
43
45
|
template: `
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<button (click)="
|
|
47
|
-
<button (click)="
|
|
48
|
-
|
|
49
|
-
@if (counter.history().length > 0) {
|
|
46
|
+
<p>Count: {{ counter.value }}</p>
|
|
47
|
+
<p>Doubled: {{ doubled() }}</p>
|
|
48
|
+
<button (click)="inc()">+</button>
|
|
49
|
+
<button (click)="dec()">-</button>
|
|
50
|
+
@if (counter.history().length > 1) {
|
|
50
51
|
<button (click)="counter.undo()">Undo</button>
|
|
51
52
|
}
|
|
52
53
|
`,
|
|
53
54
|
})
|
|
54
55
|
export class CounterComponent {
|
|
55
|
-
|
|
56
|
-
|
|
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);
|
|
56
|
+
counter = sp(0).persist("counter").withHistory(10).validate((n) => n >= 0).build();
|
|
57
|
+
doubled = computed(() => this.counter.value * 2);
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
this.counter.setValue(this.counter.value
|
|
59
|
+
inc() {
|
|
60
|
+
this.counter.setValue(this.counter.value + 1);
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
if (this.counter.value
|
|
71
|
-
this.counter.setValue(this.counter.value() - 1);
|
|
72
|
-
}
|
|
63
|
+
dec() {
|
|
64
|
+
if (this.counter.value > 0) this.counter.setValue(this.counter.value - 1);
|
|
73
65
|
}
|
|
74
66
|
}
|
|
75
67
|
```
|
|
76
68
|
|
|
77
|
-
## Core
|
|
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
|
-
### Computed Signal Enhancement
|
|
120
|
-
|
|
121
|
-
Create computed signals with persistence, history, and validation:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
import { spComputed } from "ngx-signal-plus";
|
|
125
|
-
import { signal } from "@angular/core";
|
|
126
|
-
|
|
127
|
-
const firstName = signal("John");
|
|
128
|
-
const lastName = signal("Doe");
|
|
129
|
-
|
|
130
|
-
// Computed signal with history and persistence
|
|
131
|
-
const fullName = spComputed(() => `${firstName()} ${lastName()}`, { persist: "user-fullname", historySize: 5 });
|
|
132
|
-
|
|
133
|
-
fullName.value; // 'John Doe'
|
|
134
|
-
firstName.set("Jane");
|
|
135
|
-
fullName.value; // 'Jane Doe' (auto-updates)
|
|
136
|
-
fullName.undo(); // 'John Doe'
|
|
137
|
-
fullName.isValid(); // true
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Signal Operators
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
import { spMap, spFilter, spDebounceTime, spCombineLatest } from "ngx-signal-plus";
|
|
144
|
-
import { signal } from "@angular/core";
|
|
145
|
-
|
|
146
|
-
// Transform values
|
|
147
|
-
const price = signal(100);
|
|
148
|
-
const withTax = price.pipe(
|
|
149
|
-
spMap((n) => n * 1.2),
|
|
150
|
-
spMap((n) => Math.round(n * 100) / 100),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Combine signals
|
|
154
|
-
const firstName = signal("John");
|
|
155
|
-
const lastName = signal("Doe");
|
|
156
|
-
const fullName = spCombineLatest([firstName, lastName]).pipe(spMap(([first, last]) => `${first} ${last}`));
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Form Handling
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
import { spForm } from "ngx-signal-plus";
|
|
163
|
-
import { computed } from "@angular/core";
|
|
164
|
-
|
|
165
|
-
// Form inputs with validation
|
|
166
|
-
const username = spForm.text("", { minLength: 3, maxLength: 20 });
|
|
167
|
-
const email = spForm.email("");
|
|
168
|
-
const age = spForm.number({ min: 18, max: 99, initial: 30 });
|
|
169
|
-
|
|
170
|
-
// Form validation
|
|
171
|
-
const isFormValid = computed(() => username.isValid() && email.isValid() && age.isValid());
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Form Groups
|
|
175
|
-
|
|
176
|
-
Group multiple form controls together with aggregated state, validation, and persistence:
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
import { spFormGroup, spForm } from "ngx-signal-plus";
|
|
180
|
-
|
|
181
|
-
// Basic form group
|
|
182
|
-
const loginForm = spFormGroup({
|
|
183
|
-
email: spForm.email(""),
|
|
184
|
-
password: spForm.text("", { minLength: 8 }),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Access aggregated state
|
|
188
|
-
loginForm.isValid(); // false if password < 8 chars
|
|
189
|
-
loginForm.isDirty(); // true if any field changed
|
|
190
|
-
loginForm.isTouched(); // true if any field touched
|
|
191
|
-
loginForm.value(); // { email: '', password: '' }
|
|
192
|
-
loginForm.errors(); // { email: [...], password: [...] }
|
|
193
|
-
|
|
194
|
-
// Update values
|
|
195
|
-
loginForm.setValue({ email: "user@example.com", password: "secret123" });
|
|
196
|
-
loginForm.patchValue({ email: "new@example.com" }); // Partial update
|
|
197
|
-
|
|
198
|
-
// Form actions
|
|
199
|
-
loginForm.reset(); // Reset all fields to initial values
|
|
200
|
-
loginForm.markAsTouched(); // Mark all fields as touched
|
|
201
|
-
loginForm.submit(); // Returns values if valid, null otherwise
|
|
202
|
-
|
|
203
|
-
// Nested form groups
|
|
204
|
-
const credentials = spFormGroup({
|
|
205
|
-
email: spForm.email(""),
|
|
206
|
-
password: spForm.text("", { minLength: 8 }),
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const profile = spFormGroup({
|
|
210
|
-
name: spForm.text(""),
|
|
211
|
-
age: spForm.number({ min: 18 }),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const registrationForm = spFormGroup({
|
|
215
|
-
credentials,
|
|
216
|
-
profile,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Group-level validation
|
|
220
|
-
const passwordForm = spFormGroup(
|
|
221
|
-
{
|
|
222
|
-
password: spForm.text("password123"),
|
|
223
|
-
confirmPassword: spForm.text("password123"),
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
validators: [(values) => values.password === values.confirmPassword || "Passwords must match"],
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Persistence
|
|
231
|
-
const persistedForm = spFormGroup(
|
|
232
|
-
{
|
|
233
|
-
email: spForm.email(""),
|
|
234
|
-
preferences: spForm.text(""),
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
persistKey: "user-form", // Automatically saves/restores from localStorage
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Async State Management
|
|
243
|
-
|
|
244
|
-
Manage asynchronous operations with built-in loading, error, and data states:
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
import { spAsync } from "ngx-signal-plus";
|
|
248
|
-
|
|
249
|
-
const userData = spAsync<User>({
|
|
250
|
-
fetcher: () => fetch("/api/user").then((r) => r.json()),
|
|
251
|
-
initialValue: null,
|
|
252
|
-
retryCount: 3,
|
|
253
|
-
retryDelay: 1000,
|
|
254
|
-
cacheTime: 5000,
|
|
255
|
-
autoFetch: true,
|
|
256
|
-
onSuccess: (data) => console.log("Loaded:", data),
|
|
257
|
-
onError: (error) => console.error("Failed:", error),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Reactive state signals
|
|
261
|
-
userData.data(); // Signal<User | null>
|
|
262
|
-
userData.loading(); // Signal<boolean>
|
|
263
|
-
userData.error(); // Signal<Error | null>
|
|
264
|
-
userData.isSuccess(); // Signal<boolean>
|
|
265
|
-
userData.isError(); // Signal<boolean>
|
|
266
|
-
|
|
267
|
-
// Methods
|
|
268
|
-
await userData.refetch(); // Manually refetch data
|
|
269
|
-
userData.invalidate(); // Mark cache as stale
|
|
270
|
-
userData.reset(); // Reset to initial state
|
|
271
|
-
userData.mutate(newData); // Optimistic update
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Reactive Queries
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { QueryClient, setGlobalQueryClient } from "ngx-signal-plus";
|
|
278
|
-
import { spQuery, spMutation } from "ngx-signal-plus";
|
|
279
|
-
|
|
280
|
-
const qc = new QueryClient();
|
|
281
|
-
setGlobalQueryClient(qc);
|
|
282
|
-
|
|
283
|
-
const todosQuery = spQuery({
|
|
284
|
-
queryKey: ["todos"],
|
|
285
|
-
queryFn: async () => fetch("/api/todos").then((r) => r.json()),
|
|
286
|
-
staleTime: 5000,
|
|
287
|
-
refetchOnWindowFocus: true,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const addTodo = spMutation({
|
|
291
|
-
mutationFn: async (title: string) => postTodo(title),
|
|
292
|
-
onMutate: (title) => {
|
|
293
|
-
qc.setQueryData(["todos"], (prev) => [...((prev as { title: string }[] | undefined) ?? []), { title }], true);
|
|
294
|
-
},
|
|
295
|
-
onSuccess: () => qc.refetchQueries(["todos"]),
|
|
296
|
-
});
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
Highlights:
|
|
300
|
-
|
|
301
|
-
- Cache-aware queries with invalidation and refetch
|
|
302
|
-
- Mutations with optimistic updates
|
|
303
|
-
- Interval/focus/reconnect refetch strategies
|
|
304
|
-
|
|
305
|
-
### Collection Management
|
|
306
|
-
|
|
307
|
-
Manage arrays of entities with ID-based operations, optimized updates, and history support:
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
import { spCollection } from "ngx-signal-plus";
|
|
311
|
-
|
|
312
|
-
interface Todo {
|
|
313
|
-
id: string;
|
|
314
|
-
title: string;
|
|
315
|
-
completed: boolean;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const todos = spCollection<Todo>({
|
|
319
|
-
idField: "id",
|
|
320
|
-
initialValue: [],
|
|
321
|
-
persist: "todos-key",
|
|
322
|
-
withHistory: true,
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// CRUD operations
|
|
326
|
-
todos.add({ id: "1", title: "Learn Angular", completed: false });
|
|
327
|
-
todos.addMany([todo1, todo2, todo3]);
|
|
328
|
-
todos.update("1", { completed: true });
|
|
329
|
-
todos.updateMany([
|
|
330
|
-
{ id: "1", changes: { completed: true } },
|
|
331
|
-
{ id: "2", changes: { title: "Updated" } },
|
|
332
|
-
]);
|
|
333
|
-
todos.remove("1");
|
|
334
|
-
todos.removeMany(["1", "2"]);
|
|
335
|
-
todos.clear();
|
|
336
|
-
|
|
337
|
-
// Query operations
|
|
338
|
-
const todo = todos.findById("1");
|
|
339
|
-
const completed = todos.filter((t) => t.completed);
|
|
340
|
-
const firstCompleted = todos.find((t) => t.completed);
|
|
341
|
-
const hasCompleted = todos.some((t) => t.completed);
|
|
342
|
-
const allCompleted = todos.every((t) => t.completed);
|
|
343
|
-
|
|
344
|
-
// Transform operations
|
|
345
|
-
const sorted = todos.sort((a, b) => a.title.localeCompare(b.title));
|
|
346
|
-
const titles = todos.map((t) => t.title);
|
|
347
|
-
const totalCompleted = todos.reduce((acc, t) => acc + (t.completed ? 1 : 0), 0);
|
|
348
|
-
|
|
349
|
-
// History operations
|
|
350
|
-
todos.undo(); // Undo last operation
|
|
351
|
-
todos.redo(); // Redo last undone operation
|
|
352
|
-
todos.canUndo(); // Check if undo is available
|
|
353
|
-
todos.canRedo(); // Check if redo is available
|
|
354
|
-
|
|
355
|
-
// Reactive signals
|
|
356
|
-
todos.value(); // Signal<Todo[]>
|
|
357
|
-
todos.count(); // Signal<number>
|
|
358
|
-
todos.isEmpty(); // Signal<boolean>
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Validation and Presets
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
import { spValidators, spPresets } from "ngx-signal-plus";
|
|
365
|
-
|
|
366
|
-
// Use built-in validators
|
|
367
|
-
const email = sp("").validate(spValidators.string.required).validate(spValidators.string.email).build();
|
|
368
|
-
|
|
369
|
-
// Use presets for common patterns
|
|
370
|
-
const counter = spPresets.counter({
|
|
371
|
-
initial: 0,
|
|
372
|
-
min: 0,
|
|
373
|
-
max: 100,
|
|
374
|
-
step: 1,
|
|
375
|
-
withHistory: true,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const darkMode = spPresets.toggle({
|
|
379
|
-
initial: false,
|
|
380
|
-
persistent: true,
|
|
381
|
-
storageKey: "theme-mode",
|
|
382
|
-
});
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Schema Validation (Zod/Yup/Joi)
|
|
386
|
-
|
|
387
|
-
Use any schema validation library with signals:
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
import { sp, spSchema, spSchemaValidator } from "ngx-signal-plus";
|
|
391
|
-
import { z } from "zod";
|
|
392
|
-
|
|
393
|
-
const userSchema = z.object({
|
|
394
|
-
name: z.string().min(1),
|
|
395
|
-
email: z.string().email(),
|
|
396
|
-
age: z.number().min(18),
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// Basic boolean validation with SignalBuilder
|
|
400
|
-
const user = sp({ name: "", email: "", age: 0 }).validate(spSchema(userSchema)).build();
|
|
401
|
-
|
|
402
|
-
// Advanced: Get detailed error messages
|
|
403
|
-
const validator = spSchemaValidator(userSchema);
|
|
404
|
-
const result = validator.validateWithErrors({ name: "", email: "invalid", age: 10 });
|
|
405
|
-
// result: { valid: false, errors: ["name: String must contain at least 1 character(s)", "email: Invalid email", "age: Number must be greater than or equal to 18"] }
|
|
406
|
-
|
|
407
|
-
// Use with SignalBuilder for boolean validation
|
|
408
|
-
const validatedSignal = sp({ name: "", email: "", age: 0 }).validate(validator.validate).build();
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Middleware/Plugin System
|
|
412
|
-
|
|
413
|
-
Intercept signal operations for logging, analytics, and error tracking:
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
import { spUseMiddleware, spLoggerMiddleware, spAnalyticsMiddleware } from "ngx-signal-plus";
|
|
417
|
-
|
|
418
|
-
// Built-in logger middleware
|
|
419
|
-
spUseMiddleware(spLoggerMiddleware("[DEBUG]"));
|
|
420
|
-
|
|
421
|
-
// Custom analytics middleware
|
|
422
|
-
spUseMiddleware(
|
|
423
|
-
spAnalyticsMiddleware((event) => {
|
|
424
|
-
analytics.track("signal_change", event);
|
|
425
|
-
}),
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
// Custom middleware
|
|
429
|
-
spUseMiddleware({
|
|
430
|
-
name: "error-tracker",
|
|
431
|
-
onSet: (ctx) => console.log(`${ctx.signalName}: ${ctx.oldValue} -> ${ctx.newValue}`),
|
|
432
|
-
onError: (error) => Sentry.captureException(error),
|
|
433
|
-
});
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
### State Management
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
import { spStorageManager, sp } from "ngx-signal-plus";
|
|
440
|
-
|
|
441
|
-
// Storage management (saves to localStorage with namespace prefix)
|
|
442
|
-
spStorageManager.save("app-settings", { theme: "dark", language: "en" });
|
|
443
|
-
const settings = spStorageManager.load<{ theme: string; language: string }>("app-settings");
|
|
444
|
-
|
|
445
|
-
// Remove when no longer needed
|
|
446
|
-
spStorageManager.remove("app-settings");
|
|
447
|
-
|
|
448
|
-
// History management through signals
|
|
449
|
-
const counter = sp(0)
|
|
450
|
-
.withHistory(10) // Keep last 10 values
|
|
451
|
-
.build();
|
|
452
|
-
|
|
453
|
-
counter.setValue(1);
|
|
454
|
-
counter.setValue(2);
|
|
455
|
-
counter.setValue(3);
|
|
456
|
-
|
|
457
|
-
// Navigate history
|
|
458
|
-
counter.undo(); // Back to 2
|
|
459
|
-
counter.undo(); // Back to 1
|
|
460
|
-
counter.redo(); // Forward to 2
|
|
461
|
-
|
|
462
|
-
// Check history
|
|
463
|
-
console.log(counter.history()); // Array of past values
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Cleanup and Memory Management
|
|
467
|
-
|
|
468
|
-
**ngx-signal-plus** provides automatic and manual cleanup to prevent memory leaks:
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
import { sp } from "ngx-signal-plus";
|
|
472
|
-
|
|
473
|
-
// Automatic cleanup when all subscribers unsubscribe
|
|
474
|
-
const signal = sp(0).persist("counter").debounce(300).build();
|
|
475
|
-
const unsubscribe = signal.subscribe((value) => console.log(value));
|
|
476
|
-
|
|
477
|
-
// When you're done with the signal
|
|
478
|
-
unsubscribe(); // Automatically cleans up when last subscriber unsubscribes
|
|
479
|
-
|
|
480
|
-
// Manual cleanup with destroy()
|
|
481
|
-
const signal2 = sp(0).persist("data").withHistory(10).build();
|
|
482
|
-
signal2.setValue(42);
|
|
483
|
-
|
|
484
|
-
// Explicitly destroy and clean up all resources
|
|
485
|
-
signal2.destroy(); // Removes event listeners, clears timers, frees memory
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**What gets cleaned up:**
|
|
489
|
-
|
|
490
|
-
- ✅ Storage event listeners (for `localStorage` synchronization)
|
|
491
|
-
- ✅ Debounce/throttle timers
|
|
492
|
-
- ✅ All subscribers
|
|
493
|
-
- ✅ Pending operations
|
|
494
|
-
|
|
495
|
-
**SSR-Safe:** All cleanup operations work safely in server-side rendering environments.
|
|
496
|
-
|
|
497
|
-
### Transactions and Batching
|
|
498
|
-
|
|
499
|
-
Group multiple updates together with automatic rollback on errors:
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
import { spTransaction, spBatch } from "ngx-signal-plus";
|
|
503
|
-
|
|
504
|
-
const balance = sp(100).build();
|
|
505
|
-
const cart = sp<string[]>([]).build();
|
|
506
|
-
|
|
507
|
-
// Transaction with automatic rollback
|
|
508
|
-
try {
|
|
509
|
-
spTransaction(() => {
|
|
510
|
-
balance.setValue(balance.value() - 50);
|
|
511
|
-
cart.update((items) => [...items, "premium-item"]);
|
|
512
|
-
|
|
513
|
-
if (balance.value() < 0) {
|
|
514
|
-
throw new Error("Insufficient funds");
|
|
515
|
-
}
|
|
516
|
-
// Success - changes are committed
|
|
517
|
-
});
|
|
518
|
-
} catch (error) {
|
|
519
|
-
// Error - all changes automatically rolled back
|
|
520
|
-
console.log(balance.value()); // 100 (original value)
|
|
521
|
-
console.log(cart.value()); // [] (original value)
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Batch updates for performance (no rollback)
|
|
525
|
-
spBatch(() => {
|
|
526
|
-
signal1.setValue(1);
|
|
527
|
-
signal2.setValue(2);
|
|
528
|
-
signal3.setValue(3);
|
|
529
|
-
// All changes applied together efficiently
|
|
530
|
-
});
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Server-Side Rendering
|
|
534
|
-
|
|
535
|
-
The library works seamlessly with Angular Universal:
|
|
536
|
-
|
|
537
|
-
```typescript
|
|
538
|
-
// This code works in both SSR and browser
|
|
539
|
-
const userPrefs = sp({ theme: "dark" }).persist("user-preferences").build();
|
|
540
|
-
|
|
541
|
-
// In SSR: works in-memory, localStorage calls are safely skipped
|
|
542
|
-
// In browser: full persistence with localStorage
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
What happens during SSR:
|
|
69
|
+
## Core APIs
|
|
546
70
|
|
|
547
|
-
-
|
|
548
|
-
-
|
|
549
|
-
-
|
|
71
|
+
- Signal creation: `sp`, `spCounter`, `spToggle`, `spForm`, `spComputed`
|
|
72
|
+
- Signal enhancement: `enhance`
|
|
73
|
+
- Operators: `spMap`, `spFilter`, `spDebounceTime`, `spThrottleTime`, `spDelay`, `spDistinctUntilChanged`
|
|
74
|
+
- Forms and groups: `spForm`, `spFormGroup`
|
|
75
|
+
- Async helpers: `spAsync`, `spCollection`
|
|
76
|
+
- Reactive queries: `spQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`
|
|
77
|
+
- Transactions: `spTransaction`, `spBatch`
|
|
78
|
+
- Schema validation: `spSchema`, `spSchemaValidator`
|
|
79
|
+
- Middleware: `spUseMiddleware`, `spRemoveMiddleware`, `spLoggerMiddleware`, `spAnalyticsMiddleware`
|
|
550
80
|
|
|
551
|
-
##
|
|
81
|
+
## Comparisons
|
|
552
82
|
|
|
553
|
-
|
|
554
|
-
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
555
|
-
| **Signal Creation** | `sp`, `spCounter`, `spToggle`, `spForm`, `spComputed` |
|
|
556
|
-
| **Signal Enhancement** | `enhance`, validation, transformation, persistence, history |
|
|
557
|
-
| **Signal Operators** | `spMap`, `spFilter`, `spDebounceTime`, `spThrottleTime`, `spDelay`, `spDistinctUntilChanged`, `spSkip`, `spTake`, `spMerge`, `spCombineLatest` |
|
|
558
|
-
| **Form Groups** | `spFormGroup` - Group multiple controls with aggregated state, validation, and persistence |
|
|
559
|
-
| **Async State Management** | `spAsync` - Manage asynchronous operations with loading, error, retry, and caching |
|
|
560
|
-
| **Collection Management** | `spCollection` - Manage arrays of entities with ID-based CRUD, queries, transforms, and history |
|
|
561
|
-
| **Transactions & Batching** | `spTransaction`, `spBatch`, `spIsTransactionActive`, `spIsInTransaction`, `spIsInBatch`, `spGetModifiedSignals` |
|
|
562
|
-
| **Utilities** | `spValidators`, `spPresets`, `spSchema`, `spSchemaValidator` |
|
|
563
|
-
| **Middleware/Plugins** | `spUseMiddleware`, `spRemoveMiddleware`, `spLoggerMiddleware`, `spAnalyticsMiddleware` |
|
|
564
|
-
| **State Management** | `spHistoryManager`, `spStorageManager` |
|
|
565
|
-
| **Components** | `spSignalPlusComponent`, `spSignalPlusService`, `spSignalBuilder` |
|
|
83
|
+
### ngx-signal-plus vs Angular native signals
|
|
566
84
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
### Modern Package Exports
|
|
572
|
-
|
|
573
|
-
The package provides **modular exports** for selective importing:
|
|
574
|
-
|
|
575
|
-
```typescript
|
|
576
|
-
// Import only what you need - tree-shaking removes unused code
|
|
577
|
-
|
|
578
|
-
// Core signals only (~3KB gzipped)
|
|
579
|
-
import { sp, spCounter, spToggle } from "ngx-signal-plus/core";
|
|
580
|
-
|
|
581
|
-
// Operators only (~2KB gzipped)
|
|
582
|
-
import { spMap, spFilter, spDebounceTime } from "ngx-signal-plus/operators";
|
|
583
|
-
|
|
584
|
-
// Utilities only (~2KB gzipped)
|
|
585
|
-
import { enhance, spValidators, spPresets } from "ngx-signal-plus/utils";
|
|
586
|
-
|
|
587
|
-
// State managers (~1KB gzipped)
|
|
588
|
-
import { spHistoryManager, spStorageManager } from "ngx-signal-plus";
|
|
589
|
-
|
|
590
|
-
// Everything (~8KB gzipped)
|
|
591
|
-
import { sp, spMap, spFilter, enhance, spValidators } from "ngx-signal-plus";
|
|
592
|
-
```
|
|
85
|
+
- Angular provides core signal primitives (signal, computed, effect) and now also resource/httpResource for async patterns.
|
|
86
|
+
- ngx-signal-plus focuses on higher-level utilities on top of signals: validation, persistence, undo/redo, middleware, transactions, collections, and query-style helpers.
|
|
87
|
+
- Angular resource and httpResource are still marked experimental in Angular docs.
|
|
593
88
|
|
|
594
|
-
###
|
|
89
|
+
### ngx-signal-plus vs NgRx Signals (@ngrx/signals)
|
|
595
90
|
|
|
596
|
-
|
|
91
|
+
- NgRx Signals is a full state-management approach centered on Signal Store architecture (store features, methods/hooks, and structured app state patterns).
|
|
92
|
+
- ngx-signal-plus is intentionally lighter: composable utilities that keep you close to native Angular signal usage without adopting a full store architecture.
|
|
93
|
+
- @ngrx/signals is actively maintained (current npm line is 20.x).
|
|
597
94
|
|
|
598
|
-
|
|
599
|
-
- ✅ **Modular exports** - separate entry points for each feature category
|
|
600
|
-
- ✅ **ES2022 modules** - modern JavaScript with full tree-shaking support
|
|
601
|
-
- ✅ **FESM bundles** - Flat ESM bundles for better optimization
|
|
602
|
-
- ✅ **Individual entry points** for granular control:
|
|
603
|
-
- `ngx-signal-plus/core` - Core signal creation
|
|
604
|
-
- `ngx-signal-plus/operators` - Signal operators
|
|
605
|
-
- `ngx-signal-plus/utils` - Utilities and validators
|
|
606
|
-
- `ngx-signal-plus/models` - TypeScript types
|
|
95
|
+
### ngx-signal-plus vs TanStack Query (Angular)
|
|
607
96
|
|
|
608
|
-
|
|
97
|
+
- TanStack Query is a dedicated server-state library (fetching, cache lifecycle, invalidation, retries, mutations).
|
|
98
|
+
- The Angular adapter package is @tanstack/angular-query-experimental, and TanStack currently labels it experimental.
|
|
99
|
+
- ngx-signal-plus includes query-style capabilities inside one package that also covers local signal utilities.
|
|
609
100
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
```typescript
|
|
613
|
-
// ✅ Good - imports only used features
|
|
614
|
-
import { sp, spCounter } from "ngx-signal-plus";
|
|
615
|
-
|
|
616
|
-
// ❌ Avoid - imports everything even if unused
|
|
617
|
-
import * as SignalPlus from "ngx-signal-plus";
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
**2. Use named imports:**
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
// ✅ Good - tree-shaking can remove unused exports
|
|
624
|
-
import { sp, spMap } from "ngx-signal-plus";
|
|
625
|
-
|
|
626
|
-
// ❌ Less optimal - may import more than needed
|
|
627
|
-
import SignalPlus from "ngx-signal-plus";
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
**3. Import from specific entry points:**
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
// ✅ Good - direct import from feature module
|
|
634
|
-
import { spMap, spFilter } from "ngx-signal-plus/operators";
|
|
635
|
-
|
|
636
|
-
// ✅ Also good - barrel export handles tree-shaking
|
|
637
|
-
import { spMap, spFilter } from "ngx-signal-plus";
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### Typical Bundle Sizes
|
|
641
|
-
|
|
642
|
-
| Feature Set | Size (gzipped) | Savings vs Full |
|
|
643
|
-
| --------------- | -------------- | --------------- |
|
|
644
|
-
| Just `sp()` | ~1.5 KB | -87% |
|
|
645
|
-
| Core signals | ~3 KB | -62% |
|
|
646
|
-
| + Operators | ~5 KB | -38% |
|
|
647
|
-
| + All utilities | ~8 KB | 0% |
|
|
648
|
-
|
|
649
|
-
### Performance Impact
|
|
650
|
-
|
|
651
|
-
- **Tree-shaking enabled**: Webpack, Vite, Rollup automatically remove unused code
|
|
652
|
-
- **No performance penalty**: Modern bundlers handle optimization automatically
|
|
653
|
-
- **Zero runtime overhead**: Only loaded features are included
|
|
101
|
+
### ngx-signal-plus vs Akita
|
|
654
102
|
|
|
103
|
+
- Akita is a store-centric architecture built around RxJS stores/queries.
|
|
104
|
+
- ngx-signal-plus is signal-first and utility-first, designed for composable local/global signal state without store boilerplate.
|
|
105
|
+
- Akita is no longer actively evolving like modern signal-first tools: the npm package is old (8.0.1, last published years ago), and the GitHub repository is archived.
|
|
655
106
|
## Documentation
|
|
656
107
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
Please read our [Contributing Guide](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/CONTRIBUTING.md).
|
|
662
|
-
|
|
663
|
-
## Support
|
|
664
|
-
|
|
665
|
-
- [Documentation](https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md)
|
|
666
|
-
- [Issue Tracker](https://github.com/milad-hub/ngx-signal-plus/issues)
|
|
108
|
+
- API documentation: https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/docs/API.md
|
|
109
|
+
- Contributing guide: https://github.com/milad-hub/ngx-signal-plus/blob/main/projects/signal-plus/CONTRIBUTING.md
|
|
110
|
+
- Issues: https://github.com/milad-hub/ngx-signal-plus/issues
|
|
667
111
|
|
|
668
112
|
## License
|
|
669
113
|
|