feat(callstatelib): add a callstate publishable lib

This commit is contained in:
thomas
2022-12-07 16:03:05 +01:00
parent 278e513538
commit 1ec45ce141
19 changed files with 1477 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "angularChallenges",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "angular-challenges",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
<p align='center'>NgRx Callstate ComponentStore</p>
<br>
## Intro
Small library to enhance ComponentStore for have a loading or error state for all XHR request.

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'shared-ngrx-callstate-store',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
},
coverageDirectory: '../../../coverage/libs/shared/ngrx-callstate-store',
transform: {
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/libs/shared/ngrx-callstate-store",
"lib": {
"entryFile": "src/index.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@tomalaforge/ngrx-callstate-store",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^15.0.0",
"@angular/core": "^15.0.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,43 @@
{
"name": "shared-ngrx-callstate-store",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/shared/ngrx-callstate-store/src",
"prefix": "angular-challenges",
"targets": {
"build": {
"executor": "@nrwl/angular:package",
"outputs": ["{workspaceRoot}/dist/{projectRoot}"],
"options": {
"project": "libs/shared/ngrx-callstate-store/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "libs/shared/ngrx-callstate-store/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "libs/shared/ngrx-callstate-store/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/ngrx-callstate-store/jest.config.ts",
"passWithNoTests": true
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": [
"libs/shared/ngrx-callstate-store/**/*.ts",
"libs/shared/ngrx-callstate-store/**/*.html"
]
}
}
},
"tags": []
}

View File

@@ -0,0 +1,2 @@
export * from './lib/custom-component-store';
export * from './lib/external.model';

View File

@@ -0,0 +1,55 @@
import { CustomError, ErrorHandler } from './external.model';
export type LoadingState = 'INIT' | 'LOADING' | 'LOADED';
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;
};
}
export interface EsuiteError {
code: string;
}
export interface ErrorState {
error: CustomError;
}
export type CallState = LoadingState | ErrorState;
export const getErrorCallState = (
callState: CallState
): CustomError | undefined => {
if (isErrorState(callState)) {
return callState.error;
}
return undefined;
};
export const isLoadedOrInError = (callState: CallState): boolean =>
callState === 'LOADED' || isErrorState(callState);
export const isErrorState = (callState: CallState): callState is ErrorState =>
Object.prototype.hasOwnProperty.call(callState, 'error');

View File

@@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/ban-types */
import { inject, Inject, Injectable, InjectionToken } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable, of, switchMap } from 'rxjs';
import {
CallState,
CallStateError,
getErrorCallState,
} from './call-state.model';
import { ERROR_TOKEN } from './external.model';
import { nonFlickerLoader } from './non-flicker-loader';
export const INITIAL_TOKEN = new InjectionToken('initial data');
export interface CallStateComponentState {
callState: CallState;
}
@Injectable()
export class CallStateComponentStore<
U extends object | void = void,
T extends
| (U & CallStateComponentState)
| CallStateComponentState = U extends void
? CallStateComponentState
: U & CallStateComponentState
> extends ComponentStore<T> {
private error = inject(ERROR_TOKEN);
constructor(@Inject(INITIAL_TOKEN) initialState: U) {
super({ callState: 'INIT', ...initialState } as T);
}
readonly isLoading$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADING'
);
readonly isLoadingWithFlicker$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADING'
).pipe(switchMap((loading) => nonFlickerLoader(of(loading))));
readonly isLoaded$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADED'
).pipe(switchMap((loaded) => of(loaded)));
readonly callState$ = this.select((state) => state.callState);
readonly error$: Observable<string | undefined> = this.select((state) =>
this.error.getErrorMessage(getErrorCallState(state.callState))
);
readonly updateCallState = this.updater(
(state, callState: CallState | undefined): T => {
return {
...(state as object),
callState: callState ?? 'LOADED',
} as T;
}
);
readonly startLoading = this.updater(
(state, patchedState: Partial<U> | void): T => {
return {
...(state as object),
...patchedState,
callState: 'LOADING',
} as T;
}
);
readonly stopLoading = this.updater(
(state, patchedState: Partial<U> | void): T => {
return {
...(state as object),
...patchedState,
callState: 'LOADED',
} as T;
}
);
protected handleError(
error: unknown,
patchedState: Partial<U> = {}
): CallStateError {
const err = this.error.toError(error);
this.patchState({
callState: { error: err },
...patchedState,
} as Partial<T>);
return err;
}
}

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import { ClassProvider, InjectionToken, Type } from '@angular/core';
import { CallStateErrorHandler } from './call-state.model';
export interface ErrorHandler<T extends CustomError> {
toError: (error: unknown) => T;
getErrorMessage: (error?: T) => string | undefined;
}
export interface CustomError {}
export const ERROR_TOKEN = new InjectionToken<ErrorHandler<any>>('error', {
factory: () => new CallStateErrorHandler(),
});
export const provideErrorHandler = <T extends ErrorHandler<any>>(
errorHandlerClass: Type<T>
): ClassProvider => ({ provide: ERROR_TOKEN, useClass: errorHandlerClass });

View File

@@ -0,0 +1,20 @@
import { combineLatest, map, mapTo, Observable, startWith, timer } from 'rxjs';
/**
* Delay the first emition of data$ value. Instead, it emits "true" until duration is elapsed
*/
export const nonFlickerLoader = (
data$: Observable<boolean>,
duration = 300
): Observable<boolean> => {
const isTrueWhileDuration$ = timer(duration).pipe(
mapTo(false),
startWith(true)
);
return combineLatest([data$, isTrueWhileDuration$]).pipe(
map(([data, isTrueWhileDuration]) =>
isTrueWhileDuration ? isTrueWhileDuration : data
)
);
};

View File

@@ -0,0 +1 @@
import 'jest-preset-angular/setup-jest';

View File

@@ -0,0 +1,32 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.lib.prod.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"target": "es2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/test-setup.ts",
"**/*.spec.ts",
"jest.config.ts",
"**/*.test.ts"
],
"include": ["**/*.ts"]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
}

1087
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,12 @@
"jest-environment-jsdom": "28.1.1",
"jest-preset-angular": "12.2.3",
"lint-staged": "^13.0.3",
"ng-packagr": "~15.0.0",
"nx": "15.2.4",
"postcss": "^8.4.5",
"postcss-import": "~14.1.0",
"postcss-preset-env": "~7.5.0",
"postcss-url": "~10.1.3",
"prettier": "^2.6.2",
"ts-jest": "28.0.5",
"ts-node": "10.9.1",

View File

@@ -20,6 +20,9 @@
],
"@angular-challenges/ngrx-notification/model": [
"libs/ngrx-notification/model/src/index.ts"
],
"@tomalaforge/ngrx-callstate-store": [
"libs/shared/ngrx-callstate-store/src/index.ts"
]
}
},