From bf5c09e563747b0397603f5e0f403bbac9d8fbdf Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:15:28 +0200 Subject: [PATCH 1/9] feat(1-projection): project card image via ng-content instead of type-based conditionals CardComponent no longer branches on CardType to pick an image; TeacherCardComponent and StudentCardComponent project their own through an [card-image] attribute selector, and each keeps its own NgOptimizedImage import. --- .../student-card/student-card.component.ts | 9 +++++++-- .../teacher-card/teacher-card.component.ts | 13 +++++++------ .../1-projection/src/app/ui/card/card.component.ts | 13 ++----------- 3 files changed, 16 insertions(+), 19 deletions(-) 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..0a56274d2 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,3 +1,4 @@ +import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -15,7 +16,11 @@ import { CardComponent } from '../../ui/card/card.component'; + customClass="bg-light-green"> + + + + `, styles: [ ` @@ -24,7 +29,7 @@ import { CardComponent } from '../../ui/card/card.component'; } `, ], - imports: [CardComponent], + imports: [CardComponent, NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent implements OnInit { 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..86c074b17 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,3 +1,4 @@ +import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -8,14 +9,14 @@ import { FakeHttpService } from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; import { CardComponent } from '../../ui/card/card.component'; - @Component({ selector: 'app-teacher-card', template: ` - + + + + + `, styles: [ ` @@ -25,7 +26,7 @@ import { CardComponent } from '../../ui/card/card.component'; `, ], changeDetection: ChangeDetectionStrategy.Eager, - imports: [CardComponent], + imports: [CardComponent, NgOptimizedImage], }) export class TeacherCardComponent implements OnInit { private http = inject(FakeHttpService); 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..34351e31e 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,4 +1,3 @@ -import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -17,13 +16,7 @@ import { ListItemComponent } from '../list-item/list-item.component';
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - - } - +
@for (item of list(); track item) { `, changeDetection: ChangeDetectionStrategy.Eager, - imports: [ListItemComponent, NgOptimizedImage], + imports: [ListItemComponent], }) export class CardComponent { private teacherStore = inject(TeacherStore); @@ -51,8 +44,6 @@ export class CardComponent { readonly type = input.required(); readonly customClass = input(''); - CardType = CardType; - addNewItem() { const type = this.type(); if (type === CardType.TEACHER) { From 436edc4c701da0334b47d62ccfcad4a6bac5f777 Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:06:18 +0200 Subject: [PATCH 2/9] feat(1-projection): replace CardComponent's type-based add logic with output() CardComponent no longer injects TeacherStore/StudentStore or branches on CardType to add items. It emits an add output on button click; each consumer card decides what "add" means for its own store. --- .../student-card/student-card.component.ts | 10 +++++++++- .../teacher-card/teacher-card.component.ts | 15 ++++++++++++-- .../src/app/ui/card/card.component.ts | 20 +++---------------- 3 files changed, 25 insertions(+), 20 deletions(-) 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 0a56274d2..ff0d1a0ab 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 @@ -5,7 +5,10 @@ import { inject, OnInit, } from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; +import { + FakeHttpService, + randStudent, +} from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; import { CardType } from '../../model/card.model'; import { CardComponent } from '../../ui/card/card.component'; @@ -16,6 +19,7 @@ import { CardComponent } from '../../ui/card/card.component'; @@ -42,4 +46,8 @@ export class StudentCardComponent implements OnInit { ngOnInit(): void { this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); } + + addStudent(): void { + this.store.addOne(randStudent()); + } } 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 86c074b17..6b8e6952a 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 @@ -5,14 +5,21 @@ import { inject, OnInit, } from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; +import { + FakeHttpService, + randTeacher, +} from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; import { CardComponent } from '../../ui/card/card.component'; @Component({ selector: 'app-teacher-card', template: ` - + @@ -38,4 +45,8 @@ export class TeacherCardComponent implements OnInit { ngOnInit(): void { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); } + + addTeacher(): void { + this.store.addOne(randTeacher()); + } } 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 34351e31e..7d5008735 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,12 +1,9 @@ import { ChangeDetectionStrategy, Component, - inject, 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'; @@ -28,7 +25,7 @@ import { ListItemComponent } from '../list-item/list-item.component';
@@ -37,19 +34,8 @@ import { ListItemComponent } from '../list-item/list-item.component'; imports: [ListItemComponent], }) export class CardComponent { - private teacherStore = inject(TeacherStore); - private studentStore = inject(StudentStore); - readonly list = input(null); readonly type = input.required(); readonly customClass = input(''); - - addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } - } + add = output(); } From d9e80884d585c18f24edb5e88c124fa79e5f47ca Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:19:57 +0200 Subject: [PATCH 3/9] feat(1-projection): replace ListItemComponent's type-based delete logic with output() ListItemComponent no longer injects TeacherStore/StudentStore or branches on CardType to delete items; it just emits a delete output. CardComponent relays it upward without resolving which store to call, and each consumer card handles the deletion for its own store. --- .../student-card/student-card.component.ts | 5 +++++ .../teacher-card/teacher-card.component.ts | 5 +++++ .../src/app/ui/card/card.component.ts | 3 ++- .../app/ui/list-item/list-item.component.ts | 21 +++---------------- 4 files changed, 15 insertions(+), 19 deletions(-) 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 ff0d1a0ab..338118197 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 @@ -20,6 +20,7 @@ import { CardComponent } from '../../ui/card/card.component'; [list]="students()" [type]="cardType" (add)="addStudent()" + (delete)="deleteStudent($event)" customClass="bg-light-green"> @@ -50,4 +51,8 @@ export class StudentCardComponent implements OnInit { 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 6b8e6952a..a69a4a984 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 @@ -19,6 +19,7 @@ import { CardComponent } from '../../ui/card/card.component'; [list]="teachers()" [type]="cardType" (add)="addTeacher()" + (delete)="deleteTeacher($event)" customClass="bg-light-red"> @@ -49,4 +50,8 @@ export class TeacherCardComponent implements OnInit { addTeacher(): void { this.store.addOne(randTeacher()); } + + deleteTeacher(id: number): void { + this.store.deleteOne(id); + } } 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 7d5008735..d850cc54e 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 @@ -19,7 +19,7 @@ import { ListItemComponent } from '../list-item/list-item.component'; + (delete)="delete.emit($event)"> } @@ -38,4 +38,5 @@ export class CardComponent { readonly type = input.required(); readonly customClass = input(''); add = output(); + delete = 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(); } From c57610b14359286909e840eec2330696a755a2d5 Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:32:54 +0200 Subject: [PATCH 4/9] feat(1-projection): project list item template via NgTemplateOutlet + CardItemDirective CardComponent no longer hardcodes app-list-item or knows about CardType; its @for loop just outlets a template provided by the consumer through a projected marked by CardItemDirective, keeping the item's expressions in the consumer's own scope (add/delete are wired directly to its store there, no relay through CardComponent needed). --- .../student-card/student-card.component.ts | 19 ++++++++++++++----- .../teacher-card/teacher-card.component.ts | 19 ++++++++++++++----- .../src/app/directive/card-item.directive.ts | 8 ++++++++ .../src/app/ui/card/card.component.ts | 17 ++++++++--------- 4 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 apps/angular/1-projection/src/app/directive/card-item.directive.ts 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 338118197..7b2ad0c05 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 @@ -10,18 +10,23 @@ import { 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: ` + + + @@ -34,7 +39,12 @@ import { CardComponent } from '../../ui/card/card.component'; } `, ], - imports: [CardComponent, NgOptimizedImage], + imports: [ + CardComponent, + ListItemComponent, + CardItemDirective, + NgOptimizedImage, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent implements OnInit { @@ -42,7 +52,6 @@ export class StudentCardComponent implements OnInit { private store = inject(StudentStore); students = this.store.students; - cardType = CardType.STUDENT; ngOnInit(): void { this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); 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 a69a4a984..4e3edb05f 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 @@ -10,17 +10,22 @@ import { 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: ` + + + @@ -34,14 +39,18 @@ import { CardComponent } from '../../ui/card/card.component'; `, ], changeDetection: ChangeDetectionStrategy.Eager, - imports: [CardComponent, NgOptimizedImage], + imports: [ + CardComponent, + ListItemComponent, + CardItemDirective, + NgOptimizedImage, + ], }) export class TeacherCardComponent implements OnInit { private http = inject(FakeHttpService); private store = inject(TeacherStore); teachers = this.store.teachers; - cardType = CardType.TEACHER; ngOnInit(): void { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); 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 d850cc54e..f195cbb56 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,11 +1,12 @@ +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, + contentChild, input, output, } from '@angular/core'; -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', @@ -16,10 +17,9 @@ import { ListItemComponent } from '../list-item/list-item.component';
@for (item of list(); track item) { - + }
@@ -31,12 +31,11 @@ import { ListItemComponent } from '../list-item/list-item.component'; `, changeDetection: ChangeDetectionStrategy.Eager, - imports: [ListItemComponent], + imports: [CommonModule], }) export class CardComponent { readonly list = input(null); - readonly type = input.required(); readonly customClass = input(''); + readonly itemTemplate = contentChild.required(CardItemDirective); add = output(); - delete = output(); } From f11baebf069604dca85cf412b7237032c19da36f Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:01:41 +0200 Subject: [PATCH 5/9] refactor(1-projection): replace ::ng-deep background hack with a CSS custom property CardComponent declares background: var(--bg, gray) on its own element in its own styles, so no encapsulation-piercing selector is needed. Each consumer sets --bg on the app-card host it controls; customClass input is gone, and CardComponent stays unaware of any specific color. --- .../component/student-card/student-card.component.ts | 9 +-------- .../component/teacher-card/teacher-card.component.ts | 9 +-------- .../1-projection/src/app/ui/card/card.component.ts | 11 ++++++++--- 3 files changed, 10 insertions(+), 19 deletions(-) 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 7b2ad0c05..fe4339897 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 @@ -20,7 +20,7 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; + [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, 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 4e3edb05f..41197be9c 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 @@ -19,7 +19,7 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; + [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, 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 f195cbb56..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 @@ -12,8 +12,7 @@ import { CardItemDirective } from '../../directive/card-item.directive'; selector: 'app-card', template: `
+ class="card-container flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4">
@for (item of list(); track item) { @@ -30,12 +29,18 @@ import { CardItemDirective } from '../../directive/card-item.directive';
`, + styles: [ + ` + .card-container { + background: var(--bg, gray); + } + `, + ], changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule], }) export class CardComponent { readonly list = input(null); - readonly customClass = input(''); readonly itemTemplate = contentChild.required(CardItemDirective); add = output(); } From 185481586e860aabb8954922e52a59a137de647f Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:16:06 +0200 Subject: [PATCH 6/9] feat(1-projection): implement CityCardComponent without touching CardComponent CityCardComponent wires app-card the same way student/teacher cards do: its own item template (city name, no CardType), its own --bg value, and its own add/delete handlers backed by CityStore. CardComponent needed no changes, confirming the projection-based refactor generalizes to a new card variant. --- .../city-card/city-card.component.ts | 59 +++++++++++++++++-- .../src/app/data-access/city.store.ts | 2 +- 2 files changed, 56 insertions(+), 5 deletions(-) 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..6037a7b27 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,60 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { NgOptimizedImage } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, +} from '@angular/core'; +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 implements OnInit { + private http = inject(FakeHttpService); + private store = inject(CityStore); + + cities = this.store.cities; + + ngOnInit(): void { + this.http.fetchCities$.subscribe((s) => this.store.addAll(s)); + } + + addCity(): void { + this.store.addOne(randomCity()); + } + + deleteCity(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..9fbcb346b 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 @@ -5,7 +5,7 @@ import { City } from '../model/city.model'; providedIn: 'root', }) export class CityStore { - private cities = signal([]); + public cities = signal([]); addAll(cities: City[]) { this.cities.set(cities); From b55c600971a4fccaa7420cd39f32afd073e98000 Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:24:31 +0200 Subject: [PATCH 7/9] refactor(1-projection): fetch data via toSignal + effect instead of ngOnInit/subscribe StudentCardComponent, TeacherCardComponent and CityCardComponent no longer implement OnInit or manually subscribe to their fetch$ observable; each converts it with toSignal() and syncs the store from an effect() in the constructor, keeping the loading flow signal-based. --- .../component/city-card/city-card.component.ts | 15 +++++++++++---- .../student-card/student-card.component.ts | 15 +++++++++++---- .../teacher-card/teacher-card.component.ts | 15 +++++++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) 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 6037a7b27..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 @@ -2,9 +2,10 @@ import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, - OnInit, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { CityStore } from '../../data-access/city.store'; import { FakeHttpService, @@ -40,14 +41,20 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent implements OnInit { +export class CityCardComponent { private http = inject(FakeHttpService); private store = inject(CityStore); + private fetchedCities = toSignal(this.http.fetchCities$); cities = this.store.cities; - ngOnInit(): void { - this.http.fetchCities$.subscribe((s) => this.store.addAll(s)); + constructor() { + effect(() => { + const cities = this.fetchedCities(); + if (cities) { + this.store.addAll(cities); + } + }); } addCity(): void { 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 fe4339897..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 @@ -2,9 +2,10 @@ import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, - OnInit, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FakeHttpService, randStudent, @@ -40,14 +41,20 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; ], 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; - 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 { 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 41197be9c..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 @@ -2,9 +2,10 @@ import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, + effect, inject, - OnInit, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FakeHttpService, randTeacher, @@ -39,14 +40,20 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; 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; - 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 { From a40883b275258967374aadbe3b650f69fa61580d Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:16:06 +0200 Subject: [PATCH 8/9] fix(1-projection): merge fetched entities instead of overwriting store addAll() was replacing the whole signal with the fetched list, so a locally added teacher/student/city added before fetchTeachers$ (or fetchStudents$/fetchCities$) emitted got silently dropped. Now it merges the fetch result with the existing entries, deduping by id. --- apps/angular/1-projection/src/app/data-access/city.store.ts | 6 +++++- .../1-projection/src/app/data-access/student.store.ts | 6 +++++- .../1-projection/src/app/data-access/teacher.store.ts | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) 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 9fbcb346b..fd02228eb 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 @@ -8,7 +8,11 @@ export class CityStore { public cities = signal([]); addAll(cities: City[]) { - this.cities.set(cities); + const existingIds = new Set(this.cities().map((c) => c.id)); + this.cities.set([ + ...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..dc81f918b 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 @@ -8,7 +8,11 @@ export class StudentStore { public students = signal([]); addAll(students: Student[]) { - this.students.set(students); + const existingIds = new Set(this.students().map((s) => s.id)); + this.students.set([ + ...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..f58e39a23 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 @@ -8,7 +8,11 @@ export class TeacherStore { public teachers = signal([]); addAll(teachers: Teacher[]) { - this.teachers.set(teachers); + const existingIds = new Set(this.teachers().map((t) => t.id)); + this.teachers.set([ + ...this.teachers(), + ...teachers.filter((t) => !existingIds.has(t.id)), + ]); } addOne(teacher: Teacher) { From 30d0215023c29a7b634a3ebc3ea2c78d5d86b8c0 Mon Sep 17 00:00:00 2001 From: grzegorz555 <149683412+grzegorz555@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:27:58 +0200 Subject: [PATCH 9/9] fix(1-projection): avoid effect reschedule loop in store addAll Read the store's own signal with untracked() inside addAll so effects that call it don't track it as a dependency and rerun on every write. --- apps/angular/1-projection/src/app/data-access/city.store.ts | 6 +++--- .../1-projection/src/app/data-access/student.store.ts | 6 +++--- .../1-projection/src/app/data-access/teacher.store.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) 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 fd02228eb..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,4 +1,4 @@ -import { Injectable, signal } from '@angular/core'; +import { Injectable, signal, untracked } from '@angular/core'; import { City } from '../model/city.model'; @Injectable({ @@ -8,9 +8,9 @@ export class CityStore { public cities = signal([]); addAll(cities: City[]) { - const existingIds = new Set(this.cities().map((c) => c.id)); + const existingIds = new Set(untracked(this.cities).map((c) => c.id)); this.cities.set([ - ...this.cities(), + ...untracked(this.cities), ...cities.filter((c) => !existingIds.has(c.id)), ]); } 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 dc81f918b..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,9 +8,9 @@ export class StudentStore { public students = signal([]); addAll(students: Student[]) { - const existingIds = new Set(this.students().map((s) => s.id)); + const existingIds = new Set(untracked(this.students).map((s) => s.id)); this.students.set([ - ...this.students(), + ...untracked(this.students), ...students.filter((s) => !existingIds.has(s.id)), ]); } 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 f58e39a23..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,9 +8,9 @@ export class TeacherStore { public teachers = signal([]); addAll(teachers: Teacher[]) { - const existingIds = new Set(this.teachers().map((t) => t.id)); + const existingIds = new Set(untracked(this.teachers).map((t) => t.id)); this.teachers.set([ - ...this.teachers(), + ...untracked(this.teachers), ...teachers.filter((t) => !existingIds.has(t.id)), ]); }