mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 12:53:03 -05:00
feat(ngrxcallstatelibray): update readme
This commit is contained in:
@@ -1,7 +1,222 @@
|
|||||||
<p align='center'>NgRx Callstate ComponentStore</p>
|
# NgRx CallState ComponentStore
|
||||||
|
|
||||||
<br>
|
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<void>(
|
||||||
|
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<T>): 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<T>): 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<T>): 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<T extends CustomError> {
|
||||||
|
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<CallStateError> {
|
||||||
|
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<MyError> {
|
||||||
|
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);
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@tomalaforge/ngrx-callstate-store",
|
"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": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^15.0.0",
|
"@angular/common": "^15.0.0",
|
||||||
"@angular/core": "^15.0.0"
|
"@angular/core": "^15.0.0"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './lib/custom-component-store';
|
export * from './lib/call-state-component-store';
|
||||||
export * from './lib/external.model';
|
export * from './lib/external.model';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
CallStateError,
|
CallStateError,
|
||||||
getErrorCallState,
|
getErrorCallState,
|
||||||
} from './call-state.model';
|
} from './call-state.model';
|
||||||
import { ERROR_TOKEN } from './external.model';
|
import { ERROR_TOKEN, FLICKER_TIME } from './external.model';
|
||||||
import { nonFlickerLoader } from './non-flicker-loader';
|
import { nonFlickerLoader } from './non-flicker-loader';
|
||||||
|
|
||||||
export const INITIAL_TOKEN = new InjectionToken('initial data');
|
export const INITIAL_TOKEN = new InjectionToken('initial data');
|
||||||
@@ -26,6 +26,7 @@ export class CallStateComponentStore<
|
|||||||
: U & CallStateComponentState
|
: U & CallStateComponentState
|
||||||
> extends ComponentStore<T> {
|
> extends ComponentStore<T> {
|
||||||
private error = inject(ERROR_TOKEN);
|
private error = inject(ERROR_TOKEN);
|
||||||
|
private flickerTime = inject(FLICKER_TIME);
|
||||||
|
|
||||||
constructor(@Inject(INITIAL_TOKEN) initialState: U) {
|
constructor(@Inject(INITIAL_TOKEN) initialState: U) {
|
||||||
super({ callState: 'INIT', ...initialState } as T);
|
super({ callState: 'INIT', ...initialState } as T);
|
||||||
@@ -37,7 +38,9 @@ export class CallStateComponentStore<
|
|||||||
|
|
||||||
readonly isLoadingWithFlicker$: Observable<boolean> = this.select(
|
readonly isLoadingWithFlicker$: Observable<boolean> = this.select(
|
||||||
(state) => state.callState === 'LOADING'
|
(state) => state.callState === 'LOADING'
|
||||||
).pipe(switchMap((loading) => nonFlickerLoader(of(loading))));
|
).pipe(
|
||||||
|
switchMap((loading) => nonFlickerLoader(of(loading), this.flickerTime))
|
||||||
|
);
|
||||||
|
|
||||||
readonly isLoaded$: Observable<boolean> = this.select(
|
readonly isLoaded$: Observable<boolean> = this.select(
|
||||||
(state) => state.callState === 'LOADED'
|
(state) => state.callState === 'LOADED'
|
||||||
@@ -29,10 +29,6 @@ export class CallStateErrorHandler implements ErrorHandler<CallStateError> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EsuiteError {
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorState {
|
export interface ErrorState {
|
||||||
error: CustomError;
|
error: CustomError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
/* 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';
|
import { CallStateErrorHandler } from './call-state.model';
|
||||||
|
|
||||||
export interface ErrorHandler<T extends CustomError> {
|
export interface ErrorHandler<T extends CustomError> {
|
||||||
@@ -16,3 +21,12 @@ export const ERROR_TOKEN = new InjectionToken<ErrorHandler<any>>('error', {
|
|||||||
export const provideErrorHandler = <T extends ErrorHandler<any>>(
|
export const provideErrorHandler = <T extends ErrorHandler<any>>(
|
||||||
errorHandlerClass: Type<T>
|
errorHandlerClass: Type<T>
|
||||||
): ClassProvider => ({ provide: ERROR_TOKEN, useClass: errorHandlerClass });
|
): ClassProvider => ({ provide: ERROR_TOKEN, useClass: errorHandlerClass });
|
||||||
|
|
||||||
|
export const FLICKER_TIME = new InjectionToken<number>('flicker', {
|
||||||
|
factory: () => 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const provideFlickerDelay = (delay: number): ValueProvider => ({
|
||||||
|
provide: FLICKER_TIME,
|
||||||
|
useValue: delay,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user