diff --git a/apps/angular/crud/src/app/app.component.ts b/apps/angular/crud/src/app/app.component.ts index 8c3d1b8ae..ea673dbbe 100644 --- a/apps/angular/crud/src/app/app.component.ts +++ b/apps/angular/crud/src/app/app.component.ts @@ -1,51 +1,95 @@ import { CommonModule } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; -import { Component, OnInit } from '@angular/core'; -import { randText } from '@ngneat/falso'; +import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { BehaviorSubject, finalize, retry, tap } 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 }} - -
+ @defer (when !loading()) { + @for (todo of todos(); track todo.id) { +
+ {{ todo.title }} | + + | + +
+ } + } @loading (minimum 1000) { + + } @error { +

There was a problem to load component.

+ } `, - 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[]; + private todoService = inject(TodoService); + private readonly todosSubject$ = new BehaviorSubject([]); + readonly todos = signal([]); + loading = signal(true); + private subject$ = inject(DestroyRef); - constructor(private http: HttpClient) {} + endLoader(): void { + this.loading.set(false); + } ngOnInit(): void { - this.http - .get('https://jsonplaceholder.typicode.com/todos') - .subscribe((todos) => { - this.todos = todos; - }); + this.todoService + .getAllTodos() + .pipe( + takeUntilDestroyed(this.subject$), + retry(2), + finalize(() => this.endLoader()), + tap((todos) => this.todos.set(todos)), + ) + .subscribe(); + } + + update(todo: Todo) { + this.todoService + .updateSingleTodo(todo) + .pipe( + takeUntilDestroyed(this.subject$), + tap((updatedTodo) => { + this.todos.update((current) => + current.map((t) => (t.id === updatedTodo.id ? updatedTodo : t)), + ); + }), + ) + .subscribe(); } - 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, + delete(id: number) { + this.todoService + .removeSingleTodo(id) + .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)); }), - { - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }, ) - .subscribe((todoUpdated: any) => { - this.todos[todoUpdated.id - 1] = todoUpdated; - }); + .subscribe(); } } 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..c7d64084a --- /dev/null +++ b/apps/angular/crud/src/app/todo.service.ts @@ -0,0 +1,51 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { randText } from '@ngneat/falso'; +import { Observable, catchError, throwError } 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) => this.throwErrorMessage(err))); + } + + updateSingleTodo(todo: Todo): Observable { + return this.http + .put( + `https://jsonplaceholder.typicode.com/todos/${todo.id}`, + { + 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))); + } + + 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) => this.throwErrorMessage(err))); + } + + throwErrorMessage(err: HttpErrorResponse): Observable { + return throwError(() => new Error(err?.message || 'Server error')); + } +} 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; +} 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..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 @@ -1,13 +1,33 @@ 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: ` + + `, 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); } } }