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": "app",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@angular-eslint/component-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "element",
|
|
||||||
"prefix": "app",
|
|
||||||
"style": "kebab-case"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"extends": ["plugin:@nx/angular-template"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Effect vs Selector
|
|
||||||
|
|
||||||
> author: thomas-laforge
|
|
||||||
|
|
||||||
### Run Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx nx serve ngrx-effect-vs-selector
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentation and Instruction
|
|
||||||
|
|
||||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/ngrx/2-effect-selector/).
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
displayName: 'ngrx-effect-vs-selector',
|
|
||||||
preset: '../../../jest.preset.js',
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
|
||||||
globals: {},
|
|
||||||
coverageDirectory: '../../../coverage/apps/ngrx/2-effect-vs-selector',
|
|
||||||
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,84 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ngrx-effect-vs-selector",
|
|
||||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "application",
|
|
||||||
"sourceRoot": "apps/ngrx/2-effect-vs-selector/src",
|
|
||||||
"prefix": "app",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {
|
|
||||||
"build": {
|
|
||||||
"executor": "@angular-devkit/build-angular:browser",
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/ngrx/2-effect-vs-selector",
|
|
||||||
"index": "apps/ngrx/2-effect-vs-selector/src/index.html",
|
|
||||||
"main": "apps/ngrx/2-effect-vs-selector/src/main.ts",
|
|
||||||
"polyfills": "apps/ngrx/2-effect-vs-selector/src/polyfills.ts",
|
|
||||||
"tsConfig": "apps/ngrx/2-effect-vs-selector/tsconfig.app.json",
|
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
|
||||||
"apps/ngrx/2-effect-vs-selector/src/favicon.ico",
|
|
||||||
"apps/ngrx/2-effect-vs-selector/src/assets"
|
|
||||||
],
|
|
||||||
"styles": ["apps/ngrx/2-effect-vs-selector/src/styles.scss"],
|
|
||||||
"scripts": [],
|
|
||||||
"allowedCommonJsDependencies": ["seedrandom"]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kb",
|
|
||||||
"maximumError": "1mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "2kb",
|
|
||||||
"maximumError": "4kb"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputHashing": "all"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"optimization": false,
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"namedChunks": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "production"
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"executor": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "ngrx-effect-vs-selector:build:production"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildTarget": "ngrx-effect-vs-selector:build:development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "development"
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "ngrx-effect-vs-selector:build"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"options": {
|
|
||||||
"passWithNoTests": true
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"ci": true,
|
|
||||||
"coverage": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { AsyncPipe, NgFor } from '@angular/common';
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
OnInit,
|
|
||||||
inject,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { loadActivities } from './store/activity/activity.actions';
|
|
||||||
import { ActivityType } from './store/activity/activity.model';
|
|
||||||
import { selectActivities } from './store/activity/activity.selectors';
|
|
||||||
import { loadStatuses } from './store/status/status.actions';
|
|
||||||
import { selectAllTeachersByActivityType } from './store/status/status.selectors';
|
|
||||||
import { loadUsers } from './store/user/user.actions';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
imports: [NgFor, AsyncPipe],
|
|
||||||
template: `
|
|
||||||
<h1>Activity Board</h1>
|
|
||||||
<section>
|
|
||||||
<div class="card" *ngFor="let activity of activities$ | async">
|
|
||||||
<h2>Activity Name: {{ activity.name }}</h2>
|
|
||||||
<p>Main teacher: {{ activity.teacher.name }}</p>
|
|
||||||
<span>All teachers available for : {{ activity.type }} are</span>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
*ngFor="
|
|
||||||
let teacher of getAllTeachersForActivityType$(activity.type)
|
|
||||||
| async
|
|
||||||
">
|
|
||||||
{{ teacher.name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: black;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class AppComponent implements OnInit {
|
|
||||||
private store = inject(Store);
|
|
||||||
|
|
||||||
activities$ = this.store.select(selectActivities);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.store.dispatch(loadActivities());
|
|
||||||
this.store.dispatch(loadUsers());
|
|
||||||
this.store.dispatch(loadStatuses());
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllTeachersForActivityType$ = (type: ActivityType) =>
|
|
||||||
this.store.select(selectAllTeachersByActivityType(type));
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ApplicationConfig } from '@angular/core';
|
|
||||||
import { provideEffects } from '@ngrx/effects';
|
|
||||||
import { provideStore } from '@ngrx/store';
|
|
||||||
import { ActivityEffects } from './store/activity/activity.effects';
|
|
||||||
import {
|
|
||||||
activityFeatureKey,
|
|
||||||
activityReducer,
|
|
||||||
} from './store/activity/activity.reducer';
|
|
||||||
import { StatusEffects } from './store/status/status.effects';
|
|
||||||
import { UserEffects } from './store/user/user.effects';
|
|
||||||
|
|
||||||
import { statusFeatureKey, statusReducer } from './store/status/status.reducer';
|
|
||||||
|
|
||||||
import { userFeatureKey, userReducer } from './store/user/user.reducer';
|
|
||||||
|
|
||||||
const reducers = {
|
|
||||||
[statusFeatureKey]: statusReducer,
|
|
||||||
[activityFeatureKey]: activityReducer,
|
|
||||||
[userFeatureKey]: userReducer,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
|
||||||
providers: [
|
|
||||||
provideStore(reducers),
|
|
||||||
provideEffects([ActivityEffects, UserEffects, StatusEffects]),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
import { Activity } from './activity.model';
|
|
||||||
|
|
||||||
export const loadActivities = createAction('[Activity Effect] Load Activities');
|
|
||||||
|
|
||||||
export const loadActivitiesSuccess = createAction(
|
|
||||||
'[Activity Effect] Load Activities Success',
|
|
||||||
props<{ activities: Activity[] }>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const loadActivitiesFailure = createAction(
|
|
||||||
'[Activity Effect] Load Activities Failure',
|
|
||||||
props<{ error: unknown }>(),
|
|
||||||
);
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { catchError, concatMap, map } from 'rxjs/operators';
|
|
||||||
import * as ActivityActions from './activity.actions';
|
|
||||||
import { ActivityService } from './activity.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ActivityEffects {
|
|
||||||
loadActivities$ = createEffect(() => {
|
|
||||||
return this.actions$.pipe(
|
|
||||||
ofType(ActivityActions.loadActivities),
|
|
||||||
concatMap(() =>
|
|
||||||
this.ActivityService.fetchActivities().pipe(
|
|
||||||
map((activities) =>
|
|
||||||
ActivityActions.loadActivitiesSuccess({ activities }),
|
|
||||||
),
|
|
||||||
catchError((error) =>
|
|
||||||
of(ActivityActions.loadActivitiesFailure({ error })),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private actions$: Actions,
|
|
||||||
private ActivityService: ActivityService,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import {
|
|
||||||
incrementalNumber,
|
|
||||||
rand,
|
|
||||||
randFirstName,
|
|
||||||
randText,
|
|
||||||
} from '@ngneat/falso';
|
|
||||||
|
|
||||||
export const activityType = [
|
|
||||||
'Sport',
|
|
||||||
'Sciences',
|
|
||||||
'History',
|
|
||||||
'Maths',
|
|
||||||
'Physics',
|
|
||||||
] as const;
|
|
||||||
export type ActivityType = (typeof activityType)[number];
|
|
||||||
|
|
||||||
export interface Person {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Activity {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
type: ActivityType;
|
|
||||||
teacher: Person;
|
|
||||||
}
|
|
||||||
|
|
||||||
const factoryPerson = incrementalNumber();
|
|
||||||
|
|
||||||
export const randPerson = () => ({
|
|
||||||
id: factoryPerson(),
|
|
||||||
name: randFirstName(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const factoryActivity = incrementalNumber();
|
|
||||||
|
|
||||||
export const randActivity = (): Activity => ({
|
|
||||||
id: factoryActivity(),
|
|
||||||
name: randText(),
|
|
||||||
type: rand(activityType),
|
|
||||||
teacher: randPerson(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const activities: Activity[] = [
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
randActivity(),
|
|
||||||
];
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import * as ActivityActions from './activity.actions';
|
|
||||||
import { Activity } from './activity.model';
|
|
||||||
|
|
||||||
export const activityFeatureKey = 'Activity';
|
|
||||||
|
|
||||||
export interface ActivityState {
|
|
||||||
activities: Activity[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialState: ActivityState = {
|
|
||||||
activities: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const activityReducer = createReducer(
|
|
||||||
initialState,
|
|
||||||
on(ActivityActions.loadActivitiesSuccess, (state, { activities }) => ({
|
|
||||||
...state,
|
|
||||||
activities,
|
|
||||||
})),
|
|
||||||
on(ActivityActions.loadActivitiesFailure, (state) => ({
|
|
||||||
state,
|
|
||||||
activities: [],
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
import { ActivityState, activityFeatureKey } from './activity.reducer';
|
|
||||||
|
|
||||||
export const selectActivityState =
|
|
||||||
createFeatureSelector<ActivityState>(activityFeatureKey);
|
|
||||||
|
|
||||||
export const selectActivities = createSelector(
|
|
||||||
selectActivityState,
|
|
||||||
(state) => state.activities,
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { map, timer } from 'rxjs';
|
|
||||||
import { activities } from './activity.model';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class ActivityService {
|
|
||||||
fetchActivities = () => timer(500).pipe(map(() => activities));
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
import { Status } from './status.model';
|
|
||||||
|
|
||||||
export const loadStatuses = createAction('[Status] Load Statuses');
|
|
||||||
|
|
||||||
export const loadStatusesSuccess = createAction(
|
|
||||||
'[Status] Load Statuses Success',
|
|
||||||
props<{ statuses: Status[] }>(),
|
|
||||||
);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { combineLatest, concatMap, map } from 'rxjs';
|
|
||||||
import { selectActivities } from '../activity/activity.selectors';
|
|
||||||
import { selectUser } from '../user/user.selectors';
|
|
||||||
import * as StatusActions from './status.actions';
|
|
||||||
import { Status } from './status.model';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class StatusEffects {
|
|
||||||
loadStatuses$ = createEffect(() => {
|
|
||||||
return this.actions$.pipe(
|
|
||||||
ofType(StatusActions.loadStatuses),
|
|
||||||
concatMap(() =>
|
|
||||||
combineLatest([
|
|
||||||
this.store.select(selectUser),
|
|
||||||
this.store.select(selectActivities),
|
|
||||||
]).pipe(
|
|
||||||
map(([user, activities]): Status[] => {
|
|
||||||
if (user?.isAdmin) {
|
|
||||||
return activities.reduce(
|
|
||||||
(status: Status[], activity): Status[] => {
|
|
||||||
const index = status.findIndex(
|
|
||||||
(s) => s.name === activity.type,
|
|
||||||
);
|
|
||||||
if (index === -1) {
|
|
||||||
return [
|
|
||||||
...status,
|
|
||||||
{ name: activity.type, teachers: [activity.teacher] },
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
status[index].teachers.push(activity.teacher);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}),
|
|
||||||
map((statuses) => StatusActions.loadStatusesSuccess({ statuses })),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private actions$: Actions,
|
|
||||||
private store: Store,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { ActivityType, Person } from '../activity/activity.model';
|
|
||||||
|
|
||||||
export interface Status {
|
|
||||||
name: ActivityType;
|
|
||||||
teachers: Person[];
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import { ActivityType, Person } from '../activity/activity.model';
|
|
||||||
import * as StatusActions from './status.actions';
|
|
||||||
import { Status } from './status.model';
|
|
||||||
|
|
||||||
export const statusFeatureKey = 'status';
|
|
||||||
|
|
||||||
export interface StatusState {
|
|
||||||
statuses: Status[];
|
|
||||||
teachersMap: Map<ActivityType, Person[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialState: StatusState = {
|
|
||||||
statuses: [],
|
|
||||||
teachersMap: new Map(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const statusReducer = createReducer(
|
|
||||||
initialState,
|
|
||||||
on(StatusActions.loadStatusesSuccess, (state, { statuses }): StatusState => {
|
|
||||||
const map = new Map();
|
|
||||||
statuses.forEach((s) => map.set(s.name, s.teachers));
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
statuses,
|
|
||||||
teachersMap: map,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
import { ActivityType } from '../activity/activity.model';
|
|
||||||
import { StatusState, statusFeatureKey } from './status.reducer';
|
|
||||||
|
|
||||||
export const selectStatusState =
|
|
||||||
createFeatureSelector<StatusState>(statusFeatureKey);
|
|
||||||
|
|
||||||
export const selectStatuses = createSelector(
|
|
||||||
selectStatusState,
|
|
||||||
(state) => state.statuses,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const selectAllTeachersByActivityType = (name: ActivityType) =>
|
|
||||||
createSelector(
|
|
||||||
selectStatusState,
|
|
||||||
(state) => state.teachersMap.get(name) ?? [],
|
|
||||||
);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
import { User } from './user.model';
|
|
||||||
|
|
||||||
export const loadUsers = createAction('[User] Load Users');
|
|
||||||
|
|
||||||
export const loadUsersSuccess = createAction(
|
|
||||||
'[User] Load Users Success',
|
|
||||||
props<{ user: User }>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const loadUsersFailure = createAction(
|
|
||||||
'[User] Load Users Failure',
|
|
||||||
props<{ error: unknown }>(),
|
|
||||||
);
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { catchError, concatMap, map } from 'rxjs/operators';
|
|
||||||
import * as UserActions from './user.actions';
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserEffects {
|
|
||||||
loadUsers$ = createEffect(() => {
|
|
||||||
return this.actions$.pipe(
|
|
||||||
ofType(UserActions.loadUsers),
|
|
||||||
concatMap(() =>
|
|
||||||
this.userService.fetchUser().pipe(
|
|
||||||
map((user) => UserActions.loadUsersSuccess({ user })),
|
|
||||||
catchError((error) => of(UserActions.loadUsersFailure({ error }))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private actions$: Actions,
|
|
||||||
private userService: UserService,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { randFirstName, randLastName, randText } from '@ngneat/falso';
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
firstname: string;
|
|
||||||
lastname: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const user: User = {
|
|
||||||
firstname: randFirstName(),
|
|
||||||
lastname: randLastName(),
|
|
||||||
isAdmin: true,
|
|
||||||
apiKey: randText(),
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import * as UserActions from './user.actions';
|
|
||||||
import { User } from './user.model';
|
|
||||||
|
|
||||||
export const userFeatureKey = 'user';
|
|
||||||
|
|
||||||
export interface UserState {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialState: UserState = {
|
|
||||||
user: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userReducer = createReducer(
|
|
||||||
initialState,
|
|
||||||
on(UserActions.loadUsersSuccess, (state, { user }) => ({ ...state, user })),
|
|
||||||
on(UserActions.loadUsersFailure, (state) => ({ ...state, user: undefined })),
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
import { UserState, userFeatureKey } from './user.reducer';
|
|
||||||
|
|
||||||
export const selectUserState = createFeatureSelector<UserState>(userFeatureKey);
|
|
||||||
|
|
||||||
export const selectUser = createSelector(
|
|
||||||
selectUserState,
|
|
||||||
(state) => state.user,
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { map, timer } from 'rxjs';
|
|
||||||
import { user } from './user.model';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class UserService {
|
|
||||||
fetchUser = () => timer(500).pipe(map(() => user));
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>ngrx-effect-vs-selector</title>
|
|
||||||
<base href="/" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<app-root></app-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
|
||||||
import { appConfig } from './app/app.config';
|
|
||||||
|
|
||||||
import { AppComponent } from './app/app.component';
|
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
|
||||||
* You can add your own extra polyfills to this file.
|
|
||||||
*
|
|
||||||
* This file is divided into 2 sections:
|
|
||||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
|
||||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
|
||||||
* file.
|
|
||||||
*
|
|
||||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
|
||||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
|
||||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
|
||||||
*
|
|
||||||
* Learn more in https://angular.io/guide/browser-support
|
|
||||||
*/
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* BROWSER POLYFILLS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
|
||||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
|
||||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
|
||||||
* will put import in the top of bundle, so user need to create a separate file
|
|
||||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
|
||||||
* into that file, and then add the following code before importing zone.js.
|
|
||||||
* import './zone-flags';
|
|
||||||
*
|
|
||||||
* The flags allowed in zone-flags.ts are listed here.
|
|
||||||
*
|
|
||||||
* The following flags will work for all browsers.
|
|
||||||
*
|
|
||||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
|
||||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
|
||||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
|
||||||
*
|
|
||||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
|
||||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
|
||||||
*
|
|
||||||
* (window as any).__Zone_enable_cross_context_check = true;
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* Zone JS is required by default for Angular itself.
|
|
||||||
*/
|
|
||||||
import 'zone.js'; // Included with Angular CLI.
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* APPLICATION IMPORTS
|
|
||||||
*/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import 'jest-preset-angular/setup-jest';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../../dist/out-tsc",
|
|
||||||
"types": [],
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": false
|
|
||||||
},
|
|
||||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
|
||||||
"include": ["src/**/*.d.ts"],
|
|
||||||
"exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["**/*.ts"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["jest", "node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.base.json",
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.editor.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,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": "app",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@angular-eslint/component-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "element",
|
|
||||||
"prefix": "app",
|
|
||||||
"style": "kebab-case"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"extends": ["plugin:@nx/angular-template"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Power of Effect
|
|
||||||
|
|
||||||
> author: thomas-laforge
|
|
||||||
|
|
||||||
### Run Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx nx serve ngrx-power-of-effect
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documentation and Instruction
|
|
||||||
|
|
||||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/ngrx/7-power-effect/).
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ngrx-power-of-effect",
|
|
||||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"projectType": "application",
|
|
||||||
"sourceRoot": "apps/ngrx/7-power-of-effect/src",
|
|
||||||
"prefix": "app",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {
|
|
||||||
"build": {
|
|
||||||
"executor": "@angular-devkit/build-angular:browser",
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/apps/ngrx/7-power-of-effect",
|
|
||||||
"index": "apps/ngrx/7-power-of-effect/src/index.html",
|
|
||||||
"main": "apps/ngrx/7-power-of-effect/src/main.ts",
|
|
||||||
"polyfills": "apps/ngrx/7-power-of-effect/src/polyfills.ts",
|
|
||||||
"tsConfig": "apps/ngrx/7-power-of-effect/tsconfig.app.json",
|
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
|
||||||
"apps/ngrx/7-power-of-effect/src/favicon.ico",
|
|
||||||
"apps/ngrx/7-power-of-effect/src/assets"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"apps/ngrx/7-power-of-effect/src/styles.scss",
|
|
||||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
|
||||||
"allowedCommonJsDependencies": ["seedrandom"]
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kb",
|
|
||||||
"maximumError": "1mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "2kb",
|
|
||||||
"maximumError": "4kb"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputHashing": "all"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"optimization": false,
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"namedChunks": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "production"
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"executor": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "ngrx-power-of-effect:build:production"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildTarget": "ngrx-power-of-effect:build:development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "development"
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "ngrx-power-of-effect:build"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createActionGroup, emptyProps } from '@ngrx/store';
|
|
||||||
|
|
||||||
// This is the global actions.
|
|
||||||
export const appActions = createActionGroup({
|
|
||||||
source: 'App Component',
|
|
||||||
events: {
|
|
||||||
'Init App': emptyProps(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
|
||||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { appActions } from './app.actions';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
imports: [RouterOutlet, RouterLink],
|
|
||||||
selector: 'app-root',
|
|
||||||
template: `
|
|
||||||
<nav>
|
|
||||||
<button routerLink="/teacher">Teacher</button>
|
|
||||||
<button routerLink="/student">Student</button>
|
|
||||||
<button routerLink="/school">School</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppComponent implements OnInit {
|
|
||||||
private store = inject(Store);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.store.dispatch(appActions.initApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { FakeBackendService } from '@angular-challenges/power-of-effect/backend';
|
|
||||||
import {
|
|
||||||
ApplicationConfig,
|
|
||||||
inject,
|
|
||||||
provideAppInitializer,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
|
||||||
import { provideRouter } from '@angular/router';
|
|
||||||
import { provideEffects } from '@ngrx/effects';
|
|
||||||
import { provideStore } from '@ngrx/store';
|
|
||||||
import { NotificationService } from './data-access/notification.service';
|
|
||||||
import { ROUTES } from './routes';
|
|
||||||
import { StudentEffects } from './student/store/student.effects';
|
|
||||||
import {
|
|
||||||
studentReducer,
|
|
||||||
studentsFeatureKey,
|
|
||||||
} from './student/store/student.reducer';
|
|
||||||
import { TeacherEffects } from './teacher/store/teacher.effects';
|
|
||||||
import {
|
|
||||||
teacherReducer,
|
|
||||||
teachersFeatureKey,
|
|
||||||
} from './teacher/store/teacher.reducer';
|
|
||||||
|
|
||||||
const REDUCERS = {
|
|
||||||
[teachersFeatureKey]: teacherReducer,
|
|
||||||
[studentsFeatureKey]: studentReducer,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
|
||||||
providers: [
|
|
||||||
provideStore(REDUCERS),
|
|
||||||
provideEffects([TeacherEffects, StudentEffects]),
|
|
||||||
provideRouter(ROUTES),
|
|
||||||
provideAppInitializer(() => {
|
|
||||||
const initializerFn = (() => {
|
|
||||||
const service = inject(FakeBackendService);
|
|
||||||
return () => service.start();
|
|
||||||
})();
|
|
||||||
return initializerFn();
|
|
||||||
}),
|
|
||||||
provideAppInitializer(() => {
|
|
||||||
const initializerFn = (() => {
|
|
||||||
const service = inject(NotificationService);
|
|
||||||
return () => service.init();
|
|
||||||
})();
|
|
||||||
return initializerFn();
|
|
||||||
}),
|
|
||||||
provideAnimations(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { FakeBackendService } from '@angular-challenges/power-of-effect/backend';
|
|
||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { take } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class HttpService {
|
|
||||||
private fakeBackend = inject(FakeBackendService);
|
|
||||||
|
|
||||||
getAllTeachers = () => this.fakeBackend.getAllTeachers().pipe(take(1));
|
|
||||||
|
|
||||||
getAllStudents = () => this.fakeBackend.getAllStudents().pipe(take(1));
|
|
||||||
|
|
||||||
getAllSchools = () => this.fakeBackend.getAllSchools().pipe(take(1));
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { PushService } from '@angular-challenges/power-of-effect/backend';
|
|
||||||
import {
|
|
||||||
isSchool,
|
|
||||||
isStudent,
|
|
||||||
isTeacher,
|
|
||||||
Push,
|
|
||||||
} from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { filter } from 'rxjs';
|
|
||||||
import { studentActions } from '../student/store/student.actions';
|
|
||||||
import { teacherActions } from '../teacher/store/teacher.actions';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class NotificationService {
|
|
||||||
private pushService = inject(PushService);
|
|
||||||
private store = inject(Store);
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.pushService.notification$
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe((notification: Push) => {
|
|
||||||
if (isTeacher(notification)) {
|
|
||||||
this.store.dispatch(
|
|
||||||
teacherActions.addOneTeacher({ teacher: notification }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isStudent(notification)) {
|
|
||||||
this.store.dispatch(
|
|
||||||
studentActions.addOneStudent({ student: notification }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isSchool(notification)) {
|
|
||||||
// SchoolStore is a ComponentStore. We can't dispatch a school action here.
|
|
||||||
// We are stuck. We must have done something wrong and need to refactor...
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Route } from '@angular/router';
|
|
||||||
import { TeacherComponent } from './teacher/teacher.component';
|
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
|
||||||
{ path: '', pathMatch: 'full', redirectTo: 'teacher' },
|
|
||||||
{
|
|
||||||
path: 'teacher',
|
|
||||||
component: TeacherComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'student',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./student/student.component').then((m) => m.StudentComponent),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'school',
|
|
||||||
loadComponent: () =>
|
|
||||||
import('./school/school.component').then((m) => m.SchoolComponent),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
|
||||||
import { AsyncPipe, NgFor } from '@angular/common';
|
|
||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { provideComponentStore } from '@ngrx/component-store';
|
|
||||||
import { SchoolStore } from './school.store';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
imports: [NgFor, AsyncPipe],
|
|
||||||
providers: [provideComponentStore(SchoolStore)],
|
|
||||||
selector: 'school',
|
|
||||||
template: `
|
|
||||||
<h3>SCHOOL</h3>
|
|
||||||
<div *ngFor="let school of school$ | async">
|
|
||||||
{{ school.name }} - {{ school.version }}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
border: 1px solid red;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SchoolComponent {
|
|
||||||
private store = inject(SchoolStore);
|
|
||||||
school$ = this.store.schools$;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { School } from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ComponentStore, OnStoreInit } from '@ngrx/component-store';
|
|
||||||
import { tapResponse } from '@ngrx/operators';
|
|
||||||
import { pipe, switchMap } from 'rxjs';
|
|
||||||
import { HttpService } from '../data-access/http.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SchoolStore
|
|
||||||
extends ComponentStore<{ schools: School[] }>
|
|
||||||
implements OnStoreInit
|
|
||||||
{
|
|
||||||
readonly schools$ = this.select((state) => state.schools);
|
|
||||||
|
|
||||||
constructor(private httpService: HttpService) {
|
|
||||||
super({ schools: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
addSchool = this.updater((state, school: School) => ({
|
|
||||||
...state,
|
|
||||||
schools: [...state.schools, school],
|
|
||||||
}));
|
|
||||||
|
|
||||||
updateSchool = this.updater((state, school: School) => ({
|
|
||||||
...state,
|
|
||||||
schools: state.schools.map((t) => (t.id === school.id ? school : t)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
private readonly loadSchools = this.effect<void>(
|
|
||||||
pipe(
|
|
||||||
switchMap(() =>
|
|
||||||
this.httpService.getAllSchools().pipe(
|
|
||||||
tapResponse(
|
|
||||||
(schools) => this.patchState({ schools }),
|
|
||||||
(_) => _, // not handling the error
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
ngrxOnStoreInit() {
|
|
||||||
this.loadSchools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Student } from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { createActionGroup, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
export const studentActions = createActionGroup({
|
|
||||||
source: 'Student API',
|
|
||||||
events: {
|
|
||||||
'Add One Student': props<{ student: Student }>(),
|
|
||||||
'Add All Students': props<{ students: Student[] }>(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
import { map, switchMap } from 'rxjs';
|
|
||||||
import { appActions } from '../../app.actions';
|
|
||||||
import { HttpService } from '../../data-access/http.service';
|
|
||||||
import { studentActions } from './student.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class StudentEffects {
|
|
||||||
private actions$ = inject(Actions);
|
|
||||||
private httpService = inject(HttpService);
|
|
||||||
|
|
||||||
loadStudents$ = createEffect(() =>
|
|
||||||
this.actions$.pipe(
|
|
||||||
ofType(appActions.initApp),
|
|
||||||
switchMap(() =>
|
|
||||||
this.httpService
|
|
||||||
.getAllStudents()
|
|
||||||
.pipe(map((students) => studentActions.addAllStudents({ students }))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Student } from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
|
|
||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import { studentActions } from './student.actions';
|
|
||||||
|
|
||||||
export const studentsFeatureKey = 'students';
|
|
||||||
|
|
||||||
export type StudentState = EntityState<Student>;
|
|
||||||
|
|
||||||
export const studentAdapter: EntityAdapter<Student> =
|
|
||||||
createEntityAdapter<Student>();
|
|
||||||
|
|
||||||
export const studentReducer = createReducer(
|
|
||||||
studentAdapter.getInitialState(),
|
|
||||||
on(studentActions.addOneStudent, (state, { student }) =>
|
|
||||||
studentAdapter.upsertOne(student, state),
|
|
||||||
),
|
|
||||||
on(studentActions.addAllStudents, (state, { students }) =>
|
|
||||||
studentAdapter.setAll(students, state),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const { selectIds, selectEntities, selectAll, selectTotal } =
|
|
||||||
studentAdapter.getSelectors();
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
import {
|
|
||||||
StudentState,
|
|
||||||
studentAdapter,
|
|
||||||
studentsFeatureKey,
|
|
||||||
} from './student.reducer';
|
|
||||||
|
|
||||||
const selectStudentState =
|
|
||||||
createFeatureSelector<StudentState>(studentsFeatureKey);
|
|
||||||
|
|
||||||
export const { selectAll } = studentAdapter.getSelectors();
|
|
||||||
|
|
||||||
const selectStudents = createSelector(selectStudentState, selectAll);
|
|
||||||
|
|
||||||
export const StudentSelectors = {
|
|
||||||
selectStudents,
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
|
||||||
import { AsyncPipe, NgFor } from '@angular/common';
|
|
||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { StudentSelectors } from './store/student.selectors';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
imports: [NgFor, AsyncPipe],
|
|
||||||
selector: 'student',
|
|
||||||
template: `
|
|
||||||
<h3>STUDENTS</h3>
|
|
||||||
<div *ngFor="let student of students$ | async">
|
|
||||||
{{ student.firstname }} {{ student.lastname }} - {{ student.version }}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
border: 1px solid red;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class StudentComponent {
|
|
||||||
private store = inject(Store);
|
|
||||||
students$ = this.store.select(StudentSelectors.selectStudents);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Teacher } from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { createActionGroup, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
export const teacherActions = createActionGroup({
|
|
||||||
source: 'Teacher API',
|
|
||||||
events: {
|
|
||||||
'Add One Teacher': props<{ teacher: Teacher }>(),
|
|
||||||
'Add All Teachers': props<{ teachers: Teacher[] }>(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
import { map, switchMap } from 'rxjs';
|
|
||||||
import { appActions } from '../../app.actions';
|
|
||||||
import { HttpService } from '../../data-access/http.service';
|
|
||||||
import { teacherActions } from './teacher.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TeacherEffects {
|
|
||||||
private actions$ = inject(Actions);
|
|
||||||
private httpService = inject(HttpService);
|
|
||||||
|
|
||||||
loadTeachers$ = createEffect(() =>
|
|
||||||
this.actions$.pipe(
|
|
||||||
ofType(appActions.initApp),
|
|
||||||
switchMap(() =>
|
|
||||||
this.httpService
|
|
||||||
.getAllTeachers()
|
|
||||||
.pipe(map((teachers) => teacherActions.addAllTeachers({ teachers }))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Teacher } from '@angular-challenges/power-of-effect/model';
|
|
||||||
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
|
|
||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import { teacherActions } from './teacher.actions';
|
|
||||||
|
|
||||||
export const teachersFeatureKey = 'teachers';
|
|
||||||
|
|
||||||
export type TeacherState = EntityState<Teacher>;
|
|
||||||
|
|
||||||
export const teacherAdapter: EntityAdapter<Teacher> =
|
|
||||||
createEntityAdapter<Teacher>();
|
|
||||||
|
|
||||||
export const teacherReducer = createReducer(
|
|
||||||
teacherAdapter.getInitialState(),
|
|
||||||
on(teacherActions.addOneTeacher, (state, { teacher }) =>
|
|
||||||
teacherAdapter.upsertOne(teacher, state),
|
|
||||||
),
|
|
||||||
on(teacherActions.addAllTeachers, (state, { teachers }) =>
|
|
||||||
teacherAdapter.setAll(teachers, state),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const { selectIds, selectEntities, selectAll, selectTotal } =
|
|
||||||
teacherAdapter.getSelectors();
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
import {
|
|
||||||
TeacherState,
|
|
||||||
teacherAdapter,
|
|
||||||
teachersFeatureKey,
|
|
||||||
} from './teacher.reducer';
|
|
||||||
|
|
||||||
const selectTeacherState =
|
|
||||||
createFeatureSelector<TeacherState>(teachersFeatureKey);
|
|
||||||
|
|
||||||
export const { selectAll } = teacherAdapter.getSelectors();
|
|
||||||
|
|
||||||
const selectTeachers = createSelector(selectTeacherState, selectAll);
|
|
||||||
|
|
||||||
export const TeacherSelectors = {
|
|
||||||
selectTeachers,
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
|
||||||
import { AsyncPipe, NgFor } from '@angular/common';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { TeacherSelectors } from './store/teacher.selectors';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
imports: [NgFor, AsyncPipe],
|
|
||||||
selector: 'teacher',
|
|
||||||
template: `
|
|
||||||
<h3>TEACHERS</h3>
|
|
||||||
<div *ngFor="let teacher of teacher$ | async">
|
|
||||||
{{ teacher.firstname }} {{ teacher.lastname }} - {{ teacher.version }}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
border: 1px solid red;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class TeacherComponent {
|
|
||||||
teacher$ = this.store.select(TeacherSelectors.selectTeachers);
|
|
||||||
|
|
||||||
constructor(private store: Store) {}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>ngrx-power-of-effect</title>
|
|
||||||
<base href="/" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<app-root></app-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { appConfig } from './app/app.config';
|
|
||||||
|
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { AppComponent } from './app/app.component';
|
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
|
||||||
console.error(err),
|
|
||||||
);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
|
||||||
* You can add your own extra polyfills to this file.
|
|
||||||
*
|
|
||||||
* This file is divided into 2 sections:
|
|
||||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
|
||||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
|
||||||
* file.
|
|
||||||
*
|
|
||||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
|
||||||
* automatically update themselves. This includes recent versions of Safari, Chrome (including
|
|
||||||
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
|
|
||||||
*
|
|
||||||
* Learn more in https://angular.io/guide/browser-support
|
|
||||||
*/
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* BROWSER POLYFILLS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
|
||||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
|
||||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
|
||||||
* will put import in the top of bundle, so user need to create a separate file
|
|
||||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
|
||||||
* into that file, and then add the following code before importing zone.js.
|
|
||||||
* import './zone-flags';
|
|
||||||
*
|
|
||||||
* The flags allowed in zone-flags.ts are listed here.
|
|
||||||
*
|
|
||||||
* The following flags will work for all browsers.
|
|
||||||
*
|
|
||||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
|
||||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
|
||||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
|
||||||
*
|
|
||||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
|
||||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
|
||||||
*
|
|
||||||
* (window as any).__Zone_enable_cross_context_check = true;
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* Zone JS is required by default for Angular itself.
|
|
||||||
*/
|
|
||||||
import 'zone.js'; // Included with Angular CLI.
|
|
||||||
|
|
||||||
/***************************************************************************************************
|
|
||||||
* APPLICATION IMPORTS
|
|
||||||
*/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../../dist/out-tsc",
|
|
||||||
"types": [],
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": false
|
|
||||||
},
|
|
||||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
|
||||||
"include": ["src/**/*.d.ts"],
|
|
||||||
"exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
"../../../libs/power-of-effect/backend/src/lib/fake-backend.service.ts"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.base.json",
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.editor.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,46 +1,44 @@
|
|||||||
/* eslint-disable @angular-eslint/component-selector */
|
/* eslint-disable @angular-eslint/component-selector */
|
||||||
import { AsyncPipe, NgFor } from '@angular/common';
|
import { Component, inject, input, signal } from '@angular/core';
|
||||||
import { Component, Input, inject } from '@angular/core';
|
import { take } from 'rxjs';
|
||||||
import { BehaviorSubject, take } from 'rxjs';
|
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TopicType } from './localDB.service';
|
import { TopicType } from './localDB.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'button-delete-topic',
|
selector: 'button-delete-topic',
|
||||||
imports: [AsyncPipe],
|
|
||||||
template: `
|
template: `
|
||||||
<button (click)="deleteTopic()"><ng-content></ng-content></button>
|
<button (click)="deleteTopic()"><ng-content /></button>
|
||||||
<div>{{ message$$ | async }}</div>
|
<div>{{ message() }}</div>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class ButtonDeleteComponent {
|
export class ButtonDeleteComponent {
|
||||||
@Input() topic!: TopicType;
|
readonly topic = input.required<TopicType>();
|
||||||
|
|
||||||
message$$ = new BehaviorSubject<string>('');
|
message = signal('');
|
||||||
|
|
||||||
private service = inject(AppService);
|
private service = inject(AppService);
|
||||||
|
|
||||||
deleteTopic() {
|
deleteTopic() {
|
||||||
this.service
|
this.service
|
||||||
.deleteOldTopics(this.topic)
|
.deleteOldTopics(this.topic())
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((result) =>
|
.subscribe((result) =>
|
||||||
this.message$$.next(
|
this.message.set(
|
||||||
result
|
result
|
||||||
? `All ${this.topic} have been deleted`
|
? `All ${this.topic()} have been deleted`
|
||||||
: `Error: deletion of some ${this.topic} failed`,
|
: `Error: deletion of some ${this.topic()} failed`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
imports: [AsyncPipe, NgFor, ButtonDeleteComponent],
|
imports: [ButtonDeleteComponent],
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: `
|
template: `
|
||||||
<div *ngFor="let item of all$ | async">
|
@for (info of allInfo(); track info.id) {
|
||||||
{{ item.id }} - {{ item.topic }}
|
<div>{{ info.id }} - {{ info.topic }}</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<button-delete-topic topic="food">Delete Food</button-delete-topic>
|
<button-delete-topic topic="food">Delete Food</button-delete-topic>
|
||||||
<button-delete-topic topic="sport">Delete Sport</button-delete-topic>
|
<button-delete-topic topic="sport">Delete Sport</button-delete-topic>
|
||||||
@@ -50,5 +48,5 @@ export class ButtonDeleteComponent {
|
|||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
private service = inject(AppService);
|
private service = inject(AppService);
|
||||||
|
|
||||||
all$ = this.service.getAll$;
|
allInfo = this.service.getAllInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { merge, mergeMap, Observable, of, take } from 'rxjs';
|
import { merge, Observable, of } from 'rxjs';
|
||||||
import { LocalDBService, TopicType } from './localDB.service';
|
import { LocalDBService, TopicType } from './localDB.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AppService {
|
export class AppService {
|
||||||
private dbService = inject(LocalDBService);
|
private dbService = inject(LocalDBService);
|
||||||
|
|
||||||
getAll$ = this.dbService.infos$;
|
getAllInfo = this.dbService.infos;
|
||||||
|
|
||||||
deleteOldTopics(type: TopicType): Observable<boolean> {
|
deleteOldTopics(type: TopicType): Observable<boolean> {
|
||||||
return this.dbService.searchByType(type).pipe(
|
const infoByType = this.dbService.searchByType(type);
|
||||||
take(1),
|
return infoByType.length > 0
|
||||||
mergeMap((topicToDelete) =>
|
? infoByType
|
||||||
topicToDelete.length > 0
|
.map((t) => this.dbService.deleteOneTopic(t.id))
|
||||||
? topicToDelete
|
.reduce((acc, curr) => merge(acc, curr), of(true))
|
||||||
.map((t) => this.dbService.deleteOneTopic(t.id))
|
: of(true);
|
||||||
.reduce((acc, curr) => merge(acc, curr), of(true))
|
|
||||||
: of(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/member-ordering */
|
/* eslint-disable @typescript-eslint/member-ordering */
|
||||||
import { randomError } from '@angular-challenges/shared/utils';
|
import { randomError } from '@angular-challenges/shared/utils';
|
||||||
import { Injectable } from '@angular/core';
|
import { computed, Injectable, signal } from '@angular/core';
|
||||||
import { ComponentStore } from '@ngrx/component-store';
|
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
export type TopicType = 'food' | 'book' | 'sport';
|
export type TopicType = 'food' | 'book' | 'sport';
|
||||||
@@ -31,21 +30,17 @@ const initialState: DBState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LocalDBService extends ComponentStore<DBState> {
|
export class LocalDBService {
|
||||||
constructor() {
|
private state = signal(initialState);
|
||||||
super(initialState);
|
|
||||||
}
|
|
||||||
|
|
||||||
infos$ = this.select((state) => state.infos);
|
infos = computed(() => this.state().infos);
|
||||||
|
|
||||||
searchByType = (type: TopicType) =>
|
searchByType = (type: TopicType) =>
|
||||||
this.select((state) => state.infos.filter((i) => i.topic === type));
|
this.infos().filter((i) => i.topic === type);
|
||||||
|
|
||||||
deleteOne = this.updater(
|
deleteOne = (id: number) => {
|
||||||
(state, id: number): DBState => ({
|
this.state.set({ infos: this.state().infos.filter((i) => i.id !== id) });
|
||||||
infos: state.infos.filter((i) => i.id !== id),
|
};
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
deleteOneTopic = (id: number) =>
|
deleteOneTopic = (id: number) =>
|
||||||
randomError({
|
randomError({
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
title: 🟠 Effect vs Selector
|
|
||||||
description: Challenge 2 is about learning the difference between effects and selectors in NgRx
|
|
||||||
author: thomas-laforge
|
|
||||||
contributors:
|
|
||||||
- tomalaforge
|
|
||||||
- tomer953
|
|
||||||
- svenson95
|
|
||||||
- jdegand
|
|
||||||
- LMFinney
|
|
||||||
challengeNumber: 2
|
|
||||||
command: ngrx-effect-vs-selector
|
|
||||||
blogLink: https://medium.com/@thomas.laforge/ngrx-effect-vs-reducer-vs-selector-58337ab59043
|
|
||||||
videoLinks:
|
|
||||||
- link: https://youtu.be/7fr6JBRocQM
|
|
||||||
alt: Effect vs selector video by Amos Lucian Isaila
|
|
||||||
flag: ES
|
|
||||||
sidebar:
|
|
||||||
order: 113
|
|
||||||
---
|
|
||||||
|
|
||||||
For this exercise, you will have a dashboard of activities displaying the name, the main teacher and a list of possible substitutes.
|
|
||||||
|
|
||||||
## Information
|
|
||||||
|
|
||||||
In NgRx, **selectors** is a very powerful tool that is often **misused**. You should use them as soon as you need to transform an already existing data in the store.
|
|
||||||
|
|
||||||
- You shouldn't store **derived state**. This is error-prone because when your data changes, you will have to change it at multiple places => you should have only one place of truth with that data, and every transformation should be done in a **selector**.
|
|
||||||
|
|
||||||
- Inside a component, you shouldn't transform a selector (using the map operator), and you shouldn't have to call a selector from a function in your view. The useful logic for preparing data for a component should be done in a **selector**.
|
|
||||||
|
|
||||||
## Statement
|
|
||||||
|
|
||||||
You will have to refactor this working example of a dashboard of activities.
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- Only **one action** should be dispatched from a component (or none, if you can solve the problem with Effect lifecycle hooks).
|
|
||||||
- Status effect is useless. Using **combineLatest** should be a red flag. Effects are made for side effects, not for transforming data. That's a selector's role.
|
|
||||||
- Status state might not be useful; it's only a **derived state** of existing state.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
title: 🔴 Power of Effect
|
|
||||||
description: Challenge 7 is about creating an NgRx effect with another Rxjs Hot observable
|
|
||||||
author: thomas-laforge
|
|
||||||
contributors:
|
|
||||||
- tomalaforge
|
|
||||||
- tomer953
|
|
||||||
- jdegand
|
|
||||||
- LMFinney
|
|
||||||
challengeNumber: 7
|
|
||||||
command: ngrx-power-of-effect
|
|
||||||
sidebar:
|
|
||||||
order: 206
|
|
||||||
---
|
|
||||||
|
|
||||||
## Information
|
|
||||||
|
|
||||||
This application exhibits local and global state confusion. Right now, a notification service is used to update the component lists of students and teachers. You need to add schools to this service, but you _cannot_. The school component uses its _own_ local state inside a `ComponentStore`. Thus, you are unable to dispatch an action in the notification service that the school component can respond to (remember, component stores do not have `actions`). So, how can we get around these issues?
|
|
||||||
|
|
||||||
Injection tokens and NgRx effects can greatly help.
|
|
||||||
|
|
||||||
`NgRx Effects` is a very powerful library developed by the NgRx team. Effects subscribe to a HOT Observable and listen for all events dispatched inside an application. `NgRx Effects` can subscribe to _any_ observable, which means we can wrap a hot observable inside an effect and add logic to it. You don't have to worry about the local or global state. Although you should be mindful of bad practices, when you refactor this application, you should make a determination of what should be a part of the local state and what should be a part of the global state.
|
|
||||||
|
|
||||||
In this exercise, we will need to find a way to create a very powerful, scalable, and maintainable push message listener.
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
Create an injection token to `inject` the push service inside each component. Injection tokens are very powerful. If you are unfamiliar with them, you may want to complete the [Injection token challenge](https://angular-challenges.vercel.app/challenges/angular/39-injection-token) first. This [article](https://netbasal.com/the-hidden-power-of-injectiontoken-factory-functions-in-angular-d42d5575859b) is also a great resource.
|
|
||||||
|
|
||||||
_Eliminate_ the notification service. It is not extensible. Testing (not required for this challenge) the notification service would also be overly complicated. You would need to test each branching scenario. Injection tokens can easily be mocked.
|
|
||||||
|
|
||||||
Since the notification service is global, all component lists update, even if a user is not on that route. We need to decouple that logic. The notification messages should display only on their respective routes.
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
Create one NgRx effect or component store effect for each push type, and implement your logic.
|
|
||||||
|
|
||||||
### Step 3
|
|
||||||
|
|
||||||
Show an [Angular Material snackbar](https://material.angular.io/components/snack-bar/overview) notification when an add event happens. Make each notification distinct.
|
|
||||||
|
|
||||||
### Step 4
|
|
||||||
|
|
||||||
Load your effect only when necessary.
|
|
||||||
|
|
||||||
The application contains a root route, a lazy loaded route, and a component with a local state (implemented with `ComponentStore`).
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user