From 705da0320c978ea9147aaee9c8d49c8fb5a53c68 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 20 Mar 2024 16:14:47 +0100 Subject: [PATCH 1/6] finish task projection ! --- .../city-card/city-card.component.ts | 35 ++++++++++++++++--- .../src/app/ui/card/card.component.ts | 17 +++++++-- .../app/ui/list-item/list-item.component.ts | 4 +++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/apps/angular/projection/src/app/component/city-card/city-card.component.ts b/apps/angular/projection/src/app/component/city-card/city-card.component.ts index 30c8f88ec..22612eb10 100644 --- a/apps/angular/projection/src/app/component/city-card/city-card.component.ts +++ b/apps/angular/projection/src/app/component/city-card/city-card.component.ts @@ -1,13 +1,40 @@ import { Component, OnInit } from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; +import { FakeHttpService } from '../../data-access/fake-http.service'; +import { CardType } from '../../model/card.model'; +import { City } from '../../model/city.model'; +import { CardComponent } from '../../ui/card/card.component'; @Component({ selector: 'app-city-card', - template: 'TODO City', + template: ` + + `, + styles: [ + ` + ::ng-deep .bg-light-red { + background-color: rgba(250, 0, 0, 0.1); + } + `, + ], standalone: true, - imports: [], + imports: [CardComponent], }) export class CityCardComponent implements OnInit { - constructor() {} + cities: City[] = []; + cardType = CardType.CITY; - ngOnInit(): void {} + constructor( + private http: FakeHttpService, + private store: CityStore, + ) {} + + ngOnInit(): void { + this.http.fetchCities$.subscribe((s) => this.store.addAll(s)); + + this.store.cities$.subscribe((s) => (this.cities = s)); + } } diff --git a/apps/angular/projection/src/app/ui/card/card.component.ts b/apps/angular/projection/src/app/ui/card/card.component.ts index f06c9ae00..417757cc5 100644 --- a/apps/angular/projection/src/app/ui/card/card.component.ts +++ b/apps/angular/projection/src/app/ui/card/card.component.ts @@ -1,6 +1,11 @@ import { NgFor, NgIf } from '@angular/common'; import { Component, Input } from '@angular/core'; -import { randStudent, randTeacher } from '../../data-access/fake-http.service'; +import { CityStore } from '../../data-access/city.store'; +import { + randStudent, + randTeacher, + randomCity, +} from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; @@ -21,10 +26,15 @@ import { ListItemComponent } from '../list-item/list-item.component'; src="assets/img/student.webp" width="200px" /> + +
@@ -49,6 +59,7 @@ export class CardComponent { constructor( private teacherStore: TeacherStore, private studentStore: StudentStore, + private cityStore: CityStore, ) {} addNewItem() { @@ -56,6 +67,8 @@ export class CardComponent { this.teacherStore.addOne(randTeacher()); } else if (this.type === CardType.STUDENT) { this.studentStore.addOne(randStudent()); + } else if (this.type === CardType.CITY) { + this.cityStore.addOne(randomCity()); } } } diff --git a/apps/angular/projection/src/app/ui/list-item/list-item.component.ts b/apps/angular/projection/src/app/ui/list-item/list-item.component.ts index c0f9cff7f..e6a77dfcb 100644 --- a/apps/angular/projection/src/app/ui/list-item/list-item.component.ts +++ b/apps/angular/projection/src/app/ui/list-item/list-item.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; import { StudentStore } from '../../data-access/student.store'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; @@ -23,6 +24,7 @@ export class ListItemComponent { constructor( private teacherStore: TeacherStore, private studentStore: StudentStore, + private cityStore: CityStore, ) {} delete(id: number) { @@ -30,6 +32,8 @@ export class ListItemComponent { this.teacherStore.deleteOne(id); } else if (this.type === CardType.STUDENT) { this.studentStore.deleteOne(id); + } else if (this.type === CardType.CITY) { + this.cityStore.deleteOne(id); } } } From 11964fa7c94c5e98e6543fc2ab33b5459a95d3a0 Mon Sep 17 00:00:00 2001 From: michal Date: Mon, 25 Mar 2024 08:26:44 +0100 Subject: [PATCH 2/6] fix: correct style css --- .../src/app/component/city-card/city-card.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/angular/projection/src/app/component/city-card/city-card.component.ts b/apps/angular/projection/src/app/component/city-card/city-card.component.ts index 22612eb10..6d9fbe00e 100644 --- a/apps/angular/projection/src/app/component/city-card/city-card.component.ts +++ b/apps/angular/projection/src/app/component/city-card/city-card.component.ts @@ -15,7 +15,7 @@ import { CardComponent } from '../../ui/card/card.component'; `, styles: [ ` - ::ng-deep .bg-light-red { + .bg-light-red { background-color: rgba(250, 0, 0, 0.1); } `, From 981ba9b308889e928ac26607df34aa006e88de25 Mon Sep 17 00:00:00 2001 From: michal Date: Mon, 25 Mar 2024 08:29:12 +0100 Subject: [PATCH 3/6] fix: correct style css v1 --- .../src/app/component/city-card/city-card.component.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/angular/projection/src/app/component/city-card/city-card.component.ts b/apps/angular/projection/src/app/component/city-card/city-card.component.ts index 6d9fbe00e..558e39117 100644 --- a/apps/angular/projection/src/app/component/city-card/city-card.component.ts +++ b/apps/angular/projection/src/app/component/city-card/city-card.component.ts @@ -13,13 +13,6 @@ import { CardComponent } from '../../ui/card/card.component'; [type]="cardType" customClass="bg-light-red"> `, - styles: [ - ` - .bg-light-red { - background-color: rgba(250, 0, 0, 0.1); - } - `, - ], standalone: true, imports: [CardComponent], }) From 6cca0fde5ea4aca2e05030e3d2a5439d7465ad5c Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 2 Apr 2024 11:41:22 +0200 Subject: [PATCH 4/6] fix: steps 1, 2 --- apps/angular/crud/src/app/app.component.ts | 80 ++++++++++++++-------- apps/angular/crud/src/app/todo.service.ts | 65 ++++++++++++++++++ apps/angular/crud/src/app/todo.ts | 6 ++ 3 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 apps/angular/crud/src/app/todo.service.ts create mode 100644 apps/angular/crud/src/app/todo.ts diff --git a/apps/angular/crud/src/app/app.component.ts b/apps/angular/crud/src/app/app.component.ts index 8c3d1b8ae..57826954d 100644 --- a/apps/angular/crud/src/app/app.component.ts +++ b/apps/angular/crud/src/app/app.component.ts @@ -1,51 +1,75 @@ import { CommonModule } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; -import { randText } from '@ngneat/falso'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { take, timer } from 'rxjs'; +import { Todo } from './todo'; +import { TodoService } from './todo.service'; @Component({ standalone: true, - imports: [CommonModule], + imports: [CommonModule, MatProgressSpinnerModule], selector: 'app-root', template: ` +
{{ todo.title }} +
`, - styles: [], + styles: [ + ` + .mdc-circular-progress { + position: absolute; + transition: opacity 250ms 0ms cubic-bezier(0.4, 0, 0.6, 1); + margin: auto; + right: 0; + left: 0; + bottom: 0; + top: 0; + } + `, + ], }) export class AppComponent implements OnInit { - todos!: any[]; + todos!: Todo[]; + loading: boolean = true; - constructor(private http: HttpClient) {} + constructor(private todoService: TodoService) {} + + endLoader(): void { + timer(3000) + .pipe(take(1)) + .subscribe(() => (this.loading = false)); + } ngOnInit(): void { - this.http - .get('https://jsonplaceholder.typicode.com/todos') - .subscribe((todos) => { + this.todoService.getAllTodos().subscribe({ + next: (todos) => { + this.loading = true; this.todos = todos; - }); + }, + complete: () => this.endLoader(), + }); } - update(todo: any) { - this.http - .put( - `https://jsonplaceholder.typicode.com/todos/${todo.id}`, - JSON.stringify({ - todo: todo.id, - title: randText(), - body: todo.body, - userId: todo.userId, - }), - { - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }, - ) - .subscribe((todoUpdated: any) => { + update(todo: Todo) { + this.todoService.updateSingleTodo(todo).subscribe({ + complete: () => this.endLoader(), + next: (todoUpdated: Todo) => { + this.loading = true; this.todos[todoUpdated.id - 1] = todoUpdated; - }); + }, + }); + } + + delete(id: number) { + this.todoService.removeSingleTodo(id).subscribe({ + next: () => (this.loading = true), + complete: () => this.endLoader(), + }); } } diff --git a/apps/angular/crud/src/app/todo.service.ts b/apps/angular/crud/src/app/todo.service.ts new file mode 100644 index 000000000..2bb0758c8 --- /dev/null +++ b/apps/angular/crud/src/app/todo.service.ts @@ -0,0 +1,65 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { randText } from '@ngneat/falso'; +import { catchError, Observable } from 'rxjs'; +import { Todo } from './todo'; + +@Injectable({ + providedIn: 'root', +}) +export class TodoService { + constructor(private http: HttpClient) {} + + getAllTodos(): Observable { + return this.http + .get('https://jsonplaceholder.typicode.com/todos') + .pipe( + catchError((err) => { + console.error('Caught in CatchError. Throwing error'); + + throw new Error(err); + }), + ); + } + + updateSingleTodo(todo: Todo): Observable { + return this.http + .put( + `https://jsonplaceholder.typicode.com/todos/${todo.id}`, + JSON.stringify({ + id: todo.id, + title: randText(), + userId: todo.userId, + completed: todo.completed, + }), + { + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + }, + ) + .pipe( + catchError((err) => { + console.error('Caught in CatchError. Throwing error'); + + throw new Error(err); + }), + ); + } + + removeSingleTodo(id: number): Observable { + return this.http + .delete(`https://jsonplaceholder.typicode.com/todos/${id}`, { + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + }) + .pipe( + catchError((err) => { + console.error('Caught in CatchError. Throwing error'); + + throw new Error(err); + }), + ); + } +} diff --git a/apps/angular/crud/src/app/todo.ts b/apps/angular/crud/src/app/todo.ts new file mode 100644 index 000000000..db525ce1d --- /dev/null +++ b/apps/angular/crud/src/app/todo.ts @@ -0,0 +1,6 @@ +export interface Todo { + userId: number; + id: number; + title: string; + completed: boolean; +} From f29843c9bad2d52725cc4c58e8946b0fa1f0cb03 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 19 May 2026 16:49:22 +0200 Subject: [PATCH 5/6] feat(crud): crud finish task --- apps/angular/crud/src/app/app.component.ts | 86 +++++++++++++--------- apps/angular/crud/src/app/todo.service.ts | 46 ++++++++---- 2 files changed, 83 insertions(+), 49 deletions(-) diff --git a/apps/angular/crud/src/app/app.component.ts b/apps/angular/crud/src/app/app.component.ts index 57826954d..72c050a8b 100644 --- a/apps/angular/crud/src/app/app.component.ts +++ b/apps/angular/crud/src/app/app.component.ts @@ -1,7 +1,14 @@ import { CommonModule } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, signal } from '@angular/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { take, timer } from 'rxjs'; +import { + Observable, + Subject, + finalize, + retry, + takeUntil, + throttleTime, +} from 'rxjs'; import { Todo } from './todo'; import { TodoService } from './todo.service'; @@ -10,15 +17,22 @@ import { TodoService } from './todo.service'; imports: [CommonModule, MatProgressSpinnerModule], selector: 'app-root', template: ` - -
- {{ todo.title }} - - -
+ @defer (when loading) { + @for (todo of todos$ | async; track $index) { +
+ {{ todo.title }} | + + | + +
+ } + } @loading (minimum 3000) { + + } @error { +

There was a problem to load component.

+ } `, styles: [ ` @@ -34,42 +48,44 @@ import { TodoService } from './todo.service'; `, ], }) -export class AppComponent implements OnInit { - todos!: Todo[]; - loading: boolean = true; +export class AppComponent implements OnInit, OnDestroy { + loading = signal(true); + todoSubject$: Subject = new Subject(); + todos$: Observable = this.todoService.todos$; constructor(private todoService: TodoService) {} endLoader(): void { - timer(3000) - .pipe(take(1)) - .subscribe(() => (this.loading = false)); + this.loading.set(false); } ngOnInit(): void { - this.todoService.getAllTodos().subscribe({ - next: (todos) => { - this.loading = true; - this.todos = todos; - }, - complete: () => this.endLoader(), - }); + this.todoService + .getAllTodos() + .pipe( + takeUntil(this.todoSubject$), + retry(2), + finalize(() => this.endLoader()), + ) + .subscribe(); } update(todo: Todo) { - this.todoService.updateSingleTodo(todo).subscribe({ - complete: () => this.endLoader(), - next: (todoUpdated: Todo) => { - this.loading = true; - this.todos[todoUpdated.id - 1] = todoUpdated; - }, - }); + this.todoService + .updateSingleTodo(todo) + .pipe(throttleTime(500), retry(2), takeUntil(this.todoSubject$)) + .subscribe(); } delete(id: number) { - this.todoService.removeSingleTodo(id).subscribe({ - next: () => (this.loading = true), - complete: () => this.endLoader(), - }); + this.todoService + .removeSingleTodo(id) + .pipe(takeUntil(this.todoSubject$), throttleTime(500)) + .subscribe(); + } + + ngOnDestroy(): void { + this.todoSubject$.next([]); + this.todoSubject$.complete(); } } diff --git a/apps/angular/crud/src/app/todo.service.ts b/apps/angular/crud/src/app/todo.service.ts index 2bb0758c8..5e36ba507 100644 --- a/apps/angular/crud/src/app/todo.service.ts +++ b/apps/angular/crud/src/app/todo.service.ts @@ -1,24 +1,31 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { randText } from '@ngneat/falso'; -import { catchError, Observable } from 'rxjs'; +import { + BehaviorSubject, + catchError, + Observable, + of, + tap, + throwError, +} from 'rxjs'; import { Todo } from './todo'; @Injectable({ providedIn: 'root', }) export class TodoService { + private readonly todosSubject$ = new BehaviorSubject([]); + readonly todos$ = this.todosSubject$.asObservable(); + constructor(private http: HttpClient) {} getAllTodos(): Observable { return this.http .get('https://jsonplaceholder.typicode.com/todos') .pipe( - catchError((err) => { - console.error('Caught in CatchError. Throwing error'); - - throw new Error(err); - }), + catchError((err) => this.throwErrorMessage(err)), + tap((todos) => this.todosSubject$.next(todos)), ); } @@ -39,10 +46,13 @@ export class TodoService { }, ) .pipe( - catchError((err) => { - console.error('Caught in CatchError. Throwing error'); - - throw new Error(err); + catchError((err) => this.throwErrorMessage(err)), + tap((updatedTodo) => { + const current = this.todosSubject$.value; + const updated = current.map((t) => + t.id === updatedTodo.id ? updatedTodo : t, + ); + this.todosSubject$.next(updated); }), ); } @@ -55,11 +65,19 @@ export class TodoService { }, }) .pipe( - catchError((err) => { - console.error('Caught in CatchError. Throwing error'); - - throw new Error(err); + catchError((err) => this.throwErrorMessage(err)), + tap(() => { + const current = this.todosSubject$.value; + this.todosSubject$.next(current.filter((t) => t.id !== id)); }), ); } + + throwErrorMessage(err: any): Observable { + return throwError(() => { + const message = new Error(err?.message || err); + console.error(message); + return of([]); + }); + } } From fb8ba5a6e5f1bc05efd64e58549c8f28d9344d91 Mon Sep 17 00:00:00 2001 From: michal Date: Thu, 21 May 2026 16:55:56 +0200 Subject: [PATCH 6/6] fix(Answer:21): change structure code to signals --- apps/angular/crud/src/app/app.component.ts | 56 ++++++++++++---------- apps/angular/crud/src/app/todo.service.ts | 50 ++++--------------- 2 files changed, 39 insertions(+), 67 deletions(-) diff --git a/apps/angular/crud/src/app/app.component.ts b/apps/angular/crud/src/app/app.component.ts index 72c050a8b..ea673dbbe 100644 --- a/apps/angular/crud/src/app/app.component.ts +++ b/apps/angular/crud/src/app/app.component.ts @@ -1,14 +1,8 @@ import { CommonModule } from '@angular/common'; -import { Component, OnDestroy, OnInit, signal } from '@angular/core'; +import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { - Observable, - Subject, - finalize, - retry, - takeUntil, - throttleTime, -} from 'rxjs'; +import { BehaviorSubject, finalize, retry, tap } from 'rxjs'; import { Todo } from './todo'; import { TodoService } from './todo.service'; @@ -17,8 +11,8 @@ import { TodoService } from './todo.service'; imports: [CommonModule, MatProgressSpinnerModule], selector: 'app-root', template: ` - @defer (when loading) { - @for (todo of todos$ | async; track $index) { + @defer (when !loading()) { + @for (todo of todos(); track todo.id) {
{{ todo.title }} | @@ -26,10 +20,10 @@ import { TodoService } from './todo.service';
} - } @loading (minimum 3000) { + } @loading (minimum 1000) { + value="50"> } @error {

There was a problem to load component.

} @@ -48,12 +42,12 @@ import { TodoService } from './todo.service'; `, ], }) -export class AppComponent implements OnInit, OnDestroy { +export class AppComponent implements OnInit { + private todoService = inject(TodoService); + private readonly todosSubject$ = new BehaviorSubject([]); + readonly todos = signal([]); loading = signal(true); - todoSubject$: Subject = new Subject(); - todos$: Observable = this.todoService.todos$; - - constructor(private todoService: TodoService) {} + private subject$ = inject(DestroyRef); endLoader(): void { this.loading.set(false); @@ -63,9 +57,10 @@ export class AppComponent implements OnInit, OnDestroy { this.todoService .getAllTodos() .pipe( - takeUntil(this.todoSubject$), + takeUntilDestroyed(this.subject$), retry(2), finalize(() => this.endLoader()), + tap((todos) => this.todos.set(todos)), ) .subscribe(); } @@ -73,19 +68,28 @@ export class AppComponent implements OnInit, OnDestroy { update(todo: Todo) { this.todoService .updateSingleTodo(todo) - .pipe(throttleTime(500), retry(2), takeUntil(this.todoSubject$)) + .pipe( + takeUntilDestroyed(this.subject$), + tap((updatedTodo) => { + this.todos.update((current) => + current.map((t) => (t.id === updatedTodo.id ? updatedTodo : t)), + ); + }), + ) .subscribe(); } delete(id: number) { this.todoService .removeSingleTodo(id) - .pipe(takeUntil(this.todoSubject$), throttleTime(500)) + .pipe( + takeUntilDestroyed(this.subject$), + tap(() => { + const current = this.todosSubject$.value; + this.todosSubject$.next(current.filter((t) => t.id !== id)); + this.todos.update((current) => current.filter((t) => t.id !== id)); + }), + ) .subscribe(); } - - ngOnDestroy(): void { - this.todoSubject$.next([]); - this.todoSubject$.complete(); - } } diff --git a/apps/angular/crud/src/app/todo.service.ts b/apps/angular/crud/src/app/todo.service.ts index 5e36ba507..c7d64084a 100644 --- a/apps/angular/crud/src/app/todo.service.ts +++ b/apps/angular/crud/src/app/todo.service.ts @@ -1,60 +1,38 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { randText } from '@ngneat/falso'; -import { - BehaviorSubject, - catchError, - Observable, - of, - tap, - throwError, -} from 'rxjs'; +import { Observable, catchError, throwError } from 'rxjs'; import { Todo } from './todo'; @Injectable({ providedIn: 'root', }) export class TodoService { - private readonly todosSubject$ = new BehaviorSubject([]); - readonly todos$ = this.todosSubject$.asObservable(); - constructor(private http: HttpClient) {} getAllTodos(): Observable { return this.http .get('https://jsonplaceholder.typicode.com/todos') - .pipe( - catchError((err) => this.throwErrorMessage(err)), - tap((todos) => this.todosSubject$.next(todos)), - ); + .pipe(catchError((err) => this.throwErrorMessage(err))); } updateSingleTodo(todo: Todo): Observable { return this.http .put( `https://jsonplaceholder.typicode.com/todos/${todo.id}`, - JSON.stringify({ + { id: todo.id, title: randText(), userId: todo.userId, completed: todo.completed, - }), + }, { headers: { 'Content-type': 'application/json; charset=UTF-8', }, }, ) - .pipe( - catchError((err) => this.throwErrorMessage(err)), - tap((updatedTodo) => { - const current = this.todosSubject$.value; - const updated = current.map((t) => - t.id === updatedTodo.id ? updatedTodo : t, - ); - this.todosSubject$.next(updated); - }), - ); + .pipe(catchError((err) => this.throwErrorMessage(err))); } removeSingleTodo(id: number): Observable { @@ -64,20 +42,10 @@ export class TodoService { 'Content-type': 'application/json; charset=UTF-8', }, }) - .pipe( - catchError((err) => this.throwErrorMessage(err)), - tap(() => { - const current = this.todosSubject$.value; - this.todosSubject$.next(current.filter((t) => t.id !== id)); - }), - ); + .pipe(catchError((err) => this.throwErrorMessage(err))); } - throwErrorMessage(err: any): Observable { - return throwError(() => { - const message = new Error(err?.message || err); - console.error(message); - return of([]); - }); + throwErrorMessage(err: HttpErrorResponse): Observable { + return throwError(() => new Error(err?.message || 'Server error')); } }