Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/nigrosimone/ng-simple-state
Simple state management in Angular with only Services and RxJS or Signal.
https://github.com/nigrosimone/ng-simple-state
angular angular2 rxjs state state-management
Last synced: 28 days ago
JSON representation
Simple state management in Angular with only Services and RxJS or Signal.
- Host: GitHub
- URL: https://github.com/nigrosimone/ng-simple-state
- Owner: nigrosimone
- License: mit
- Created: 2021-04-30T17:45:55.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-09-24T17:20:34.000Z (3 months ago)
- Last Synced: 2024-11-07T06:25:43.341Z (about 1 month ago)
- Topics: angular, angular2, rxjs, state, state-management
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/ng-simple-state
- Size: 3.09 MB
- Stars: 40
- Watchers: 3
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-angular - ng-simple-state - Simple state management in Angular with only Services and RxJS. (Table of contents / Third Party Components)
- fucking-awesome-angular - ng-simple-state - Simple state management in Angular with only Services and RxJS. (Table of contents / Third Party Components)
- fucking-awesome-angular - ng-simple-state - Simple state management in Angular with only Services and RxJS. (Table of contents / Third Party Components)
README
# 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)
Simple state management in Angular with only Services and RxJS or Signal.
## Description
Sharing state between components as simple as possible and leverage the good parts of component state and Angular's dependency injection system.
See the demos:
- [Counter](https://stackblitz.com/edit/demo-ng-simple-state?file=src%2Fapp%2Fapp.component.ts)
- [Tour of heroes](https://stackblitz.com/edit/ng-simple-state-tour-of-heroes?file=src%2Fapp%2Fhero.service.ts)
- [To Do List](https://stackblitz.com/edit/ng-simple-state-todo?file=src%2Fapp%2Fapp.component.ts)## Get Started
### Step 1: install `ng-simple-state`
```bash
npm i ng-simple-state
```### Step 2: Import `provideNgSimpleState` into your providers
`provideNgSimpleState` has some global optional config defined by `NgSimpleStateConfig` interface:
| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *persistentStorage* | Set the persistent storage `local` or `session`. | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |_Side note: each store can be override the global configuration implementing `storeConfig()` method (see "Override global config")._
```ts
import { isDevMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';import { AppComponent } from './app.component';
import { provideNgSimpleState } from 'ng-simple-state';bootstrapApplication(AppComponent, {
providers: [
provideNgSimpleState({
enableDevTool: isDevMode(),
enableLocalStorage: true,
persistentStorage: 'local'
})
]
});
```### Step 3: Chose your store
There are two type of store `NgSimpleStateBaseRxjsStore` based on RxJS `BehaviorSubject` and `NgSimpleStateBaseSignalStore` based on Angular `Signal`:
- [RxJS Store](#rxjs-store)
- [Signal Store](#signal-store)## RxJS Store
This is an example for a counter store in a `src/app/counter-store.ts` file.
Obviously, you can create every store you want with every complexity you need.1) Define your state interface, eg.:
```ts
export interface CounterState {
count: number;
}
```2) Define your store service by extending `NgSimpleStateBaseRxjsStore`, eg.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {
}
```3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}
initialState(): CounterState {
return {
count: 0
};
}}
```4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
import { Observable } from 'rxjs';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}
initialState(): CounterState {
return {
count: 0
};
}selectCount(): Observable {
return this.selectState(state => state.count);
}
}
```
5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
import { Observable } from 'rxjs';export interface CounterState {
count: number;
}@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}initialState(): CounterState {
return {
count: 0
};
}selectCount(): Observable {
return this.selectState(state => state.count);
}increment(increment: number = 1): void {
this.setState(state => ({ count: state.count + increment }));
}decrement(decrement: number = 1): void {
this.setState(state => ({ count: state.count - decrement }));
}
}
```#### Step 3: Inject your store into the providers, eg.:
```ts
import { Component } from '@angular/core';
import { CounterStore } from './counter-store';@Component({
selector: 'app-root',
imports: [CounterStore]
})
export class AppComponent {}
```#### Step 4: Use your store into the components, eg.:
```ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterStore } from './counter-store';@Component({
selector: 'app-root',
imports: [CounterStore],
template: `
Counter: {{ counter$ | async }}
Decrement
Reset
Increment
`,
})
export class AppComponent {
public counter$: Observable;constructor(public counterStore: CounterStore) {
this.counter$ = this.counterStore.selectCount();
}
}
```#### That's all!
![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
### Manage component state without service
If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseRxjsStore`:
```ts
import { Component } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
import { Observable } from 'rxjs';export interface CounterState {
count: number;
}@Component({
selector: 'ng-counter',
template: `
{{counter$ | async}}
+
-
`
})
export class CounterComponent extends NgSimpleStateBaseRxjsStore {public counter$: Observable = this.selectState(state => state.count);
storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterComponent'
};
}initialState(): CounterState {
return {
count: 0
};
}increment(): void {
this.setState(state => ({ count: state.count + 1 }));
}decrement(): void {
this.setState(state => ({ count: state.count - 1 }));
}
}
```### Override global config
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.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateStoreConfig } from 'ng-simple-state';@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {override storeConfig(): NgSimpleStateStoreConfig {
return {
enableLocalStorage: true, // enable local storage for this store
persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
storeName: 'CounterStore2', // For default the store name is the class name, you can set a specific name for this store (must be be unique)
}
}
}
```The options are defined by `NgSimpleStateStoreConfig` interface:
| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *storeName* | The name used into `Redux DevTools` and local storage key. | Class name |
| *persistentStorage* | Set the persistent storage `local` or `session` | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |### Testing
`ng-simple-state` is simple to test. Eg.:
```ts
import { TestBed } from '@angular/core/testing';
import { provideNgSimpleState } from 'ng-simple-state';
import { CounterStore } from './counter-store';describe('CounterStore', () => {
let counterStore: CounterStore;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideNgSimpleState({
enableDevTool: false,
enableLocalStorage: false
}),
CounterStore
]
});counterStore = TestBed.inject(CounterStore);
});it('initialState', () => {
expect(counterStore.getCurrentState()).toEqual({ count: 0 });
});it('increment', () => {
counterStore.increment();
expect(counterStore.getCurrentState()).toEqual({ count: 1 });
});it('decrement', () => {
counterStore.decrement();
expect(counterStore.getCurrentState()).toEqual({ count: -1 });
});it('selectCount', (done) => {
counterStore.selectCount().subscribe(value => {
expect(value).toBe(0);
done();
});
});});
```### Example: array store
This is an example for a todo list store in a `src/app/todo-store.ts` file.
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
import { Observable } from 'rxjs';export interface Todo {
id: number;
name: string;
completed: boolean;
}export type TodoState = Array;
@Injectable()
export class TodoStore extends NgSimpleStateBaseRxjsStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'TodoStore'
};
}initialState(): TodoState {
return [];
}add(todo: Omit): void {
this.setState(state => [...state, {...todo, id: Date.now()}]);
}delete(id: number): void {
this.setState(state => state.filter(item => item.id !== id) );
}setComplete(id: number, completed: boolean = true): void {
this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
}
}
```usage:
```ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Todo, TodoStore } from './todo-store';@Component({
selector: 'app-root',
template: `
Add todo
✅
{{ todo.name }}
Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}
Delete
`,
providers: [TodoStore]
})
export class AppComponent {
public todoList$: Observable;constructor(public todoStore: TodoStore) {
this.todoList$ = this.todoStore.selectState();
}
}
```### NgSimpleStateBaseRxjsStore API
```ts
@Injectable()
@Directive()
export abstract class NgSimpleStateBaseRxjsStore> implements OnDestroy {/**
* Return the observable of the state
* @returns Observable of the state
*/
public get state(): BehaviorSubject;/**
* When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
*/
ngOnDestroy(): void;/**
* Reset store to first loaded store state:
* - the last saved state, if `enableLocalStorage` config is `true`
* - otherwise the initial state provided from `initialState()` method.
*/
resetState(): boolean;/**
* Restart the store to initial state provided from `initialState()` method
*/
restartState(): boolean;/**
* Override this method for set a specific config for the store
* @returns NgSimpleStateStoreConfig
*/
storeConfig(): NgSimpleStateStoreConfig;/**
* Set into the store the initial state
* @returns The state object
*/
initialState(): S;/**
* Select a store state
* @param selectFn State selector (if not provided return full state)
* @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
* @returns Observable of the selected state
*/
selectState(selectFn?: (state: Readonly) => K, comparator?: (previous: K, current: K) => boolean): Observable;/**
* Return the current store state (snapshot)
* @returns The current state
*/
getCurrentState(): Readonly;/**
* Return the first loaded store state:
* the last saved state, if `enableLocalStorage` config is `true`;
* otherwise the initial state provided from `initialState()` method.
* @returns The first state
*/
getFirstState(): Readonly | null;/**
* Set a new state
* @param selectFn State reducer
* @param actionName The action label into Redux DevTools (default is parent function name)
* @returns True if the state is changed
*/
setState(stateFn: (currentState: Readonly) => Partial, actionName?: string): boolean;
}
```
## Signal StoreThis is an example for a counter store in a `src/app/counter-store.ts` file.
Obviously, you can create every store you want with every complexity you need.1) Define your state interface, eg.:
```ts
export interface CounterState {
count: number;
}
```2) Define your store service by extending `NgSimpleStateBaseSignalStore`, eg.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {
}
```3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}
initialState(): CounterState {
return {
count: 0
};
}}
```4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:
```ts
import { Injectable, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';export interface CounterState {
count: number;
}
@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}
initialState(): CounterState {
return {
count: 0
};
}selectCount(): Signal {
return this.selectState(state => state.count);
}
}
```
5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:```ts
import { Injectable, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';export interface CounterState {
count: number;
}@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}initialState(): CounterState {
return {
count: 0
};
}selectCount(): Signal {
return this.selectState(state => state.count);
}increment(increment: number = 1): void {
this.setState(state => ({ count: state.count + increment }));
}decrement(decrement: number = 1): void {
this.setState(state => ({ count: state.count - decrement }));
}
}
```#### Step 3: Inject your store into the providers, eg.:
```ts
import { Component } from '@angular/core';
import { CounterStore } from './counter-store';@Component({
selector: 'app-root',
imports: [CounterStore]
})
export class AppComponent {}
```#### Step 4: Use your store into the components, eg.:
```ts
import { Component, Signal } from '@angular/core';
import { CounterStore } from './counter-store';@Component({
selector: 'app-root',
template: `
Counter: {{ counterSig() }}
Decrement
Reset
Increment
`,
})
export class AppComponent {
public counterSig: Signal;constructor(public counterStore: CounterStore) {
this.counterSig = this.counterStore.selectCount();
}
}
```#### That's all!
![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)
### Manage component state without service
If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseSignalStore`:
```ts
import { Component, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';export interface CounterState {
count: number;
}@Component({
selector: 'ng-counter',
template: `
{{counterSig()}}
+
-
`
})
export class CounterComponent extends NgSimpleStateBaseSignalStore {public counterSig: Signal = this.selectState(state => state.count);
storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterComponent'
};
}initialState(): CounterState {
return {
count: 0
};
}increment(): void {
this.setState(state => ({ count: state.count + 1 }));
}decrement(): void {
this.setState(state => ({ count: state.count - 1 }));
}
}
```### Override global config
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.:
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateStoreConfig } from 'ng-simple-state';@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {override storeConfig(): NgSimpleStateStoreConfig {
return {
enableLocalStorage: true, // enable local storage for this store
persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
storeName: 'CounterStore2', // For default the store name is the class name, you can set a specific name for this store (must be be unique)
}
}
}
```The options are defined by `NgSimpleStateStoreConfig` interface:
| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *storeName* | The name used into `Redux DevTools` and local storage key. | Class name |
| *persistentStorage* | Set the persistent storage `local` or `session` | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |### Testing
`ng-simple-state` is simple to test. Eg.:
```ts
import { TestBed } from '@angular/core/testing';
import { provideNgSimpleState } from 'ng-simple-state';
import { CounterStore } from './counter-store';describe('CounterStore', () => {
let counterStore: CounterStore;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideNgSimpleState({
enableDevTool: false,
enableLocalStorage: false
}),
CounterStore
]
});counterStore = TestBed.inject(CounterStore);
});it('initialState', () => {
expect(counterStore.getCurrentState()).toEqual({ count: 0 });
});it('increment', () => {
counterStore.increment();
expect(counterStore.getCurrentState()).toEqual({ count: 1 });
});it('decrement', () => {
counterStore.decrement();
expect(counterStore.getCurrentState()).toEqual({ count: -1 });
});it('selectCount', () => {
const valueSig = counterStore.selectCount();
expect(valueSig()).toBe(0);
});});
```### Example: array store
This is an example for a todo list store in a `src/app/todo-store.ts` file.
```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';export interface Todo {
id: number;
name: string;
completed: boolean;
}export type TodoState = Array;
@Injectable()
export class TodoStore extends NgSimpleStateBaseSignalStore {storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'TodoStore'
};
}initialState(): TodoState {
return [];
}add(todo: Omit): void {
this.setState(state => [...state, {...todo, id: Date.now()}]);
}delete(id: number): void {
this.setState(state => state.filter(item => item.id !== id) );
}setComplete(id: number, completed: boolean = true): void {
this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
}
}
```usage:
```ts
import { Component, Signal } from '@angular/core';
import { Todo, TodoStore } from './todo-store';@Component({
selector: 'app-root',
template: `
Add todo
✅
{{ todo.name }}
Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}
Delete
`,
providers: [TodoStore]
})
export class AppComponent {
public todoListSig: Signal;constructor(public todoStore: TodoStore) {
this.todoListSig = this.todoStore.selectState();
}
}
```### NgSimpleStateBaseSignalStore API
```ts
@Injectable()
@Directive()
export abstract class NgSimpleStateBaseSignalStore> implements OnDestroy {/**
* Return the Signal of the state
* @returns Signal of the state
*/
public get state(): Signal;/**
* When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
*/
ngOnDestroy(): void;/**
* Reset store to first loaded store state:
* - the last saved state, if `enableLocalStorage` config is `true`
* - otherwise the initial state provided from `initialState()` method.
*/
resetState(): boolean;/**
* Restart the store to initial state provided from `initialState()` method
*/
restartState(): boolean;/**
* Override this method for set a specific config for the store
* @returns NgSimpleStateStoreConfig
*/
storeConfig(): NgSimpleStateStoreConfig;/**
* Set into the store the initial state
* @returns The state object
*/
initialState(): S;/**
* Select a store state
* @param selectFn State selector (if not provided return full state)
* @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
* @returns Signal of the selected state
*/
selectState(selectFn?: (state: Readonly) => K, comparator?: (previous: K, current: K) => boolean): Signal;/**
* Return the current store state (snapshot)
* @returns The current state
*/
getCurrentState(): Readonly;/**
* Return the first loaded store state:
* the last saved state, if `enableLocalStorage` config is `true`;
* otherwise the initial state provided from `initialState()` method.
* @returns The first state
*/
getFirstState(): Readonly | null;/**
* Set a new state
* @param selectFn State reducer
* @param actionName The action label into Redux DevTools (default is parent function name)
* @returns True if the state is changed
*/
setState(stateFn: (currentState: Readonly) => Partial, actionName?: string): boolean;
}
```## Alternatives
Aren't you satisfied? there are some valid alternatives:
- [@tinystate](https://www.npmjs.com/package/@tinystate/core)
- [@ngxs](https://www.npmjs.com/package/@ngxs/store)
## SupportThis 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!
## My other libraries
I have published some other Angular libraries, take a look:
- [NgHttpCaching: Cache for HTTP requests in Angular application](https://www.npmjs.com/package/ng-http-caching)
- [NgGenericPipe: Generic pipe for Angular application for use a component method into component template.](https://www.npmjs.com/package/ng-generic-pipe)
- [NgLet: Structural directive for sharing data as local variable into html component template](https://www.npmjs.com/package/ng-let)
- [NgForTrackByProperty: Angular global trackBy property directive with strict type checking](https://www.npmjs.com/package/ng-for-track-by-property)