diff --git a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts index 8895c8c84..50dcd6454 100644 --- a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts +++ b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts @@ -1,9 +1,67 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgOptimizedImage } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CityStore } from '../../data-access/city.store'; +import { + FakeHttpService, + randomCity, +} from '../../data-access/fake-http.service'; +import { CardItemDirective } from '../../directive/card-item.directive'; +import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-city-card', - template: 'TODO City', - imports: [], + template: ` + + + + + + + + + `, + imports: [ + CardComponent, + ListItemComponent, + CardItemDirective, + NgOptimizedImage, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent {} +export class CityCardComponent { + private http = inject(FakeHttpService); + private store = inject(CityStore); + + private fetchedCities = toSignal(this.http.fetchCities$); + cities = this.store.cities; + + constructor() { + effect(() => { + const cities = this.fetchedCities(); + if (cities) { + this.store.addAll(cities); + } + }); + } + + addCity(): void { + this.store.addOne(randomCity()); + } + + deleteCity(id: number): void { + this.store.deleteOne(id); + } +} diff --git a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts index bdfa4abd4..1026fe6cc 100644 --- a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts +++ b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts @@ -1,40 +1,67 @@ +import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, - OnInit, } from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + FakeHttpService, + randStudent, +} from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; -import { CardType } from '../../model/card.model'; +import { CardItemDirective } from '../../directive/card-item.directive'; import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-student-card', template: ` + (add)="addStudent()" + [style.--bg]="'rgba(0, 250, 0, 0.1)'"> + + + + + + + `, - styles: [ - ` - ::ng-deep .bg-light-green { - background-color: rgba(0, 250, 0, 0.1); - } - `, + imports: [ + CardComponent, + ListItemComponent, + CardItemDirective, + NgOptimizedImage, ], - imports: [CardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class StudentCardComponent implements OnInit { +export class StudentCardComponent { private http = inject(FakeHttpService); private store = inject(StudentStore); + private fetchedStudents = toSignal(this.http.fetchStudents$); students = this.store.students; - cardType = CardType.STUDENT; - ngOnInit(): void { - this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); + constructor() { + effect(() => { + const students = this.fetchedStudents(); + if (students) { + this.store.addAll(students); + } + }); + } + + addStudent(): void { + this.store.addOne(randStudent()); + } + + deleteStudent(id: number): void { + this.store.deleteOne(id); } } diff --git a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts index 3e454ac19..fbf4ca741 100644 --- a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts +++ b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts @@ -1,40 +1,66 @@ +import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, - OnInit, } from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + FakeHttpService, + randTeacher, +} from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; -import { CardType } from '../../model/card.model'; +import { CardItemDirective } from '../../directive/card-item.directive'; import { CardComponent } from '../../ui/card/card.component'; - +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-teacher-card', template: ` + (add)="addTeacher()" + [style.--bg]="'rgba(250, 0, 0, 0.1)'"> + + + + + + + `, - styles: [ - ` - ::ng-deep .bg-light-red { - background-color: rgba(250, 0, 0, 0.1); - } - `, - ], changeDetection: ChangeDetectionStrategy.Eager, - imports: [CardComponent], + imports: [ + CardComponent, + ListItemComponent, + CardItemDirective, + NgOptimizedImage, + ], }) -export class TeacherCardComponent implements OnInit { +export class TeacherCardComponent { private http = inject(FakeHttpService); private store = inject(TeacherStore); + private fetchedTeachers = toSignal(this.http.fetchTeachers$); teachers = this.store.teachers; - cardType = CardType.TEACHER; - ngOnInit(): void { - this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); + constructor() { + effect(() => { + const teachers = this.fetchedTeachers(); + if (teachers) { + this.store.addAll(teachers); + } + }); + } + + addTeacher(): void { + this.store.addOne(randTeacher()); + } + + deleteTeacher(id: number): void { + this.store.deleteOne(id); } } diff --git a/apps/angular/1-projection/src/app/data-access/city.store.ts b/apps/angular/1-projection/src/app/data-access/city.store.ts index a8b523569..3510ae24e 100644 --- a/apps/angular/1-projection/src/app/data-access/city.store.ts +++ b/apps/angular/1-projection/src/app/data-access/city.store.ts @@ -1,14 +1,18 @@ -import { Injectable, signal } from '@angular/core'; +import { Injectable, signal, untracked } from '@angular/core'; import { City } from '../model/city.model'; @Injectable({ providedIn: 'root', }) export class CityStore { - private cities = signal([]); + public cities = signal([]); addAll(cities: City[]) { - this.cities.set(cities); + const existingIds = new Set(untracked(this.cities).map((c) => c.id)); + this.cities.set([ + ...untracked(this.cities), + ...cities.filter((c) => !existingIds.has(c.id)), + ]); } addOne(city: City) { diff --git a/apps/angular/1-projection/src/app/data-access/student.store.ts b/apps/angular/1-projection/src/app/data-access/student.store.ts index 6e7f57022..23369a1dd 100644 --- a/apps/angular/1-projection/src/app/data-access/student.store.ts +++ b/apps/angular/1-projection/src/app/data-access/student.store.ts @@ -1,4 +1,4 @@ -import { Injectable, signal } from '@angular/core'; +import { Injectable, signal, untracked } from '@angular/core'; import { Student } from '../model/student.model'; @Injectable({ @@ -8,7 +8,11 @@ export class StudentStore { public students = signal([]); addAll(students: Student[]) { - this.students.set(students); + const existingIds = new Set(untracked(this.students).map((s) => s.id)); + this.students.set([ + ...untracked(this.students), + ...students.filter((s) => !existingIds.has(s.id)), + ]); } addOne(student: Student) { diff --git a/apps/angular/1-projection/src/app/data-access/teacher.store.ts b/apps/angular/1-projection/src/app/data-access/teacher.store.ts index 5f6dae989..ae3eee1ef 100644 --- a/apps/angular/1-projection/src/app/data-access/teacher.store.ts +++ b/apps/angular/1-projection/src/app/data-access/teacher.store.ts @@ -1,4 +1,4 @@ -import { Injectable, signal } from '@angular/core'; +import { Injectable, signal, untracked } from '@angular/core'; import { Teacher } from '../model/teacher.model'; @Injectable({ @@ -8,7 +8,11 @@ export class TeacherStore { public teachers = signal([]); addAll(teachers: Teacher[]) { - this.teachers.set(teachers); + const existingIds = new Set(untracked(this.teachers).map((t) => t.id)); + this.teachers.set([ + ...untracked(this.teachers), + ...teachers.filter((t) => !existingIds.has(t.id)), + ]); } addOne(teacher: Teacher) { diff --git a/apps/angular/1-projection/src/app/directive/card-item.directive.ts b/apps/angular/1-projection/src/app/directive/card-item.directive.ts new file mode 100644 index 000000000..109f42f97 --- /dev/null +++ b/apps/angular/1-projection/src/app/directive/card-item.directive.ts @@ -0,0 +1,8 @@ +import { Directive, inject, TemplateRef } from '@angular/core'; + +@Directive({ + selector: '[appCardItem]', +}) +export class CardItemDirective { + templateRef = inject(TemplateRef); +} diff --git a/apps/angular/1-projection/src/app/ui/card/card.component.ts b/apps/angular/1-projection/src/app/ui/card/card.component.ts index d395474e2..c25a64186 100644 --- a/apps/angular/1-projection/src/app/ui/card/card.component.ts +++ b/apps/angular/1-projection/src/app/ui/card/card.component.ts @@ -1,64 +1,46 @@ -import { NgOptimizedImage } from '@angular/common'; +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, - inject, + contentChild, input, + output, } from '@angular/core'; -import { randStudent, randTeacher } 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'; -import { ListItemComponent } from '../list-item/list-item.component'; +import { CardItemDirective } from '../../directive/card-item.directive'; @Component({ selector: 'app-card', template: `
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - - } - + class="card-container flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"> +
@for (item of list(); track item) { - + }
`, + styles: [ + ` + .card-container { + background: var(--bg, gray); + } + `, + ], changeDetection: ChangeDetectionStrategy.Eager, - imports: [ListItemComponent, NgOptimizedImage], + imports: [CommonModule], }) export class CardComponent { - private teacherStore = inject(TeacherStore); - private studentStore = inject(StudentStore); - readonly list = input(null); - readonly type = input.required(); - readonly customClass = input(''); - - CardType = CardType; - - addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } - } + readonly itemTemplate = contentChild.required(CardItemDirective); + add = output(); } diff --git a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts index c04a29c05..616590f80 100644 --- a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts +++ b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts @@ -1,19 +1,16 @@ import { ChangeDetectionStrategy, Component, - inject, input, + output, } from '@angular/core'; -import { StudentStore } from '../../data-access/student.store'; -import { TeacherStore } from '../../data-access/teacher.store'; -import { CardType } from '../../model/card.model'; @Component({ selector: 'app-list-item', template: `
{{ name() }} -
@@ -21,19 +18,7 @@ import { CardType } from '../../model/card.model'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ListItemComponent { - private teacherStore = inject(TeacherStore); - private studentStore = inject(StudentStore); - readonly id = input.required(); readonly name = input.required(); - readonly type = input.required(); - - delete(id: number) { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.deleteOne(id); - } else if (type === CardType.STUDENT) { - this.studentStore.deleteOne(id); - } - } + delete = output(); }