/ngx-base-state

Primary LanguageTypeScriptMIT LicenseMIT

ngx-base-state 🐍

npm npm Build status

Classes have implemented base work with state

Idea

The main idea of this library is remove useless code from class. Usually state services violate DRY pattern
This library will help to create state in 3 lines.

Wiki

Visit wiki page to get more useful information.

Installation

npm install ngx-base-state --save

OPTIONAL: If you want to use Devtools to explore your state via Chrome Extension:

In your AppModule

import {
    NgxBaseStateDevtoolsModule,
    NgxBaseStateDevtoolsConfig,
    NGX_BASE_STATE_DEVTOOLS_CONFIG
} from 'ngx-base-state';
import { environment } from 'src/environments/environment'; 

@NgModule({
    imports: [NgxBaseStateDevtoolsModule],
    providers: [
        {
            provide: NGX_BASE_STATE_DEVTOOLS_CONFIG,
            useValue: new NgxBaseStateDevtoolsConfig({
                isEnabled: !environment.production // Devtools will not work in production
            })
        }
    ]
})
export class AppModule {}

Chrome Extension

This tool allows you to see data in your states based on ngx-base-state.

  • Install ngx-base-state extension from Chrome WebStore;
  • Open tab with your Application using ngx-base-state;
  • Press F12 to open Devtools;
  • Choose ngx-base-state panel in devtools;

Main page will contain list of all your states. Click to some state and will opened "details page" with state changes history.

List of States List of Actions State details

Properties & Methods

BaseState

Base class for all kinds of states. You can create your abstract class based on BaseState to store the necessary custom data.

Name Type Description
data$ Observable<T | null> state data stream
data T state data
set value: T (generic type) set new value for state
clear clear value in the state
restoreInitialData restore initial data from constructor.

ObjectState

Extend your class from ObjectState to store the object.

Contains all fields and methods like at BaseState, and also:

Name Arguments Description
updateWithPartial value: Partial<T> update state by merging current state with new partial object

RecordState

Extend your class from RecordState to store the object with Record interface.

Store data in key -> value format.

Contains all fields and methods like at BaseState, and also:

Name Arguments Description
keys$ stream with all keys of your Record object
keys all keys of your Record object
values all values of your Record object
values$ stream with all values of your Record object
setItem key: TKey, value: TValue set item by key into state's object
removeItem key: TKey remove item by key from state's object
removeAllItems remove all items from state's object

ArrayState

Extend your class from ArrayState to store the array.

Contains all fields and methods like at BaseState, and also:

Name Arguments Description
getItemId item: T protected method might be overridden, it used for comparing items in array
set value: T[] set new array for state
pushItem item: T push new item to array
unshiftItem item: T unshift item to array
shift shift array
pop pop array
insertItemByIndex index: number, item: T insert item in array by index.
updateItem itemToUpdate: T update item in array
updateItemByIndex item: T, index: number update item in array by index
concatWith array: T[] concat current state with another array
removeItem item: T remove item from array
removeItemById itemId: unknown remove item from array by id (define id by overriding getItemId method)
removeItemByIndex index: number remove item from array by index

PrimitiveState

Extend your class from PrimitiveState to store the: number, string, boolean, enum, type etc...

Contains all fields and methods like at BaseState and currently nothing else.

Example with ObjectState

user.state.ts

import { ObjectState, NgxState } from 'ngx-base-state';

// So easy to create new State :)
@NgxState()
@Injectable({
  providedIn: 'root'
})
class UserState extends ObjectState<User> {}

user.service.ts

import { User } from '../interfaces';
import { UserApi } from '../api';
import { UserState } from '../states';

// IMPORTANT: Work with states only via "Service" layer.
@Injectable({
  providedIn: 'root'
})
class UserService {
  // Share data for components.
  public readonly data$ = this.userState.data$;

  constructor(
    private readonly userApi: UserApi,
    private readonly userState: UserState
  ) {}

  // Make your methods with business logic, which might affect states.
  // Return Observable. Components can process result by subscribing (complete/next/error).
  public update(): Observable<User> {
    return this.userApi.getCurrent()
      .pipe(
        tap((user) => this.userState.set(user))
      );
  }
}

user.component.ts

import { ToastService } from '@my-library';
import { UserService } from '@features/user';

// IMPORTANT: Don't inject States directly to components!
// Only services with business logic should know how to affect your states.
@Component({
  selector: 'smart-user',
  template: '{{ user$ | async | json }}'
})
class UserComponent implements OnInit {
  // Here is data from our state.
  public readonly user$ = this.userService.data$;

  constructor(
    private readonly userService: UserService,
    private readonly toastService: ToastService
  ) {}

  public ngOnInit(): void {
    this.updateUser();
  }

  // Run some services business logic from the smart component
  private updateUser(): void {
    this.userService.update()
      .pipe(
        catchError(() => this.showErrorToastAboutUserUpdatingError())
      )
      .subscribe();
  }

  // This is task of specific smart components to show UI staff, like: toasts, dialogs, bottomSheets etc...
  private showErrorToastAboutUserUpdatingError(): Observable<unknown> {
    return this.toastService.createError(`Can't update user!`);
  }
}

Example with ArrayState

users.state.ts

import { ArrayState, NgxState } from 'ngx-base-state';
import { UserFilters } from '../interfaces';

@NgxState()
@Injectable({
  providedIn: 'root'
})
class UsersState extends ArrayState<User> {
  constructor() {
    super([]); // Here you can set initial data.
  }

  // Example of "custom action"
  public filter(filters: UserFilters): void {
    const newUsers = this.data!.filter((user) => user.name.includes(filters.searchString));

    this.set(newUsers);
  }

  // ArrayState have base methods to work with array, like: removeItem, updateItem
  // and these methods might compare items in array using some unique value.
  // You can override method `getItemId` if you want operate with items via specific unique value like `id`.
  protected override getItemId(user: User): number {
    return user.id;
  }
}

users.service.ts

import { UsersState } from './users.state';

// This service demonstrates examples of work with methods of ArrayState.
@Injectable({
  providedIn: 'root'
})
export class UsersService implements OnInit {
  // Async data for components.
  public readonly data$ = this.usersState.data$;

  // Sync data for components.
  public get data(): User {
    return this.usersState.data;
  }

  constructor(
    private readonly usersState: UsersState
  ) {
    this.usersState.data$
      .subscribe(console.log);

    this.setUserArray();  // [{ name: 'Nillcon', id: 248 }, { name: 'noname', id: 1 }]
    this.updateUser()  // [{ name: 'New name', id: 248 }, { name: 'noname', id: 1 }]
    this.removeUser(); // [{ name: 'New name', id: 248 }]
    this.addUser(); // [{ name: 'New name', id: 248 }, { name: 'John Doe', id: 2 }]
  }

  private setUserArray(): void {
    this.usersState.set([
      {
        name: 'Nillcon',
        id: 248
      },
      {
        name: 'noname',
        id: 1
      }
    ]);
  }

  private updateUser(): void {
    let user = this.usersState.data[0]; // { name: 'Nillcon', id: 248 }
    user.name = 'New name';

    // ngx-base-state will create new instance of user to avoid possible object mutations
    this.usersState.updateItem(user);
  }

  private removeUser(): void {
    const user = this.usersState.data[1]; // { name: 'noname', id: 1 }

    this.usersState.removeItem(user);
  }

  private addUser(): void {
    this.usersState.pushItem({
      name: 'John Doe',
      id: 2
    });
  }
}