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