From 0979a9f3db96fdba6e1ec007ff426ac6edbb89e4 Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 14 Dec 2022 14:37:54 +0100 Subject: [PATCH] feat(ngrxcallstatelibray): update readme --- libs/shared/ngrx-callstate-store/README.md | 223 +++++++++++++++++- libs/shared/ngrx-callstate-store/package.json | 18 +- libs/shared/ngrx-callstate-store/src/index.ts | 2 +- ...store.ts => call-state-component-store.ts} | 7 +- .../src/lib/call-state.model.ts | 4 - .../src/lib/external.model.ts | 16 +- 6 files changed, 257 insertions(+), 13 deletions(-) rename libs/shared/ngrx-callstate-store/src/lib/{custom-component-store.ts => call-state-component-store.ts} (92%) diff --git a/libs/shared/ngrx-callstate-store/README.md b/libs/shared/ngrx-callstate-store/README.md index 9e43d86..a6da01f 100644 --- a/libs/shared/ngrx-callstate-store/README.md +++ b/libs/shared/ngrx-callstate-store/README.md @@ -1,7 +1,222 @@ -

NgRx Callstate ComponentStore

+# NgRx CallState ComponentStore -
+NgRx CallState ComponentStore is a small library that extends the **@Ngrx/component-store** by adding a loading and error state to your custom state. -## Intro +## Installation -Small library to enhance ComponentStore for have a loading or error state for all XHR request. +Requires @Ngrx/component-store + +Yarn: + +```bash +yarn add @tomalaforge/ngrx-callstate-store +``` + +NPM: + +```bash +npm i @tomalaforge/ngrx-callstate-store +``` + +## Introduction + +When making XHR calls or any asynschronous tasks, you always need a loading or error state. By using `CallStateComponentStore`, you can easily manage the loading and error states of your async tasks, which makes your code more organized and maintainable. + +## Example + +```typescript +@Injectable() +export class AppStore extends CallStateComponentStore<{todos: Todo[]}> { + readonly todos$ = this.select((state) => state.todos); + + readonly vm$ = this.select({ + todos: this.todos$, + loading: this.loading$, + }, {debounce: true}); + + constructor(private todoService: TodoService) { + super({ todos: [] }); + } + + readonly fetchTodo = this.effect( + pipe( + tap(() => this.startLoading()), + switchMap(() => + this.todoService.getAllTodo().pipe( + tapResponse( + (todos) => this.stopLoading({ todos }), + (error: unknown) => this.handleError(error, {todos: []}) + ) + ) + ) + ) + ); +``` + +By extending your class with `CallStateComponentStore`, a `CallState` property is added to your state. + +> You don't need to provide a state if you only want to use the `CallState` property. + +```typescript +export type LoadingState = 'INIT' | 'LOADING' | 'LOADED'; + +export interface ErrorState { + error: CustomError; +} + +export type CallState = LoadingState | ErrorState; +``` + +> You can override [`CustomError`](#errorstate) as needed. + +## API + +### updater + +##### startLoading + +The `startLoading` method sets the `CallState` property to the `LOADING` state. You can pass optional state properties if you want to patch your own state. + +```typescript +startLoading = (state: Optional): void +``` + +##### stopLoading + +The `stopLoading` method sets the `CallState` to the `LOADED` state. You can pass an optional state properties as well. + +```typescript +stopLoading = (state: Optional): void +``` + +##### updateCallState + +The `updateCallState` method updates the callState with the inputed value. + +```typescript +updateCallState = (callState: CallState): void +``` + +##### handleError + +The `handleError` method handles errors. You can pass an optional state. + +```typescript +handleError = (error: unknown, state: Optional): void +``` + +### selector + +##### isLoading$ + +`isLoading$` return a boolean, true if state is loading, false otherwise + +##### isLoadingWithFlicker$ + +`isLoadingWithFlicker$` return the same as `isLoading$` but with a small delay. This can be useful when you don't want your page to flicker. + +##### isLoaded$ + +`isLoaded$` return a boolean, true if state is loaded, false otherwise + +##### callState$ + +`isLoading$` return the `CallState` + +##### error$ + +`isLoading$` return your error message using the `getErrorMessage` of your `ErrorState`. [(see below)](#errorstate) + +--- + +### Customize the library + +##### ErrorState + +You can provide your own implementation of The `ErrorState` by implementing `ErrorHandler` + +```typescript +export interface ErrorHandler { + toError: (error: unknown) => T; + getErrorMessage: (error?: T) => string | undefined; +} +``` + +The `toError` method converts the error input into the desired error object. +The `getErrorMessage` method returns the well-formed error message that you want to display to your user. + +The current implementation is as follow: + +```typescript +export const UNKNOWN_ERROR_CAUSE = 'UNKNOWN_ERROR'; +export const UNKNOWN_ERROR_MESSAGE = 'unknown error occured'; + +export class CallStateError { + name: string; + message: string; + stack?: string; + + constructor(name = '', message = '') { + this.name = name; + this.message = message; + } +} + +export class CallStateErrorHandler implements ErrorHandler { + toError = (error: unknown): CallStateError => { + if (error instanceof CallStateError) return error; + if (error instanceof Error) + return new CallStateError(error.name, error.message); + return new CallStateError(UNKNOWN_ERROR_CAUSE, UNKNOWN_ERROR_MESSAGE); + }; + + getErrorMessage = (error?: CallStateError): string | undefined => { + return error?.message; + }; +} +``` + +Let's say you want to customize it as follow: + +```typescript +export const UNKNOWN_ERROR_MESSAGE = 'unknown error occured'; + +export class MyError { + message: string; + code: number + + constructor(message = '', code = 404) { + this.message = message; + this.code = code; + } +} + +export class MyErrorHandler implements ErrorHandler { + toError = (error: unknown): MyError => { + if (error instanceof MyError) return error; + if (error instanceof Error) + return new MyError(error.message); + return new MyError(UNKNOWN_ERROR_MESSAGE); + }; + + getErrorMessage = (error?: MyError): string | undefined => { + return error.code error?.message; + }; +} +``` + +Now to override the default implementation, you need to provide it as follow : + +```typescript +provideErrorHandler(MyErrorHandler); +``` + +> You can provide it at root level to apply it to your whole application or at the component level for more specific implementation. + +##### Flicker Delay + +The default delay is 300ms but you can override it by providing it as follow: + +```typescript +provideFlickerDelay(500); +``` diff --git a/libs/shared/ngrx-callstate-store/package.json b/libs/shared/ngrx-callstate-store/package.json index 11447ee..1d9a2e3 100644 --- a/libs/shared/ngrx-callstate-store/package.json +++ b/libs/shared/ngrx-callstate-store/package.json @@ -1,6 +1,22 @@ { "name": "@tomalaforge/ngrx-callstate-store", - "version": "0.0.1", + "version": "0.0.3", + "description": "Enhance NgRx component-store by providing a loading/error state", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/tomalaforge/angular-challenges/tree/main/libs/shared/ngrx-callstate-store" + }, + "keywords": [ + "angular", + "state", + "ngrx/component-store" + ], + "author": { + "name": "Thomas Laforge" + }, "peerDependencies": { "@angular/common": "^15.0.0", "@angular/core": "^15.0.0" diff --git a/libs/shared/ngrx-callstate-store/src/index.ts b/libs/shared/ngrx-callstate-store/src/index.ts index 51c5b26..1de3f04 100644 --- a/libs/shared/ngrx-callstate-store/src/index.ts +++ b/libs/shared/ngrx-callstate-store/src/index.ts @@ -1,2 +1,2 @@ -export * from './lib/custom-component-store'; +export * from './lib/call-state-component-store'; export * from './lib/external.model'; diff --git a/libs/shared/ngrx-callstate-store/src/lib/custom-component-store.ts b/libs/shared/ngrx-callstate-store/src/lib/call-state-component-store.ts similarity index 92% rename from libs/shared/ngrx-callstate-store/src/lib/custom-component-store.ts rename to libs/shared/ngrx-callstate-store/src/lib/call-state-component-store.ts index 7e0a884..08c2259 100644 --- a/libs/shared/ngrx-callstate-store/src/lib/custom-component-store.ts +++ b/libs/shared/ngrx-callstate-store/src/lib/call-state-component-store.ts @@ -7,7 +7,7 @@ import { CallStateError, getErrorCallState, } from './call-state.model'; -import { ERROR_TOKEN } from './external.model'; +import { ERROR_TOKEN, FLICKER_TIME } from './external.model'; import { nonFlickerLoader } from './non-flicker-loader'; export const INITIAL_TOKEN = new InjectionToken('initial data'); @@ -26,6 +26,7 @@ export class CallStateComponentStore< : U & CallStateComponentState > extends ComponentStore { private error = inject(ERROR_TOKEN); + private flickerTime = inject(FLICKER_TIME); constructor(@Inject(INITIAL_TOKEN) initialState: U) { super({ callState: 'INIT', ...initialState } as T); @@ -37,7 +38,9 @@ export class CallStateComponentStore< readonly isLoadingWithFlicker$: Observable = this.select( (state) => state.callState === 'LOADING' - ).pipe(switchMap((loading) => nonFlickerLoader(of(loading)))); + ).pipe( + switchMap((loading) => nonFlickerLoader(of(loading), this.flickerTime)) + ); readonly isLoaded$: Observable = this.select( (state) => state.callState === 'LOADED' diff --git a/libs/shared/ngrx-callstate-store/src/lib/call-state.model.ts b/libs/shared/ngrx-callstate-store/src/lib/call-state.model.ts index 7dca57b..bcd13b2 100644 --- a/libs/shared/ngrx-callstate-store/src/lib/call-state.model.ts +++ b/libs/shared/ngrx-callstate-store/src/lib/call-state.model.ts @@ -29,10 +29,6 @@ export class CallStateErrorHandler implements ErrorHandler { }; } -export interface EsuiteError { - code: string; -} - export interface ErrorState { error: CustomError; } diff --git a/libs/shared/ngrx-callstate-store/src/lib/external.model.ts b/libs/shared/ngrx-callstate-store/src/lib/external.model.ts index 49be42e..1d1ff49 100644 --- a/libs/shared/ngrx-callstate-store/src/lib/external.model.ts +++ b/libs/shared/ngrx-callstate-store/src/lib/external.model.ts @@ -1,5 +1,10 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { ClassProvider, InjectionToken, Type } from '@angular/core'; +import { + ClassProvider, + InjectionToken, + Type, + ValueProvider, +} from '@angular/core'; import { CallStateErrorHandler } from './call-state.model'; export interface ErrorHandler { @@ -16,3 +21,12 @@ export const ERROR_TOKEN = new InjectionToken>('error', { export const provideErrorHandler = >( errorHandlerClass: Type ): ClassProvider => ({ provide: ERROR_TOKEN, useClass: errorHandlerClass }); + +export const FLICKER_TIME = new InjectionToken('flicker', { + factory: () => 300, +}); + +export const provideFlickerDelay = (delay: number): ValueProvider => ({ + provide: FLICKER_TIME, + useValue: delay, +});