From 12dd8adb4dd7b32f46e7f9a932f92a857052a5bf Mon Sep 17 00:00:00 2001 From: Assaf <1214assaf@gmail.com> Date: Fri, 8 May 2026 12:32:13 +0300 Subject: [PATCH] feat(projection): solve challenge 1 using ng-content and ngTemplateOutlet --- .../city-card/city-card.component.ts | 42 ++++++++++++-- .../student-card/student-card.component.ts | 45 ++++++++++----- .../teacher-card/teacher-card.component.ts | 45 ++++++++++----- .../src/app/data-access/city.store.ts | 2 +- .../src/app/ui/card/card-item.directive.ts | 19 +++++++ .../src/app/ui/card/card.component.ts | 56 +++++++------------ .../app/ui/list-item/list-item.component.ts | 20 +------ 7 files changed, 142 insertions(+), 87 deletions(-) create mode 100644 apps/angular/1-projection/src/app/ui/card/card-item.directive.ts 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..64d20c5de 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,43 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +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 '../../ui/card/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, CardItemDirective, ListItemComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent {} +export class CityCardComponent implements OnInit { + private http = inject(FakeHttpService); + protected store = inject(CityStore); + + randomCity = randomCity; + + ngOnInit(): void { + this.http.fetchCities$.subscribe((c) => this.store.addAll(c)); + } +} 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..c8401976e 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,38 +1,53 @@ +import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, 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 { CardItemDirective } from '../../ui/card/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: ` + [list]="store.students()" + customClass="bg-green-100" + (add)="store.addOne(randStudent())"> + + + + + `, - styles: [ - ` - ::ng-deep .bg-light-green { - background-color: rgba(0, 250, 0, 0.1); - } - `, + imports: [ + CardComponent, + CardItemDirective, + ListItemComponent, + NgOptimizedImage, ], - imports: [CardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent implements OnInit { private http = inject(FakeHttpService); - private store = inject(StudentStore); + protected store = inject(StudentStore); - students = this.store.students; - cardType = CardType.STUDENT; + randStudent = randStudent; 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 adf0ad3c1..ab54013d4 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,32 +1,47 @@ +import { NgOptimizedImage } from '@angular/common'; import { Component, 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 { CardItemDirective } from '../../ui/card/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: ` + [list]="store.teachers()" + customClass="bg-red-100" + (add)="store.addOne(randTeacher())"> + + + + + `, - styles: [ - ` - ::ng-deep .bg-light-red { - background-color: rgba(250, 0, 0, 0.1); - } - `, + imports: [ + CardComponent, + CardItemDirective, + ListItemComponent, + NgOptimizedImage, ], - imports: [CardComponent], }) export class TeacherCardComponent implements OnInit { private http = inject(FakeHttpService); - private store = inject(TeacherStore); + protected store = inject(TeacherStore); - teachers = this.store.teachers; - cardType = CardType.TEACHER; + randTeacher = randTeacher; ngOnInit(): void { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); 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); diff --git a/apps/angular/1-projection/src/app/ui/card/card-item.directive.ts b/apps/angular/1-projection/src/app/ui/card/card-item.directive.ts new file mode 100644 index 000000000..940b9104e --- /dev/null +++ b/apps/angular/1-projection/src/app/ui/card/card-item.directive.ts @@ -0,0 +1,19 @@ +import { Directive, inject, TemplateRef } from '@angular/core'; + +export interface CardItemContext { + $implicit: T; +} + +@Directive({ + selector: '[appCardItem]', +}) +export class CardItemDirective { + templateRef = inject>>(TemplateRef); + + static ngTemplateContextGuard( + _dir: CardItemDirective, + ctx: unknown, + ): ctx is CardItemContext { + return true; + } +} 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 166d9c49a..59ab5d0c3 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,10 +1,12 @@ -import { NgOptimizedImage } from '@angular/common'; -import { Component, inject, input } 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 { NgTemplateOutlet } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + contentChild, + input, + output, +} from '@angular/core'; +import { CardItemDirective } from './card-item.directive'; @Component({ selector: 'app-card', @@ -12,47 +14,31 @@ import { ListItemComponent } from '../list-item/list-item.component';
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - - } +
- @for (item of list(); track item) { - + @for (item of list(); track $index) { + }
`, - imports: [ListItemComponent, NgOptimizedImage], + imports: [NgTemplateOutlet], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CardComponent { - private teacherStore = inject(TeacherStore); - private studentStore = inject(StudentStore); - - readonly list = input(null); - readonly type = input.required(); +export class CardComponent { + readonly list = input([]); readonly customClass = input(''); - CardType = CardType; + protected itemTemplate = contentChild.required(CardItemDirective); - addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } - } + 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..b2ad9d3cc 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,8 @@ 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(); }