From 9cc7e002624e5dedbf4e2db67bc93f9b9fac25fa Mon Sep 17 00:00:00 2001 From: tokimanana Date: Wed, 10 Dec 2025 23:08:03 +0300 Subject: [PATCH 1/3] feat(): implemented structural directives --- .../src/app/ui/card/card-actions.directive.ts | 9 +++++++++ .../1-projection/src/app/ui/card/card-image.directive.ts | 9 +++++++++ .../src/app/ui/card/card-list-item.directive.ts | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 apps/angular/1-projection/src/app/ui/card/card-actions.directive.ts create mode 100644 apps/angular/1-projection/src/app/ui/card/card-image.directive.ts create mode 100644 apps/angular/1-projection/src/app/ui/card/card-list-item.directive.ts diff --git a/apps/angular/1-projection/src/app/ui/card/card-actions.directive.ts b/apps/angular/1-projection/src/app/ui/card/card-actions.directive.ts new file mode 100644 index 000000000..38a68856e --- /dev/null +++ b/apps/angular/1-projection/src/app/ui/card/card-actions.directive.ts @@ -0,0 +1,9 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'ng-template[cardActions]', + standalone: true, +}) +export class CardActionsDirective { + constructor(public templateRef: TemplateRef) {} +} diff --git a/apps/angular/1-projection/src/app/ui/card/card-image.directive.ts b/apps/angular/1-projection/src/app/ui/card/card-image.directive.ts new file mode 100644 index 000000000..417917fbe --- /dev/null +++ b/apps/angular/1-projection/src/app/ui/card/card-image.directive.ts @@ -0,0 +1,9 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'ng-template[cardImage]', + standalone: true, +}) +export class CardImageDirective { + constructor(public templateRef: TemplateRef) {} +} diff --git a/apps/angular/1-projection/src/app/ui/card/card-list-item.directive.ts b/apps/angular/1-projection/src/app/ui/card/card-list-item.directive.ts new file mode 100644 index 000000000..0f2002e0f --- /dev/null +++ b/apps/angular/1-projection/src/app/ui/card/card-list-item.directive.ts @@ -0,0 +1,9 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + selector: 'ng-template[cardListItem]', + standalone: true, +}) +export class CardListItemDirective { + constructor(public templateRef: TemplateRef) {} +} From b86f257506758f23686648aba1e633fc18b4a22c Mon Sep 17 00:00:00 2001 From: tokimanana Date: Fri, 12 Dec 2025 23:38:37 +0300 Subject: [PATCH 2/3] feat(): smart and dumb components implemented --- .../city-card/city-card.component.ts | 74 ++++++++++++++++++- .../student-card/student-card.component.ts | 56 ++++++++++++-- .../teacher-card/teacher-card.component.ts | 60 ++++++++++++--- .../src/app/data-access/city.store.ts | 2 +- .../src/app/ui/card/card.component.ts | 61 ++++++--------- .../app/ui/list-item/list-item.component.ts | 30 +------- 6 files changed, 197 insertions(+), 86 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..268a0a198 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,75 @@ -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 { CardActionsDirective } from '../../ui/card/card-actions.directive'; +import { CardImageDirective } from '../../ui/card/card-image.directive'; +import { CardListItemDirective } from '../../ui/card/card-list-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: [], + standalone: true, + imports: [ + NgOptimizedImage, + CardComponent, + ListItemComponent, + CardImageDirective, + CardListItemDirective, + CardActionsDirective, + ], + template: ` + + + City + + + + + {{ city.name }} + + + + + + + + `, + styles: ` + ::ng-deep .bg-light-blue { + background-color: rgba(0, 0, 250, 0.1); + } + `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent {} +export class CityCardComponent implements OnInit { + private readonly http = inject(FakeHttpService); + private readonly store = inject(CityStore); + + cities = this.store.cities; + + ngOnInit(): void { + this.http.fetchCities$.subscribe((c) => this.store.addAll(c)); + } + + 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..1aa1edc3e 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,21 +1,47 @@ +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 { CardActionsDirective } from '../../ui/card/card-actions.directive'; +import { CardImageDirective } from '../../ui/card/card-image.directive'; +import { CardListItemDirective } from '../../ui/card/card-list-item.directive'; import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-student-card', template: ` - + + + Student + + + + + {{ student.firstName }} + + + + + + + `, styles: [ ` @@ -24,7 +50,14 @@ import { CardComponent } from '../../ui/card/card.component'; } `, ], - imports: [CardComponent], + imports: [ + CardComponent, + ListItemComponent, + NgOptimizedImage, + CardImageDirective, + CardListItemDirective, + CardActionsDirective, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent implements OnInit { @@ -32,9 +65,16 @@ 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)); } + + addStudent() { + this.store.addOne(randStudent()); + } + + deleteStudent(id: number) { + 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 adf0ad3c1..7adec8396 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,16 +1,42 @@ +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 { CardActionsDirective } from '../../ui/card/card-actions.directive'; +import { CardImageDirective } from '../../ui/card/card-image.directive'; +import { CardListItemDirective } from '../../ui/card/card-list-item.directive'; import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-teacher-card', template: ` - + + + Teacher + + + + + {{ teacher.firstName }} + + + + + + + `, styles: [ ` @@ -19,16 +45,30 @@ import { CardComponent } from '../../ui/card/card.component'; } `, ], - imports: [CardComponent], + imports: [ + NgOptimizedImage, + CardComponent, + ListItemComponent, + CardImageDirective, + CardListItemDirective, + CardActionsDirective, + ], }) export class TeacherCardComponent implements OnInit { - private http = inject(FakeHttpService); - private store = inject(TeacherStore); + private readonly http = inject(FakeHttpService); + private readonly store = inject(TeacherStore); teachers = this.store.teachers; - cardType = CardType.TEACHER; ngOnInit(): void { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); } + + addTeacher() { + this.store.addOne(randTeacher()); + } + + deleteTeacher(id: number) { + 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); 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 1a6c3648c..75dbfa53e 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,8 @@ -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 { Component, ContentChild, input } from '@angular/core'; +import { CardActionsDirective } from './card-actions.directive'; +import { CardImageDirective } from './card-image.directive'; +import { CardListItemDirective } from './card-list-item.directive'; @Component({ selector: 'app-card', @@ -12,47 +10,36 @@ import { ListItemComponent } from '../list-item/list-item.component';
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - + @if (imageTemplate) { + }
- @for (item of list(); track item) { - + @for (item of list(); track trackByFn(item)) { + }
- + @if (actionsTemplate) { + + }
`, - imports: [ListItemComponent, NgOptimizedImage], + imports: [NgTemplateOutlet], }) -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(null); readonly customClass = input(''); - CardType = CardType; + @ContentChild(CardImageDirective) imageTemplate?: CardImageDirective; + @ContentChild(CardListItemDirective) listItemTemplate?: CardListItemDirective; + @ContentChild(CardActionsDirective) actionsTemplate?: CardActionsDirective; - addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } + protected trackByFn(item: T): number | string { + return item.id; } } 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 5d504f372..243e74314 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,11 @@ -import { - ChangeDetectionStrategy, - Component, - inject, - input, -} from '@angular/core'; -import { StudentStore } from '../../data-access/student.store'; -import { TeacherStore } from '../../data-access/teacher.store'; -import { CardType } from '../../model/card.model'; +import { ChangeDetectionStrategy, Component, output } from '@angular/core'; @Component({ selector: 'app-list-item', template: `
- {{ name() }} -
@@ -21,19 +13,5 @@ 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); - } - } + readonly onDelete = output(); } From b4ff1f120e6b543d3cfcfc245daa43ec8997726b Mon Sep 17 00:00:00 2001 From: tokimanana Date: Fri, 12 Dec 2025 23:55:16 +0300 Subject: [PATCH 3/3] feat(): removed ng-deep and host styles --- .../city-card/city-card.component.ts | 4 +- .../student-card/student-card.component.ts | 4 +- .../teacher-card/teacher-card.component.ts | 4 +- .../src/app/ui/card/card.component.ts | 38 +++++++++---------- 4 files changed, 24 insertions(+), 26 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 268a0a198..5b9ad694f 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 @@ -28,7 +28,7 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; CardActionsDirective, ], template: ` - + City @@ -49,7 +49,7 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; `, styles: ` - ::ng-deep .bg-light-blue { + .bg-light-blue { background-color: rgba(0, 0, 250, 0.1); } `, 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 1aa1edc3e..252802ce0 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 @@ -19,7 +19,7 @@ import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-student-card', template: ` - + + - @if (imageTemplate) { - - } - -
- @for (item of list(); track trackByFn(item)) { - - } -
+ @if (imageTemplate) { + + } - @if (actionsTemplate) { - +
+ @for (item of list(); track trackByFn(item)) { + } - +
+ + @if (actionsTemplate) { + + } `, + host: { + class: 'flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4', + }, imports: [NgTemplateOutlet], }) export class CardComponent { readonly list = input(null); - readonly customClass = input(''); @ContentChild(CardImageDirective) imageTemplate?: CardImageDirective; @ContentChild(CardListItemDirective) listItemTemplate?: CardListItemDirective;