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);
}
}
}