ng-simple-state 19.0.5 → 20.1.1

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,964 +1,984 @@
1
- # NgSimpleState [![Build Status](https://app.travis-ci.com/nigrosimone/ng-simple-state.svg?branch=main)](https://app.travis-ci.com/nigrosimone/ng-simple-state) [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/ng-simple-state/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/ng-simple-state?branch=main) [![NPM version](https://img.shields.io/npm/v/ng-simple-state.svg)](https://www.npmjs.com/package/ng-simple-state) [![Maintainability](https://api.codeclimate.com/v1/badges/1bfc363a95053ecc3429/maintainability)](https://codeclimate.com/github/nigrosimone/ng-simple-state/maintainability)
2
-
3
- Simple state management in Angular with only Services and RxJS or Signal.
4
-
5
- ## Description
6
-
7
- Sharing state between components as simple as possible and leverage the good parts of component state and Angular's dependency injection system.
8
-
9
- See the demos:
10
- - [Counter](https://stackblitz.com/edit/demo-ng-simple-state?file=src%2Fapp%2Fapp.component.ts)
11
- - [Tour of heroes](https://stackblitz.com/edit/ng-simple-state-tour-of-heroes?file=src%2Fapp%2Fhero.service.ts)
12
- - [To Do List](https://stackblitz.com/edit/ng-simple-state-todo?file=src%2Fapp%2Fapp.component.ts)
13
-
14
- ## Get Started
15
-
16
- ### Step 1: install `ng-simple-state`
17
-
18
- ```bash
19
- npm i ng-simple-state
20
- ```
21
-
22
- ### Step 2: Import `provideNgSimpleState` into your providers
23
-
24
- `provideNgSimpleState` has some global optional config defined by `NgSimpleStateConfig` interface:
25
-
26
- | Option | Description | Default |
27
- | -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
28
- | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
29
- | *persistentStorage* | Set the persistent storage `local` or `session`. | undefined |
30
- | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
31
-
32
- _Side note: each store can be override the global configuration implementing `storeConfig()` method (see "Override global config")._
33
-
34
- ```ts
35
- import { isDevMode } from '@angular/core';
36
- import { bootstrapApplication } from '@angular/platform-browser';
37
-
38
- import { AppComponent } from './app.component';
39
- import { provideNgSimpleState } from 'ng-simple-state';
40
-
41
- bootstrapApplication(AppComponent, {
42
- providers: [
43
- provideNgSimpleState({
44
- enableDevTool: isDevMode(),
45
- persistentStorage: 'local'
46
- })
47
- ]
48
- });
49
- ```
50
-
51
- ### Step 3: Chose your store
52
-
53
- There are two type of store `NgSimpleStateBaseRxjsStore` based on RxJS `BehaviorSubject` and `NgSimpleStateBaseSignalStore` based on Angular `Signal`:
54
-
55
- - [RxJS Store](#rxjs-store)
56
- - [Signal Store](#signal-store)
57
-
58
- ## RxJS Store
59
-
60
- This is an example for a counter store in a `src/app/counter-store.ts` file.
61
- Obviously, you can create every store you want with every complexity you need.
62
-
63
- 1) Define your state interface, eg.:
64
-
65
- ```ts
66
- export interface CounterState {
67
- count: number;
68
- }
69
- ```
70
-
71
- 2) Define your store service by extending `NgSimpleStateBaseRxjsStore`, eg.:
72
-
73
- ```ts
74
- import { Injectable } from '@angular/core';
75
- import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
76
-
77
- export interface CounterState {
78
- count: number;
79
- }
80
-
81
- @Injectable()
82
- export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
83
-
84
- }
85
- ```
86
-
87
- 3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
88
-
89
- ```ts
90
- import { Injectable } from '@angular/core';
91
- import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
92
-
93
- export interface CounterState {
94
- count: number;
95
- }
96
-
97
- @Injectable()
98
- export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
99
-
100
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
101
- return {
102
- storeName: 'CounterStore'
103
- };
104
- }
105
-
106
- initialState(): CounterState {
107
- return {
108
- count: 0
109
- };
110
- }
111
-
112
- }
113
- ```
114
-
115
- 4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
116
-
117
- ```ts
118
- import { Injectable } from '@angular/core';
119
- import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
120
- import { Observable } from 'rxjs';
121
-
122
- export interface CounterState {
123
- count: number;
124
- }
125
-
126
- @Injectable()
127
- export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
128
-
129
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
130
- return {
131
- storeName: 'CounterStore'
132
- };
133
- }
134
-
135
- initialState(): CounterState {
136
- return {
137
- count: 0
138
- };
139
- }
140
-
141
- selectCount(): Observable<number> {
142
- return this.selectState(state => state.count);
143
- }
144
- }
145
- ```
146
-
147
- 5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:
148
-
149
- ```ts
150
- import { Injectable } from '@angular/core';
151
- import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
152
- import { Observable } from 'rxjs';
153
-
154
- export interface CounterState {
155
- count: number;
156
- }
157
-
158
- @Injectable()
159
- export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
160
-
161
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
162
- return {
163
- storeName: 'CounterStore'
164
- };
165
- }
166
-
167
- initialState(): CounterState {
168
- return {
169
- count: 0
170
- };
171
- }
172
-
173
- selectCount(): Observable<number> {
174
- return this.selectState(state => state.count);
175
- }
176
-
177
- increment(increment: number = 1): void {
178
- this.setState(state => ({ count: state.count + increment }));
179
- }
180
-
181
- decrement(decrement: number = 1): void {
182
- this.setState(state => ({ count: state.count - decrement }));
183
- }
184
- }
185
- ```
186
-
187
- #### Step 3: Inject your store into the providers, eg.:
188
-
189
- ```ts
190
- import { Component } from '@angular/core';
191
- import { CounterStore } from './counter-store';
192
-
193
- @Component({
194
- selector: 'app-root',
195
- imports: [CounterStore]
196
- })
197
- export class AppComponent {
198
-
199
- }
200
- ```
201
-
202
- #### Step 4: Use your store into the components, eg.:
203
-
204
- ```ts
205
- import { Component, inject } from '@angular/core';
206
- import { Observable } from 'rxjs';
207
- import { CounterStore } from './counter-store';
208
-
209
- @Component({
210
- selector: 'app-root',
211
- imports: [CounterStore],
212
- template: `
213
- <h1>Counter: {{ counter$ | async }}</h1>
214
- <button (click)="counterStore.decrement()">Decrement</button>
215
- <button (click)="counterStore.resetState()">Reset</button>
216
- <button (click)="counterStore.increment()">Increment</button>
217
- `,
218
- })
219
- export class AppComponent {
220
- public counterStore = inject(CounterStore);
221
- public counter$: Observable<number> = this.counterStore.selectCount();
222
- }
223
- ```
224
-
225
- #### That's all!
226
-
227
- ![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
228
-
229
- ### Manage component state without service
230
-
231
- If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseRxjsStore`:
232
-
233
- ```ts
234
- import { Component } from '@angular/core';
235
- import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
236
- import { Observable } from 'rxjs';
237
-
238
- export interface CounterState {
239
- count: number;
240
- }
241
-
242
- @Component({
243
- selector: 'app-counter',
244
- template: `
245
- {{counter$ | async}}
246
- <button (click)="increment()">+</button>
247
- <button (click)="decrement()">-</button>
248
- `
249
- })
250
- export class CounterComponent extends NgSimpleStateBaseRxjsStore<CounterState> {
251
-
252
- public counter$: Observable<number> = this.selectState(state => state.count);
253
-
254
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
255
- return {
256
- storeName: 'CounterComponent'
257
- };
258
- }
259
-
260
- initialState(): CounterState {
261
- return {
262
- count: 0
263
- };
264
- }
265
-
266
- increment(): void {
267
- this.setState(state => ({ count: state.count + 1 }));
268
- }
269
-
270
- decrement(): void {
271
- this.setState(state => ({ count: state.count - 1 }));
272
- }
273
- }
274
- ```
275
-
276
- ### Override global config
277
-
278
- If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:
279
-
280
- ```ts
281
- import { Injectable } from '@angular/core';
282
- import { NgSimpleStateStoreConfig } from 'ng-simple-state';
283
-
284
-
285
- @Injectable()
286
- export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
287
-
288
- override storeConfig(): NgSimpleStateStoreConfig<CounterState> {
289
- return {
290
- persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
291
- storeName: 'CounterStore2', // set a specific name for this store (must be be unique)
292
- }
293
- }
294
- }
295
- ```
296
-
297
- The options are defined by `NgSimpleStateStoreConfig` interface:
298
-
299
- | Option | Description | Default |
300
- | -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
301
- | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
302
- | *storeName* | The store name. | undefined |
303
- | *persistentStorage* | Set the persistent storage `local` or `session` | undefined |
304
- | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
305
-
306
-
307
- ### Testing
308
-
309
- `ng-simple-state` is simple to test. Eg.:
310
-
311
- ```ts
312
- import { TestBed } from '@angular/core/testing';
313
- import { provideNgSimpleState } from 'ng-simple-state';
314
- import { CounterStore } from './counter-store';
315
-
316
- describe('CounterStore', () => {
317
-
318
- let counterStore: CounterStore;
319
-
320
- beforeEach(() => {
321
- TestBed.configureTestingModule({
322
- providers: [
323
- provideNgSimpleState({
324
- enableDevTool: false
325
- }),
326
- CounterStore
327
- ]
328
- });
329
-
330
- counterStore = TestBed.inject(CounterStore);
331
- });
332
-
333
- it('initialState', () => {
334
- expect(counterStore.getCurrentState()).toEqual({ count: 0 });
335
- });
336
-
337
- it('increment', () => {
338
- counterStore.increment();
339
- expect(counterStore.getCurrentState()).toEqual({ count: 1 });
340
- });
341
-
342
- it('decrement', () => {
343
- counterStore.decrement();
344
- expect(counterStore.getCurrentState()).toEqual({ count: -1 });
345
- });
346
-
347
- it('selectCount', (done) => {
348
- counterStore.selectCount().subscribe(value => {
349
- expect(value).toBe(0);
350
- done();
351
- });
352
- });
353
-
354
- });
355
- ```
356
-
357
- ### Example: array store
358
-
359
- This is an example for a todo list store in a `src/app/todo-store.ts` file.
360
-
361
- ```ts
362
- import { Injectable } from '@angular/core';
363
- import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
364
- import { Observable } from 'rxjs';
365
-
366
- export interface Todo {
367
- id: number;
368
- name: string;
369
- completed: boolean;
370
- }
371
-
372
- export type TodoState = Array<Todo>;
373
-
374
- @Injectable()
375
- export class TodoStore extends NgSimpleStateBaseRxjsStore<TodoState> {
376
-
377
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
378
- return {
379
- storeName: 'TodoStore'
380
- };
381
- }
382
-
383
- initialState(): TodoState {
384
- return [];
385
- }
386
-
387
- add(todo: Omit<Todo, 'id'>): void {
388
- this.setState(state => [...state, {...todo, id: Date.now()}]);
389
- }
390
-
391
- delete(id: number): void {
392
- this.setState(state => state.filter(item => item.id !== id) );
393
- }
394
-
395
- setComplete(id: number, completed: boolean = true): void {
396
- this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
397
- }
398
- }
399
- ```
400
-
401
- usage:
402
-
403
- ```ts
404
- import { Component, inject } from '@angular/core';
405
- import { Observable } from 'rxjs';
406
- import { Todo, TodoStore } from './todo-store';
407
-
408
- @Component({
409
- selector: 'app-root',
410
- template: `
411
- <input #newTodo> <button (click)="todoStore.add({name: newTodo.value, completed: false})">Add todo</button>
412
- <ol>
413
- @for(todo of todoList$ | async; track todo.id) {
414
- <li>
415
- @if(todo.completed) {
416
-
417
- }
418
- {{ todo.name }}
419
- <button (click)="todoStore.setComplete(todo.id, !todo.completed)">Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}</button>
420
- <button (click)="todoStore.delete(todo.id)">Delete</button>
421
- </li>
422
- }
423
- </ol>
424
- `,
425
- providers: [TodoStore]
426
- })
427
- export class AppComponent {
428
- public todoStore = inject(TodoStore);
429
- public todoList$: Observable<Todo[]> = this.todoStore.selectState();
430
- }
431
- ```
432
-
433
-
434
- ### NgSimpleStateBaseRxjsStore API
435
-
436
- ```ts
437
- @Injectable()
438
- @Directive()
439
- export abstract class NgSimpleStateBaseRxjsStore<S extends object | Array<any>> implements OnDestroy {
440
-
441
- /**
442
- * Return the observable of the state
443
- * @returns Observable of the state
444
- */
445
- public get state(): BehaviorSubject<S>;
446
-
447
- /**
448
- * When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
449
- */
450
- ngOnDestroy(): void;
451
-
452
- /**
453
- * Reset store to first loaded store state:
454
- * - the last saved state
455
- * - otherwise the initial state provided from `initialState()` method.
456
- */
457
- resetState(): boolean;
458
-
459
- /**
460
- * Restart the store to initial state provided from `initialState()` method
461
- */
462
- restartState(): boolean;
463
-
464
- /**
465
- * Override this method for set a specific config for the store
466
- * @returns NgSimpleStateStoreConfig
467
- */
468
- storeConfig(): NgSimpleStateStoreConfig<S>;
469
-
470
- /**
471
- * Set into the store the initial state
472
- * @returns The state object
473
- */
474
- initialState(): S;
475
-
476
- /**
477
- * Select a store state
478
- * @param selectFn State selector (if not provided return full state)
479
- * @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
480
- * @returns Observable of the selected state
481
- */
482
- selectState<K>(selectFn?: (state: Readonly<S>) => K, comparator?: (previous: K, current: K) => boolean): Observable<K>;
483
-
484
- /**
485
- * Return the current store state (snapshot)
486
- * @returns The current state
487
- */
488
- getCurrentState(): Readonly<S>;
489
-
490
- /**
491
- * Return the first loaded store state:
492
- * the last saved state
493
- * otherwise the initial state provided from `initialState()` method.
494
- * @returns The first state
495
- */
496
- getFirstState(): Readonly<S> | null;
497
-
498
- /**
499
- * Set a new state
500
- * @param selectFn State reducer
501
- * @param actionName The action label into Redux DevTools (default is parent function name)
502
- * @returns True if the state is changed
503
- */
504
- setState(stateFn: (currentState: Readonly<S>) => Partial<S>, actionName?: string): boolean;
505
- }
506
- ```
507
- ## Signal Store
508
-
509
- This is an example for a counter store in a `src/app/counter-store.ts` file.
510
- Obviously, you can create every store you want with every complexity you need.
511
-
512
- 1) Define your state interface, eg.:
513
-
514
- ```ts
515
- export interface CounterState {
516
- count: number;
517
- }
518
- ```
519
-
520
- 2) Define your store service by extending `NgSimpleStateBaseSignalStore`, eg.:
521
-
522
- ```ts
523
- import { Injectable } from '@angular/core';
524
- import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
525
-
526
- export interface CounterState {
527
- count: number;
528
- }
529
-
530
- @Injectable()
531
- export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
532
-
533
- }
534
- ```
535
-
536
- 3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
537
-
538
- ```ts
539
- import { Injectable } from '@angular/core';
540
- import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
541
-
542
- export interface CounterState {
543
- count: number;
544
- }
545
-
546
- @Injectable()
547
- export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
548
-
549
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
550
- return {
551
- storeName: 'CounterStore'
552
- };
553
- }
554
-
555
- initialState(): CounterState {
556
- return {
557
- count: 0
558
- };
559
- }
560
-
561
- }
562
- ```
563
-
564
- 4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
565
-
566
- ```ts
567
- import { Injectable, Signal } from '@angular/core';
568
- import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
569
-
570
- export interface CounterState {
571
- count: number;
572
- }
573
-
574
- @Injectable()
575
- export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
576
-
577
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
578
- return {
579
- storeName: 'CounterStore'
580
- };
581
- }
582
-
583
- initialState(): CounterState {
584
- return {
585
- count: 0
586
- };
587
- }
588
-
589
- selectCount(): Signal<number> {
590
- return this.selectState(state => state.count);
591
- }
592
- }
593
- ```
594
-
595
- 5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:
596
-
597
- ```ts
598
- import { Injectable, Signal } from '@angular/core';
599
- import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
600
-
601
- export interface CounterState {
602
- count: number;
603
- }
604
-
605
- @Injectable()
606
- export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
607
-
608
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
609
- return {
610
- storeName: 'CounterStore'
611
- };
612
- }
613
-
614
- initialState(): CounterState {
615
- return {
616
- count: 0
617
- };
618
- }
619
-
620
- selectCount(): Signal<number> {
621
- return this.selectState(state => state.count);
622
- }
623
-
624
- increment(increment: number = 1): void {
625
- this.setState(state => ({ count: state.count + increment }));
626
- }
627
-
628
- decrement(decrement: number = 1): void {
629
- this.setState(state => ({ count: state.count - decrement }));
630
- }
631
- }
632
- ```
633
-
634
- #### Step 3: Inject your store into the providers, eg.:
635
-
636
- ```ts
637
- import { Component } from '@angular/core';
638
- import { CounterStore } from './counter-store';
639
-
640
- @Component({
641
- selector: 'app-root',
642
- imports: [CounterStore]
643
- })
644
- export class AppComponent {
645
-
646
- }
647
- ```
648
-
649
- #### Step 4: Use your store into the components, eg.:
650
-
651
- ```ts
652
- import { Component, Signal, inject } from '@angular/core';
653
- import { CounterStore } from './counter-store';
654
-
655
- @Component({
656
- selector: 'app-root',
657
- template: `
658
- <h1>Counter: {{ counterSig() }}</h1>
659
- <button (click)="counterStore.decrement()">Decrement</button>
660
- <button (click)="counterStore.resetState()">Reset</button>
661
- <button (click)="counterStore.increment()">Increment</button>
662
- `,
663
- })
664
- export class AppComponent {
665
- public counterStore = inject(CounterStore);
666
- public counterSig: Signal<number> = this.counterStore.selectCount();
667
- }
668
- ```
669
-
670
- #### That's all!
671
-
672
- ![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
673
-
674
- ### Manage component state without service
675
-
676
- If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseSignalStore`:
677
-
678
- ```ts
679
- import { Component, Signal } from '@angular/core';
680
- import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
681
-
682
- export interface CounterState {
683
- count: number;
684
- }
685
-
686
- @Component({
687
- selector: 'app-counter',
688
- template: `
689
- {{counterSig()}}
690
- <button (click)="increment()">+</button>
691
- <button (click)="decrement()">-</button>
692
- `
693
- })
694
- export class CounterComponent extends NgSimpleStateBaseSignalStore<CounterState> {
695
-
696
- public counterSig: Signal<number> = this.selectState(state => state.count);
697
-
698
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
699
- return {
700
- storeName: 'CounterComponent'
701
- };
702
- }
703
-
704
- initialState(): CounterState {
705
- return {
706
- count: 0
707
- };
708
- }
709
-
710
- increment(): void {
711
- this.setState(state => ({ count: state.count + 1 }));
712
- }
713
-
714
- decrement(): void {
715
- this.setState(state => ({ count: state.count - 1 }));
716
- }
717
- }
718
- ```
719
-
720
- ### Override global config
721
-
722
- If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:
723
-
724
- ```ts
725
- import { Injectable } from '@angular/core';
726
- import { NgSimpleStateStoreConfig } from 'ng-simple-state';
727
-
728
-
729
- @Injectable()
730
- export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
731
-
732
- override storeConfig(): NgSimpleStateStoreConfig<CounterState> {
733
- return {
734
- persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
735
- storeName: 'CounterStore2', // set a specific name for this store (must be be unique)
736
- }
737
- }
738
- }
739
- ```
740
-
741
- The options are defined by `NgSimpleStateStoreConfig` interface:
742
-
743
- | Option | Description | Default |
744
- | -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
745
- | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
746
- | *storeName* | The store name. | undefined |
747
- | *persistentStorage* | Set the persistent storage `local` or `session` | undefined |
748
- | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
749
-
750
- ### Testing
751
-
752
- `ng-simple-state` is simple to test. Eg.:
753
-
754
- ```ts
755
- import { TestBed } from '@angular/core/testing';
756
- import { provideNgSimpleState } from 'ng-simple-state';
757
- import { CounterStore } from './counter-store';
758
-
759
- describe('CounterStore', () => {
760
-
761
- let counterStore: CounterStore;
762
-
763
- beforeEach(() => {
764
- TestBed.configureTestingModule({
765
- providers: [
766
- provideNgSimpleState({
767
- enableDevTool: false
768
- }),
769
- CounterStore
770
- ]
771
- });
772
-
773
- counterStore = TestBed.inject(CounterStore);
774
- });
775
-
776
- it('initialState', () => {
777
- expect(counterStore.getCurrentState()).toEqual({ count: 0 });
778
- });
779
-
780
- it('increment', () => {
781
- counterStore.increment();
782
- expect(counterStore.getCurrentState()).toEqual({ count: 1 });
783
- });
784
-
785
- it('decrement', () => {
786
- counterStore.decrement();
787
- expect(counterStore.getCurrentState()).toEqual({ count: -1 });
788
- });
789
-
790
- it('selectCount', () => {
791
- const valueSig = counterStore.selectCount();
792
- expect(valueSig()).toBe(0);
793
- });
794
-
795
- });
796
- ```
797
-
798
- ### Example: array store
799
-
800
- This is an example for a todo list store in a `src/app/todo-store.ts` file.
801
-
802
- ```ts
803
- import { Injectable } from '@angular/core';
804
- import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
805
-
806
- export interface Todo {
807
- id: number;
808
- name: string;
809
- completed: boolean;
810
- }
811
-
812
- export type TodoState = Array<Todo>;
813
-
814
- @Injectable()
815
- export class TodoStore extends NgSimpleStateBaseSignalStore<TodoState> {
816
-
817
- storeConfig(): NgSimpleStateStoreConfig<CounterState> {
818
- return {
819
- storeName: 'TodoStore'
820
- };
821
- }
822
-
823
- initialState(): TodoState {
824
- return [];
825
- }
826
-
827
- add(todo: Omit<Todo, 'id'>): void {
828
- this.setState(state => [...state, {...todo, id: Date.now()}]);
829
- }
830
-
831
- delete(id: number): void {
832
- this.setState(state => state.filter(item => item.id !== id) );
833
- }
834
-
835
- setComplete(id: number, completed: boolean = true): void {
836
- this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
837
- }
838
- }
839
- ```
840
-
841
- usage:
842
-
843
- ```ts
844
- import { Component, Signal, inject } from '@angular/core';
845
- import { Todo, TodoStore } from './todo-store';
846
-
847
- @Component({
848
- selector: 'app-root',
849
- template: `
850
- <input #newTodo> <button (click)="todoStore.add({name: newTodo.value, completed: false})">Add todo</button>
851
- <ol>
852
- @for(todo of todoListSig() | async; track todo.id) {
853
- <li>
854
- @if(todo.completed) {
855
-
856
- }
857
- {{ todo.name }}
858
- <button (click)="todoStore.setComplete(todo.id, !todo.completed)">Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}</button>
859
- <button (click)="todoStore.delete(todo.id)">Delete</button>
860
- </li>
861
- }
862
- </ol>
863
- `,
864
- providers: [TodoStore]
865
- })
866
- export class AppComponent {
867
- public todoStore = inject(TodoStore);
868
- public todoListSig: Signal<Todo[]> = this.todoStore.selectState();
869
- }
870
- ```
871
-
872
-
873
- ### NgSimpleStateBaseSignalStore API
874
-
875
- ```ts
876
- @Injectable()
877
- @Directive()
878
- export abstract class NgSimpleStateBaseSignalStore<S extends object | Array<any>> implements OnDestroy {
879
-
880
- /**
881
- * Return the Signal of the state
882
- * @returns Signal of the state
883
- */
884
- public get state(): Signal<S>;
885
-
886
- /**
887
- * When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
888
- */
889
- ngOnDestroy(): void;
890
-
891
- /**
892
- * Reset store to first loaded store state:
893
- * - the last saved state
894
- * - otherwise the initial state provided from `initialState()` method.
895
- */
896
- resetState(): boolean;
897
-
898
- /**
899
- * Restart the store to initial state provided from `initialState()` method
900
- */
901
- restartState(): boolean;
902
-
903
- /**
904
- * Override this method for set a specific config for the store
905
- * @returns NgSimpleStateStoreConfig
906
- */
907
- storeConfig(): NgSimpleStateStoreConfig<S>;
908
-
909
- /**
910
- * Set into the store the initial state
911
- * @returns The state object
912
- */
913
- initialState(): S;
914
-
915
- /**
916
- * Select a store state
917
- * @param selectFn State selector (if not provided return full state)
918
- * @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
919
- * @returns Signal of the selected state
920
- */
921
- selectState<K>(selectFn?: (state: Readonly<S>) => K, comparator?: (previous: K, current: K) => boolean): Signal<K>;
922
-
923
- /**
924
- * Return the current store state (snapshot)
925
- * @returns The current state
926
- */
927
- getCurrentState(): Readonly<S>;
928
-
929
- /**
930
- * Return the first loaded store state:
931
- * the last saved state
932
- * otherwise the initial state provided from `initialState()` method.
933
- * @returns The first state
934
- */
935
- getFirstState(): Readonly<S> | null;
936
-
937
- /**
938
- * Set a new state
939
- * @param selectFn State reducer
940
- * @param actionName The action label into Redux DevTools (default is parent function name)
941
- * @returns True if the state is changed
942
- */
943
- setState(stateFn: (currentState: Readonly<S>) => Partial<S>, actionName?: string): boolean;
944
- }
945
- ```
946
-
947
- ## Alternatives
948
-
949
- Aren't you satisfied? there are some valid alternatives:
950
-
951
- - [@tinystate](https://www.npmjs.com/package/@tinystate/core)
952
- - [@ngxs](https://www.npmjs.com/package/@ngxs/store)
953
- ## Support
954
-
955
- This is an open-source project. Star this [repository](https://github.com/nigrosimone/ng-simple-state), if you like it, or even [donate](https://www.paypal.com/paypalme/snwp). Thank you so much!
956
-
957
- ## My other libraries
958
-
959
- I have published some other Angular libraries, take a look:
960
-
961
- - [NgHttpCaching: Cache for HTTP requests in Angular application](https://www.npmjs.com/package/ng-http-caching)
962
- - [NgGenericPipe: Generic pipe for Angular application for use a component method into component template.](https://www.npmjs.com/package/ng-generic-pipe)
963
- - [NgLet: Structural directive for sharing data as local variable into html component template](https://www.npmjs.com/package/ng-let)
964
- - [NgForTrackByProperty: Angular global trackBy property directive with strict type checking](https://www.npmjs.com/package/ng-for-track-by-property)
1
+ # NgSimpleState [![Build Status](https://app.travis-ci.com/nigrosimone/ng-simple-state.svg?branch=main)](https://app.travis-ci.com/nigrosimone/ng-simple-state) [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/ng-simple-state/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/ng-simple-state?branch=main) [![NPM version](https://img.shields.io/npm/v/ng-simple-state.svg)](https://www.npmjs.com/package/ng-simple-state) [![Maintainability](https://api.codeclimate.com/v1/badges/1bfc363a95053ecc3429/maintainability)](https://codeclimate.com/github/nigrosimone/ng-simple-state/maintainability)
2
+
3
+ Simple state management in Angular with only Services and RxJS or Signal.
4
+
5
+ ## Description
6
+
7
+ Sharing state between components as simple as possible and leverage the good parts of component state and Angular's dependency injection system.
8
+
9
+ See the demos:
10
+ - [Counter](https://stackblitz.com/edit/demo-ng-simple-state?file=src%2Fapp%2Fapp.component.ts)
11
+ - [Tour of heroes](https://stackblitz.com/edit/ng-simple-state-tour-of-heroes?file=src%2Fapp%2Fhero.service.ts)
12
+ - [To Do List](https://stackblitz.com/edit/ng-simple-state-todo?file=src%2Fapp%2Fapp.component.ts)
13
+
14
+ ## Get Started
15
+
16
+ ### Step 1: install `ng-simple-state`
17
+
18
+ ```bash
19
+ npm i ng-simple-state
20
+ ```
21
+
22
+ ### Step 2: Import `provideNgSimpleState` into your providers
23
+
24
+ `provideNgSimpleState` has some global optional config defined by `NgSimpleStateConfig` interface:
25
+
26
+ | Option | Description | Default |
27
+ | -------------------- | ----------------------------------------------------------------------------------------------- | ---------------- |
28
+ | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
29
+ | *persistentStorage* | Set the persistent storage `local` or `session`. | undefined |
30
+ | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
31
+ | *serializeState* | A function used to serialize the state to a string. | `JSON.stringify` |
32
+ | *deserializeState* | A function used to deserialize the state from a string. | `JSON.parse` |
33
+
34
+ _Side note: each store can be override the global configuration implementing `storeConfig()` method (see "Override global config")._
35
+
36
+ ```ts
37
+ import { isDevMode } from '@angular/core';
38
+ import { bootstrapApplication } from '@angular/platform-browser';
39
+
40
+ import { AppComponent } from './app.component';
41
+ import { provideNgSimpleState } from 'ng-simple-state';
42
+
43
+ bootstrapApplication(AppComponent, {
44
+ providers: [
45
+ provideNgSimpleState({
46
+ enableDevTool: isDevMode(),
47
+ persistentStorage: 'local'
48
+ })
49
+ ]
50
+ });
51
+ ```
52
+
53
+ ### Step 3: Chose your store
54
+
55
+ There are two type of store `NgSimpleStateBaseRxjsStore` based on RxJS `BehaviorSubject` and `NgSimpleStateBaseSignalStore` based on Angular `Signal`:
56
+
57
+ - [RxJS Store](#rxjs-store)
58
+ - [Signal Store](#signal-store)
59
+
60
+ ## RxJS Store
61
+
62
+ This is an example for a counter store in a `src/app/counter-store.ts` file.
63
+ Obviously, you can create every store you want with every complexity you need.
64
+
65
+ 1) Define your state interface, eg.:
66
+
67
+ ```ts
68
+ export interface CounterState {
69
+ count: number;
70
+ }
71
+ ```
72
+
73
+ 2) Define your store service by extending `NgSimpleStateBaseRxjsStore`, eg.:
74
+
75
+ ```ts
76
+ import { Injectable } from '@angular/core';
77
+ import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
78
+
79
+ export interface CounterState {
80
+ count: number;
81
+ }
82
+
83
+ @Injectable()
84
+ export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
85
+
86
+ }
87
+ ```
88
+
89
+ 3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
90
+
91
+ ```ts
92
+ import { Injectable } from '@angular/core';
93
+ import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
94
+
95
+ export interface CounterState {
96
+ count: number;
97
+ }
98
+
99
+ @Injectable()
100
+ export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
101
+
102
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
103
+ return {
104
+ storeName: 'CounterStore'
105
+ };
106
+ }
107
+
108
+ initialState(): CounterState {
109
+ return {
110
+ count: 0
111
+ };
112
+ }
113
+
114
+ }
115
+ ```
116
+
117
+ 4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
118
+
119
+ ```ts
120
+ import { Injectable } from '@angular/core';
121
+ import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
122
+ import { Observable } from 'rxjs';
123
+
124
+ export interface CounterState {
125
+ count: number;
126
+ }
127
+
128
+ @Injectable()
129
+ export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
130
+
131
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
132
+ return {
133
+ storeName: 'CounterStore'
134
+ };
135
+ }
136
+
137
+ initialState(): CounterState {
138
+ return {
139
+ count: 0
140
+ };
141
+ }
142
+
143
+ selectCount(): Observable<number> {
144
+ return this.selectState(state => state.count);
145
+ }
146
+ }
147
+ ```
148
+
149
+ 5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:
150
+
151
+ ```ts
152
+ import { Injectable } from '@angular/core';
153
+ import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
154
+ import { Observable } from 'rxjs';
155
+
156
+ export interface CounterState {
157
+ count: number;
158
+ }
159
+
160
+ @Injectable()
161
+ export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
162
+
163
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
164
+ return {
165
+ storeName: 'CounterStore'
166
+ };
167
+ }
168
+
169
+ initialState(): CounterState {
170
+ return {
171
+ count: 0
172
+ };
173
+ }
174
+
175
+ selectCount(): Observable<number> {
176
+ return this.selectState(state => state.count);
177
+ }
178
+
179
+ increment(increment: number = 1): void {
180
+ this.setState(state => ({ count: state.count + increment }));
181
+ }
182
+
183
+ decrement(decrement: number = 1): void {
184
+ this.setState(state => ({ count: state.count - decrement }));
185
+ }
186
+ }
187
+ ```
188
+
189
+ #### Step 3: Inject your store into the providers, eg.:
190
+
191
+ ```ts
192
+ import { Component } from '@angular/core';
193
+ import { CounterStore } from './counter-store';
194
+
195
+ @Component({
196
+ selector: 'app-root',
197
+ imports: [CounterStore]
198
+ })
199
+ export class AppComponent {
200
+
201
+ }
202
+ ```
203
+
204
+ #### Step 4: Use your store into the components, eg.:
205
+
206
+ ```ts
207
+ import { Component, inject } from '@angular/core';
208
+ import { Observable } from 'rxjs';
209
+ import { CounterStore } from './counter-store';
210
+
211
+ @Component({
212
+ selector: 'app-root',
213
+ imports: [CounterStore],
214
+ template: `
215
+ <h1>Counter: {{ counter$ | async }}</h1>
216
+ <button (click)="counterStore.decrement()">Decrement</button>
217
+ <button (click)="counterStore.resetState()">Reset</button>
218
+ <button (click)="counterStore.increment()">Increment</button>
219
+ `,
220
+ })
221
+ export class AppComponent {
222
+ public counterStore = inject(CounterStore);
223
+ public counter$: Observable<number> = this.counterStore.selectCount();
224
+ }
225
+ ```
226
+
227
+ #### That's all!
228
+
229
+ ![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
230
+
231
+ ### Manage component state without service
232
+
233
+ If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseRxjsStore`:
234
+
235
+ ```ts
236
+ import { Component } from '@angular/core';
237
+ import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
238
+ import { Observable } from 'rxjs';
239
+
240
+ export interface CounterState {
241
+ count: number;
242
+ }
243
+
244
+ @Component({
245
+ selector: 'app-counter',
246
+ template: `
247
+ {{counter$ | async}}
248
+ <button (click)="increment()">+</button>
249
+ <button (click)="decrement()">-</button>
250
+ `
251
+ })
252
+ export class CounterComponent extends NgSimpleStateBaseRxjsStore<CounterState> {
253
+
254
+ public counter$: Observable<number> = this.selectState(state => state.count);
255
+
256
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
257
+ return {
258
+ storeName: 'CounterComponent'
259
+ };
260
+ }
261
+
262
+ initialState(): CounterState {
263
+ return {
264
+ count: 0
265
+ };
266
+ }
267
+
268
+ increment(): void {
269
+ this.setState(state => ({ count: state.count + 1 }));
270
+ }
271
+
272
+ decrement(): void {
273
+ this.setState(state => ({ count: state.count - 1 }));
274
+ }
275
+ }
276
+ ```
277
+
278
+ ### Override global config
279
+
280
+ If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:
281
+
282
+ ```ts
283
+ import { Injectable } from '@angular/core';
284
+ import { NgSimpleStateStoreConfig } from 'ng-simple-state';
285
+
286
+
287
+ @Injectable()
288
+ export class CounterStore extends NgSimpleStateBaseRxjsStore<CounterState> {
289
+
290
+ override storeConfig(): NgSimpleStateStoreConfig<CounterState> {
291
+ return {
292
+ persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
293
+ storeName: 'CounterStore2', // set a specific name for this store (must be be unique)
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ The options are defined by `NgSimpleStateStoreConfig` interface:
300
+
301
+ | Option | Description | Default |
302
+ | -------------------- | ----------------------------------------------------------------------------------------------- | ---------------- |
303
+ | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
304
+ | *storeName* | The store name. | undefined |
305
+ | *persistentStorage* | Set the persistent storage `local` or `session` | undefined |
306
+ | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
307
+ | *serializeState* | A function used to serialize the state to a string. | `JSON.stringify` |
308
+ | *deserializeState* | A function used to deserialize the state from a string. | `JSON.parse` |
309
+
310
+
311
+ ### Testing
312
+
313
+ `ng-simple-state` is simple to test. Eg.:
314
+
315
+ ```ts
316
+ import { TestBed } from '@angular/core/testing';
317
+ import { provideNgSimpleState } from 'ng-simple-state';
318
+ import { CounterStore } from './counter-store';
319
+
320
+ describe('CounterStore', () => {
321
+
322
+ let counterStore: CounterStore;
323
+
324
+ beforeEach(() => {
325
+ TestBed.configureTestingModule({
326
+ providers: [
327
+ provideNgSimpleState({
328
+ enableDevTool: false
329
+ }),
330
+ CounterStore
331
+ ]
332
+ });
333
+
334
+ counterStore = TestBed.inject(CounterStore);
335
+ });
336
+
337
+ it('initialState', () => {
338
+ expect(counterStore.getCurrentState()).toEqual({ count: 0 });
339
+ });
340
+
341
+ it('increment', () => {
342
+ counterStore.increment();
343
+ expect(counterStore.getCurrentState()).toEqual({ count: 1 });
344
+ });
345
+
346
+ it('decrement', () => {
347
+ counterStore.decrement();
348
+ expect(counterStore.getCurrentState()).toEqual({ count: -1 });
349
+ });
350
+
351
+ it('selectCount', (done) => {
352
+ counterStore.selectCount().subscribe(value => {
353
+ expect(value).toBe(0);
354
+ done();
355
+ });
356
+ });
357
+
358
+ });
359
+ ```
360
+
361
+ ### Example: array store
362
+
363
+ This is an example for a todo list store in a `src/app/todo-store.ts` file.
364
+
365
+ ```ts
366
+ import { Injectable } from '@angular/core';
367
+ import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
368
+ import { Observable } from 'rxjs';
369
+
370
+ export interface Todo {
371
+ id: number;
372
+ name: string;
373
+ completed: boolean;
374
+ }
375
+
376
+ export type TodoState = Array<Todo>;
377
+
378
+ @Injectable()
379
+ export class TodoStore extends NgSimpleStateBaseRxjsStore<TodoState> {
380
+
381
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
382
+ return {
383
+ storeName: 'TodoStore'
384
+ };
385
+ }
386
+
387
+ initialState(): TodoState {
388
+ return [];
389
+ }
390
+
391
+ add(todo: Omit<Todo, 'id'>): void {
392
+ this.setState(state => [...state, {...todo, id: Date.now()}]);
393
+ }
394
+
395
+ delete(id: number): void {
396
+ this.setState(state => state.filter(item => item.id !== id) );
397
+ }
398
+
399
+ setComplete(id: number, completed: boolean = true): void {
400
+ this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
401
+ }
402
+ }
403
+ ```
404
+
405
+ usage:
406
+
407
+ ```ts
408
+ import { Component, inject } from '@angular/core';
409
+ import { Observable } from 'rxjs';
410
+ import { Todo, TodoStore } from './todo-store';
411
+
412
+ @Component({
413
+ selector: 'app-root',
414
+ template: `
415
+ <input #newTodo> <button (click)="todoStore.add({name: newTodo.value, completed: false})">Add todo</button>
416
+ <ol>
417
+ @for(todo of todoList$ | async; track todo.id) {
418
+ <li>
419
+ @if(todo.completed) {
420
+
421
+ }
422
+ {{ todo.name }}
423
+ <button (click)="todoStore.setComplete(todo.id, !todo.completed)">Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}</button>
424
+ <button (click)="todoStore.delete(todo.id)">Delete</button>
425
+ </li>
426
+ }
427
+ </ol>
428
+ `,
429
+ providers: [TodoStore]
430
+ })
431
+ export class AppComponent {
432
+ public todoStore = inject(TodoStore);
433
+ public todoList$: Observable<Todo[]> = this.todoStore.selectState();
434
+ }
435
+ ```
436
+
437
+
438
+ ### NgSimpleStateBaseRxjsStore API
439
+
440
+ ```ts
441
+ @Injectable()
442
+ @Directive()
443
+ export abstract class NgSimpleStateBaseRxjsStore<S extends object | Array<any>> implements OnDestroy {
444
+
445
+ /**
446
+ * Return the observable of the state
447
+ * @returns Observable of the state
448
+ */
449
+ public get state(): BehaviorSubject<S>;
450
+
451
+ /**
452
+ * When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
453
+ */
454
+ ngOnDestroy(): void;
455
+
456
+ /**
457
+ * Reset store to first loaded store state:
458
+ * - the last saved state
459
+ * - otherwise the initial state provided from `initialState()` method.
460
+ */
461
+ resetState(): boolean;
462
+
463
+ /**
464
+ * Restart the store to initial state provided from `initialState()` method
465
+ */
466
+ restartState(): boolean;
467
+
468
+ /**
469
+ * Override this method for set a specific config for the store
470
+ * @returns NgSimpleStateStoreConfig
471
+ */
472
+ storeConfig(): NgSimpleStateStoreConfig<S>;
473
+
474
+ /**
475
+ * Set into the store the initial state
476
+ * @returns The state object
477
+ */
478
+ initialState(): S;
479
+
480
+ /**
481
+ * Select a store state
482
+ * @param selectFn State selector (if not provided return full state)
483
+ * @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
484
+ * @returns Observable of the selected state
485
+ */
486
+ selectState<K>(selectFn?: (state: Readonly<S>) => K, comparator?: (previous: K, current: K) => boolean): Observable<K>;
487
+
488
+ /**
489
+ * Return the current store state (snapshot)
490
+ * @returns The current state
491
+ */
492
+ getCurrentState(): Readonly<S>;
493
+
494
+ /**
495
+ * Return the first loaded store state:
496
+ * the last saved state
497
+ * otherwise the initial state provided from `initialState()` method.
498
+ * @returns The first state
499
+ */
500
+ getFirstState(): Readonly<S> | null;
501
+
502
+ /**
503
+ * Set a new state
504
+ * @param newState New state
505
+ * @param actionName The action label into Redux DevTools (default is parent function name)
506
+ * @returns True if the state is changed
507
+ */
508
+ setState(newState: Partial<S>, actionName?: string): boolean;
509
+ /**
510
+ * Set a new state
511
+ * @param selectFn State reducer
512
+ * @param actionName The action label into Redux DevTools (default is parent function name)
513
+ * @returns True if the state is changed
514
+ */
515
+ setState(stateFn: NgSimpleStateSetState<S>, actionName?: string): boolean;
516
+ }
517
+ ```
518
+ ## Signal Store
519
+
520
+ This is an example for a counter store in a `src/app/counter-store.ts` file.
521
+ Obviously, you can create every store you want with every complexity you need.
522
+
523
+ 1) Define your state interface, eg.:
524
+
525
+ ```ts
526
+ export interface CounterState {
527
+ count: number;
528
+ }
529
+ ```
530
+
531
+ 2) Define your store service by extending `NgSimpleStateBaseSignalStore`, eg.:
532
+
533
+ ```ts
534
+ import { Injectable } from '@angular/core';
535
+ import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
536
+
537
+ export interface CounterState {
538
+ count: number;
539
+ }
540
+
541
+ @Injectable()
542
+ export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
543
+
544
+ }
545
+ ```
546
+
547
+ 3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
548
+
549
+ ```ts
550
+ import { Injectable } from '@angular/core';
551
+ import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
552
+
553
+ export interface CounterState {
554
+ count: number;
555
+ }
556
+
557
+ @Injectable()
558
+ export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
559
+
560
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
561
+ return {
562
+ storeName: 'CounterStore'
563
+ };
564
+ }
565
+
566
+ initialState(): CounterState {
567
+ return {
568
+ count: 0
569
+ };
570
+ }
571
+
572
+ }
573
+ ```
574
+
575
+ 4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
576
+
577
+ ```ts
578
+ import { Injectable, Signal } from '@angular/core';
579
+ import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
580
+
581
+ export interface CounterState {
582
+ count: number;
583
+ }
584
+
585
+ @Injectable()
586
+ export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
587
+
588
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
589
+ return {
590
+ storeName: 'CounterStore'
591
+ };
592
+ }
593
+
594
+ initialState(): CounterState {
595
+ return {
596
+ count: 0
597
+ };
598
+ }
599
+
600
+ selectCount(): Signal<number> {
601
+ return this.selectState(state => state.count);
602
+ }
603
+ }
604
+ ```
605
+
606
+ 5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:
607
+
608
+ ```ts
609
+ import { Injectable, Signal } from '@angular/core';
610
+ import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
611
+
612
+ export interface CounterState {
613
+ count: number;
614
+ }
615
+
616
+ @Injectable()
617
+ export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
618
+
619
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
620
+ return {
621
+ storeName: 'CounterStore'
622
+ };
623
+ }
624
+
625
+ initialState(): CounterState {
626
+ return {
627
+ count: 0
628
+ };
629
+ }
630
+
631
+ selectCount(): Signal<number> {
632
+ return this.selectState(state => state.count);
633
+ }
634
+
635
+ increment(increment: number = 1): void {
636
+ this.setState(state => ({ count: state.count + increment }));
637
+ }
638
+
639
+ decrement(decrement: number = 1): void {
640
+ this.setState(state => ({ count: state.count - decrement }));
641
+ }
642
+ }
643
+ ```
644
+
645
+ #### Step 3: Inject your store into the providers, eg.:
646
+
647
+ ```ts
648
+ import { Component } from '@angular/core';
649
+ import { CounterStore } from './counter-store';
650
+
651
+ @Component({
652
+ selector: 'app-root',
653
+ imports: [CounterStore]
654
+ })
655
+ export class AppComponent {
656
+
657
+ }
658
+ ```
659
+
660
+ #### Step 4: Use your store into the components, eg.:
661
+
662
+ ```ts
663
+ import { Component, Signal, inject } from '@angular/core';
664
+ import { CounterStore } from './counter-store';
665
+
666
+ @Component({
667
+ selector: 'app-root',
668
+ template: `
669
+ <h1>Counter: {{ counterSig() }}</h1>
670
+ <button (click)="counterStore.decrement()">Decrement</button>
671
+ <button (click)="counterStore.resetState()">Reset</button>
672
+ <button (click)="counterStore.increment()">Increment</button>
673
+ `,
674
+ })
675
+ export class AppComponent {
676
+ public counterStore = inject(CounterStore);
677
+ public counterSig: Signal<number> = this.counterStore.selectCount();
678
+ }
679
+ ```
680
+
681
+ #### That's all!
682
+
683
+ ![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
684
+
685
+ ### Manage component state without service
686
+
687
+ If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseSignalStore`:
688
+
689
+ ```ts
690
+ import { Component, Signal } from '@angular/core';
691
+ import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
692
+
693
+ export interface CounterState {
694
+ count: number;
695
+ }
696
+
697
+ @Component({
698
+ selector: 'app-counter',
699
+ template: `
700
+ {{counterSig()}}
701
+ <button (click)="increment()">+</button>
702
+ <button (click)="decrement()">-</button>
703
+ `
704
+ })
705
+ export class CounterComponent extends NgSimpleStateBaseSignalStore<CounterState> {
706
+
707
+ public counterSig: Signal<number> = this.selectState(state => state.count);
708
+
709
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
710
+ return {
711
+ storeName: 'CounterComponent'
712
+ };
713
+ }
714
+
715
+ initialState(): CounterState {
716
+ return {
717
+ count: 0
718
+ };
719
+ }
720
+
721
+ increment(): void {
722
+ this.setState(state => ({ count: state.count + 1 }));
723
+ }
724
+
725
+ decrement(): void {
726
+ this.setState(state => ({ count: state.count - 1 }));
727
+ }
728
+ }
729
+ ```
730
+
731
+ ### Override global config
732
+
733
+ If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:
734
+
735
+ ```ts
736
+ import { Injectable } from '@angular/core';
737
+ import { NgSimpleStateStoreConfig } from 'ng-simple-state';
738
+
739
+
740
+ @Injectable()
741
+ export class CounterStore extends NgSimpleStateBaseSignalStore<CounterState> {
742
+
743
+ override storeConfig(): NgSimpleStateStoreConfig<CounterState> {
744
+ return {
745
+ persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
746
+ storeName: 'CounterStore2', // set a specific name for this store (must be be unique)
747
+ }
748
+ }
749
+ }
750
+ ```
751
+
752
+ The options are defined by `NgSimpleStateStoreConfig` interface:
753
+
754
+ | Option | Description | Default |
755
+ | -------------------- | ----------------------------------------------------------------------------------------------- | ---------------- |
756
+ | *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
757
+ | *storeName* | The store name. | undefined |
758
+ | *persistentStorage* | Set the persistent storage `local` or `session` | undefined |
759
+ | *comparator* | A function used to compare the previous and current state for equality. | `a === b` |
760
+ | *serializeState* | A function used to serialize the state to a string. | `JSON.stringify` |
761
+ | *deserializeState* | A function used to deserialize the state from a string. | `JSON.parse` |
762
+
763
+ ### Testing
764
+
765
+ `ng-simple-state` is simple to test. Eg.:
766
+
767
+ ```ts
768
+ import { TestBed } from '@angular/core/testing';
769
+ import { provideNgSimpleState } from 'ng-simple-state';
770
+ import { CounterStore } from './counter-store';
771
+
772
+ describe('CounterStore', () => {
773
+
774
+ let counterStore: CounterStore;
775
+
776
+ beforeEach(() => {
777
+ TestBed.configureTestingModule({
778
+ providers: [
779
+ provideNgSimpleState({
780
+ enableDevTool: false
781
+ }),
782
+ CounterStore
783
+ ]
784
+ });
785
+
786
+ counterStore = TestBed.inject(CounterStore);
787
+ });
788
+
789
+ it('initialState', () => {
790
+ expect(counterStore.getCurrentState()).toEqual({ count: 0 });
791
+ });
792
+
793
+ it('increment', () => {
794
+ counterStore.increment();
795
+ expect(counterStore.getCurrentState()).toEqual({ count: 1 });
796
+ });
797
+
798
+ it('decrement', () => {
799
+ counterStore.decrement();
800
+ expect(counterStore.getCurrentState()).toEqual({ count: -1 });
801
+ });
802
+
803
+ it('selectCount', () => {
804
+ const valueSig = counterStore.selectCount();
805
+ expect(valueSig()).toBe(0);
806
+ });
807
+
808
+ });
809
+ ```
810
+
811
+ ### Example: array store
812
+
813
+ This is an example for a todo list store in a `src/app/todo-store.ts` file.
814
+
815
+ ```ts
816
+ import { Injectable } from '@angular/core';
817
+ import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';
818
+
819
+ export interface Todo {
820
+ id: number;
821
+ name: string;
822
+ completed: boolean;
823
+ }
824
+
825
+ export type TodoState = Array<Todo>;
826
+
827
+ @Injectable()
828
+ export class TodoStore extends NgSimpleStateBaseSignalStore<TodoState> {
829
+
830
+ storeConfig(): NgSimpleStateStoreConfig<CounterState> {
831
+ return {
832
+ storeName: 'TodoStore'
833
+ };
834
+ }
835
+
836
+ initialState(): TodoState {
837
+ return [];
838
+ }
839
+
840
+ add(todo: Omit<Todo, 'id'>): void {
841
+ this.setState(state => [...state, {...todo, id: Date.now()}]);
842
+ }
843
+
844
+ delete(id: number): void {
845
+ this.setState(state => state.filter(item => item.id !== id) );
846
+ }
847
+
848
+ setComplete(id: number, completed: boolean = true): void {
849
+ this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
850
+ }
851
+ }
852
+ ```
853
+
854
+ usage:
855
+
856
+ ```ts
857
+ import { Component, Signal, inject } from '@angular/core';
858
+ import { Todo, TodoStore } from './todo-store';
859
+
860
+ @Component({
861
+ selector: 'app-root',
862
+ template: `
863
+ <input #newTodo> <button (click)="todoStore.add({name: newTodo.value, completed: false})">Add todo</button>
864
+ <ol>
865
+ @for(todo of todoListSig(); track todo.id) {
866
+ <li>
867
+ @if(todo.completed) {
868
+
869
+ }
870
+ {{ todo.name }}
871
+ <button (click)="todoStore.setComplete(todo.id, !todo.completed)">Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}</button>
872
+ <button (click)="todoStore.delete(todo.id)">Delete</button>
873
+ </li>
874
+ }
875
+ </ol>
876
+ `,
877
+ providers: [TodoStore]
878
+ })
879
+ export class AppComponent {
880
+ public todoStore = inject(TodoStore);
881
+ public todoListSig: Signal<Todo[]> = this.todoStore.selectState();
882
+ }
883
+ ```
884
+
885
+
886
+ ### NgSimpleStateBaseSignalStore API
887
+
888
+ ```ts
889
+ @Injectable()
890
+ @Directive()
891
+ export abstract class NgSimpleStateBaseSignalStore<S extends object | Array<any>> implements OnDestroy {
892
+
893
+ /**
894
+ * Return the Signal of the state
895
+ * @returns Signal of the state
896
+ */
897
+ public get state(): Signal<S>;
898
+
899
+ /**
900
+ * When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
901
+ */
902
+ ngOnDestroy(): void;
903
+
904
+ /**
905
+ * Reset store to first loaded store state:
906
+ * - the last saved state
907
+ * - otherwise the initial state provided from `initialState()` method.
908
+ */
909
+ resetState(): boolean;
910
+
911
+ /**
912
+ * Restart the store to initial state provided from `initialState()` method
913
+ */
914
+ restartState(): boolean;
915
+
916
+ /**
917
+ * Override this method for set a specific config for the store
918
+ * @returns NgSimpleStateStoreConfig
919
+ */
920
+ storeConfig(): NgSimpleStateStoreConfig<S>;
921
+
922
+ /**
923
+ * Set into the store the initial state
924
+ * @returns The state object
925
+ */
926
+ initialState(): S;
927
+
928
+ /**
929
+ * Select a store state
930
+ * @param selectFn State selector (if not provided return full state)
931
+ * @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
932
+ * @returns Signal of the selected state
933
+ */
934
+ selectState<K>(selectFn?: (state: Readonly<S>) => K, comparator?: (previous: K, current: K) => boolean): Signal<K>;
935
+
936
+ /**
937
+ * Return the current store state (snapshot)
938
+ * @returns The current state
939
+ */
940
+ getCurrentState(): Readonly<S>;
941
+
942
+ /**
943
+ * Return the first loaded store state:
944
+ * the last saved state
945
+ * otherwise the initial state provided from `initialState()` method.
946
+ * @returns The first state
947
+ */
948
+ getFirstState(): Readonly<S> | null;
949
+
950
+ /**
951
+ * Set a new state
952
+ * @param newState New state
953
+ * @param actionName The action label into Redux DevTools (default is parent function name)
954
+ * @returns True if the state is changed
955
+ */
956
+ setState(newState: Partial<S>, actionName?: string): boolean;
957
+ /**
958
+ * Set a new state
959
+ * @param selectFn State reducer
960
+ * @param actionName The action label into Redux DevTools (default is parent function name)
961
+ * @returns True if the state is changed
962
+ */
963
+ setState(stateFn: NgSimpleStateSetState<S>, actionName?: string): boolean;
964
+ }
965
+ ```
966
+
967
+ ## Alternatives
968
+
969
+ Aren't you satisfied? there are some valid alternatives:
970
+
971
+ - [@tinystate](https://www.npmjs.com/package/@tinystate/core)
972
+ - [@ngxs](https://www.npmjs.com/package/@ngxs/store)
973
+ ## Support
974
+
975
+ This is an open-source project. Star this [repository](https://github.com/nigrosimone/ng-simple-state), if you like it, or even [donate](https://www.paypal.com/paypalme/snwp). Thank you so much!
976
+
977
+ ## My other libraries
978
+
979
+ I have published some other Angular libraries, take a look:
980
+
981
+ - [NgHttpCaching: Cache for HTTP requests in Angular application](https://www.npmjs.com/package/ng-http-caching)
982
+ - [NgGenericPipe: Generic pipe for Angular application for use a component method into component template.](https://www.npmjs.com/package/ng-generic-pipe)
983
+ - [NgLet: Structural directive for sharing data as local variable into html component template](https://www.npmjs.com/package/ng-let)
984
+ - [NgForTrackByProperty: Angular global trackBy property directive with strict type checking](https://www.npmjs.com/package/ng-for-track-by-property)