mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 04:43:03 -05:00
feat(rxjs): refactor challenge 11
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "lib",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "lib",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# power-of-effect-backend
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test power-of-effect-backend` to execute the unit tests.
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'power-of-effect-backend',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {},
|
||||
coverageDirectory: '../../../coverage/libs/power-of-effect/backend',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
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',
|
||||
],
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "power-of-effect-backend",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"sourceRoot": "libs/power-of-effect/backend/src",
|
||||
"prefix": "lib",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"coverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './lib/fake-backend.service';
|
||||
export * from './lib/push.service';
|
||||
@@ -1,105 +0,0 @@
|
||||
import {
|
||||
randSchool,
|
||||
randStudent,
|
||||
randTeacher,
|
||||
} from '@angular-challenges/power-of-effect/model';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { randCompanyName, randFirstName } from '@ngneat/falso';
|
||||
import { concatLatestFrom } from '@ngrx/operators';
|
||||
import { map, tap, timer } from 'rxjs';
|
||||
import { FakeDBService } from './fake-db.service';
|
||||
import { PushService } from './push.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FakeBackendService {
|
||||
private fakeDbService = inject(FakeDBService);
|
||||
private pushService = inject(PushService);
|
||||
|
||||
getAllTeachers = () => this.fakeDbService.teachers$;
|
||||
getAllStudents = () => this.fakeDbService.students$;
|
||||
getAllSchools = () => this.fakeDbService.schools$;
|
||||
|
||||
start() {
|
||||
this.fakeAddTeacher();
|
||||
this.fakeUpdateTeacher();
|
||||
this.fakeAddStudent();
|
||||
this.fakeUpdateStudent();
|
||||
this.fakeAddSchool();
|
||||
this.fakeUpdateSchool();
|
||||
}
|
||||
|
||||
private fakeAddTeacher() {
|
||||
timer(0, 4000)
|
||||
.pipe(
|
||||
map(() => randTeacher()),
|
||||
tap((teacher) => this.pushService.pushData(teacher)),
|
||||
tap((teacher) => this.fakeDbService.addTeacher(teacher)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private fakeUpdateTeacher() {
|
||||
timer(8000, 5000)
|
||||
.pipe(
|
||||
concatLatestFrom(() => this.fakeDbService.randomTeacher$),
|
||||
map(([, teacher]) => ({
|
||||
...teacher,
|
||||
firstname: randFirstName(),
|
||||
version: teacher.version + 1,
|
||||
})),
|
||||
tap((teacher) => this.pushService.pushData(teacher)),
|
||||
tap((teacher) => this.fakeDbService.updateTeacher(teacher)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private fakeAddStudent() {
|
||||
timer(0, 2000)
|
||||
.pipe(
|
||||
map(() => randStudent()),
|
||||
tap((student) => this.pushService.pushData(student)),
|
||||
tap((student) => this.fakeDbService.addStudent(student)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private fakeUpdateStudent() {
|
||||
timer(8000, 6000)
|
||||
.pipe(
|
||||
concatLatestFrom(() => this.fakeDbService.randomStudents$),
|
||||
map(([, student]) => ({
|
||||
...student,
|
||||
firstname: randFirstName(),
|
||||
version: student.version + 1,
|
||||
})),
|
||||
tap((student) => this.pushService.pushData(student)),
|
||||
tap((student) => this.fakeDbService.updateSudent(student)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private fakeAddSchool() {
|
||||
timer(0, 2000)
|
||||
.pipe(
|
||||
map(() => randSchool()),
|
||||
tap((school) => this.pushService.pushData(school)),
|
||||
tap((school) => this.fakeDbService.addSchool(school)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private fakeUpdateSchool() {
|
||||
timer(8000, 4000)
|
||||
.pipe(
|
||||
concatLatestFrom(() => this.fakeDbService.randomSchool$),
|
||||
map(([, school]) => ({
|
||||
...school,
|
||||
name: randCompanyName(),
|
||||
version: school.version + 1,
|
||||
})),
|
||||
tap((school) => this.pushService.pushData(school)),
|
||||
tap((school) => this.fakeDbService.updateSchool(school)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import {
|
||||
School,
|
||||
Student,
|
||||
Teacher,
|
||||
} from '@angular-challenges/power-of-effect/model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { randNumber } from '@ngneat/falso';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
|
||||
interface AppState {
|
||||
teachers: Teacher[];
|
||||
students: Student[];
|
||||
schools: School[];
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FakeDBService extends ComponentStore<AppState> {
|
||||
readonly teachers$ = this.select((state) => state.teachers);
|
||||
readonly randomTeacher$ = this.select(
|
||||
this.teachers$,
|
||||
(teachers) => teachers[randNumber({ max: teachers.length - 1 })],
|
||||
);
|
||||
|
||||
readonly students$ = this.select((state) => state.students);
|
||||
readonly randomStudents$ = this.select(
|
||||
this.students$,
|
||||
(students) => students[randNumber({ max: students.length - 1 })],
|
||||
);
|
||||
|
||||
readonly schools$ = this.select((state) => state.schools);
|
||||
readonly randomSchool$ = this.select(
|
||||
this.schools$,
|
||||
(schools) => schools[randNumber({ max: schools.length - 1 })],
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super({ teachers: [], students: [], schools: [] });
|
||||
}
|
||||
|
||||
addTeacher = this.updater(
|
||||
(state, teacher: Teacher): AppState => ({
|
||||
...state,
|
||||
teachers: [...state.teachers, teacher],
|
||||
}),
|
||||
);
|
||||
|
||||
updateTeacher = this.updater(
|
||||
(state, teacher: Teacher): AppState => ({
|
||||
...state,
|
||||
teachers: state.teachers.map((t) => (t.id === teacher.id ? teacher : t)),
|
||||
}),
|
||||
);
|
||||
|
||||
addStudent = this.updater(
|
||||
(state, student: Student): AppState => ({
|
||||
...state,
|
||||
students: [...state.students, student],
|
||||
}),
|
||||
);
|
||||
|
||||
updateSudent = this.updater(
|
||||
(state, student: Student): AppState => ({
|
||||
...state,
|
||||
students: state.students.map((t) => (t.id === student.id ? student : t)),
|
||||
}),
|
||||
);
|
||||
|
||||
addSchool = this.updater(
|
||||
(state, school: School): AppState => ({
|
||||
...state,
|
||||
schools: [...state.schools, school],
|
||||
}),
|
||||
);
|
||||
|
||||
updateSchool = this.updater(
|
||||
(state, school: School): AppState => ({
|
||||
...state,
|
||||
schools: state.schools.map((t) => (t.id === school.id ? school : t)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Push } from '@angular-challenges/power-of-effect/model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PushService {
|
||||
private notificationSubject = new BehaviorSubject<Push | undefined>(
|
||||
undefined,
|
||||
);
|
||||
notification$ = this.notificationSubject.asObservable();
|
||||
|
||||
pushData(data: Push) {
|
||||
this.notificationSubject.next(data);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "lib",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "lib",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# power-of-effect-model
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test power-of-effect-model` to execute the unit tests.
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'power-of-effect-model',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {},
|
||||
coverageDirectory: '../../../coverage/libs/power-of-effect/model',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
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',
|
||||
],
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "power-of-effect-model",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"sourceRoot": "libs/power-of-effect/model/src",
|
||||
"prefix": "lib",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"coverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './lib/push.model';
|
||||
export * from './lib/school.model';
|
||||
export * from './lib/student.model';
|
||||
export * from './lib/teacher.model';
|
||||
@@ -1,5 +0,0 @@
|
||||
export type PushType = 'teacher' | 'student' | 'school';
|
||||
|
||||
export interface Push {
|
||||
type: PushType;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { incrementalNumber, randCompanyName } from '@ngneat/falso';
|
||||
import { Push } from './push.model';
|
||||
|
||||
export interface School extends Push {
|
||||
id: number;
|
||||
name: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const factorySchool = incrementalNumber();
|
||||
|
||||
export const randSchool = (): School => ({
|
||||
id: factorySchool(),
|
||||
name: randCompanyName(),
|
||||
version: 0,
|
||||
type: 'school',
|
||||
});
|
||||
|
||||
export const isSchool = (notif: Push): notif is School => {
|
||||
return notif.type === 'school';
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { incrementalNumber, randFirstName, randLastName } from '@ngneat/falso';
|
||||
import { Push } from './push.model';
|
||||
|
||||
export interface Student extends Push {
|
||||
id: number;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const factoryStudent = incrementalNumber();
|
||||
|
||||
export const randStudent = (): Student => ({
|
||||
id: factoryStudent(),
|
||||
firstname: randFirstName(),
|
||||
lastname: randLastName(),
|
||||
version: 0,
|
||||
type: 'student',
|
||||
});
|
||||
|
||||
export const isStudent = (notif: Push): notif is Student => {
|
||||
return notif.type === 'student';
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { incrementalNumber, randFirstName, randLastName } from '@ngneat/falso';
|
||||
import { Push } from './push.model';
|
||||
|
||||
export interface Teacher extends Push {
|
||||
id: number;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const factoryTeacher = incrementalNumber();
|
||||
|
||||
export const randTeacher = (): Teacher => ({
|
||||
id: factoryTeacher(),
|
||||
firstname: randFirstName(),
|
||||
lastname: randLastName(),
|
||||
version: 0,
|
||||
type: 'teacher',
|
||||
});
|
||||
|
||||
export const isTeacher = (notif: Push): notif is Teacher => {
|
||||
return notif.type === 'teacher';
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "lib",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "lib",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Installation
|
||||
|
||||
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
|
||||
|
||||
### initialization
|
||||
|
||||
##### setInitState
|
||||
|
||||
The `setInitState` method lets you initialize your custom state if you are not using the constructor.
|
||||
|
||||
```typescript
|
||||
setInitState = (state: T): void
|
||||
```
|
||||
|
||||
### 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,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'shared-ngrx-callstate-store',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {},
|
||||
coverageDirectory: '../../../coverage/libs/shared/ngrx-callstate-store',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
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',
|
||||
],
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../../dist/libs/shared/ngrx-callstate-store",
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "@tomalaforge/ngrx-callstate-store",
|
||||
"version": "0.0.4",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "shared-ngrx-callstate-store",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"sourceRoot": "libs/shared/ngrx-callstate-store/src",
|
||||
"prefix": "lib",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/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": {
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"ci": true,
|
||||
"coverage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './lib/call-state-component-store';
|
||||
export * from './lib/external.model';
|
||||
@@ -1,105 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import {
|
||||
inject,
|
||||
Inject,
|
||||
Injectable,
|
||||
InjectionToken,
|
||||
Optional,
|
||||
} 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, FLICKER_TIME } 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);
|
||||
private flickerTime = inject(FLICKER_TIME);
|
||||
|
||||
constructor(@Inject(INITIAL_TOKEN) @Optional() 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), this.flickerTime)),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setInitState(initialState: U) {
|
||||
this.setState({ callState: 'INIT', ...initialState } as T);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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 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');
|
||||
@@ -1,32 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
import {
|
||||
ClassProvider,
|
||||
InjectionToken,
|
||||
Type,
|
||||
ValueProvider,
|
||||
} 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 });
|
||||
|
||||
export const FLICKER_TIME = new InjectionToken<number>('flicker', {
|
||||
factory: () => 300,
|
||||
});
|
||||
|
||||
export const provideFlickerDelay = (delay: number): ValueProvider => ({
|
||||
provide: FLICKER_TIME,
|
||||
useValue: delay,
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
Reference in New Issue
Block a user