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..c96dd7d7d 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,54 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; +import { + FakeHttpService, + randomCity, +} from '../../data-access/fake-http.service'; +import { CardRowDirective } from '../../ui/card/card-row.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: ` + + city + + + {{ city.name }} + + + + `, + styles: [ + ` + .bg-light-blue { + background-color: rgba(0, 0, 250, 0.1); + } + `, + ], + imports: [CardComponent, ListItemComponent, CardRowDirective], + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent {} +export class CityCardComponent { + private http = inject(FakeHttpService); + private store = inject(CityStore); + + cities = this.store.cities; + + constructor() { + this.http.fetchCities$.subscribe(this.store.addAll); + } + + handleAddNew(): void { + this.store.addOne(randomCity()); + } + + handleDelete(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..b208ac32d 100644 --- a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts +++ b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts @@ -1,40 +1,55 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { - ChangeDetectionStrategy, - Component, - inject, - OnInit, -} from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; + FakeHttpService, + randStudent, +} from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; -import { CardType } from '../../model/card.model'; +import { CardRowDirective } from '../../ui/card/card-row.directive'; import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-student-card', template: ` + class="bg-light-green" + [items]="students()" + (addNew)="handleAddNew()"> + student + + + + {{ student.firstName }} + + + `, styles: [ ` - ::ng-deep .bg-light-green { + .bg-light-green { background-color: rgba(0, 250, 0, 0.1); } `, ], - imports: [CardComponent], + imports: [CardComponent, CardRowDirective, ListItemComponent], + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class StudentCardComponent implements OnInit { +export class StudentCardComponent { private http = inject(FakeHttpService); private store = inject(StudentStore); students = this.store.students; - cardType = CardType.STUDENT; - ngOnInit(): void { - this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); + constructor() { + this.http.fetchStudents$.subscribe(this.store.addAll); + } + + handleAddNew(): void { + this.store.addOne(randStudent()); + } + + handleDelete(id: number): void { + this.store.deleteOne(id); } } diff --git a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts index 3e454ac19..3fac9106a 100644 --- a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts +++ b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts @@ -1,40 +1,55 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { - ChangeDetectionStrategy, - Component, - inject, - OnInit, -} from '@angular/core'; -import { FakeHttpService } from '../../data-access/fake-http.service'; + FakeHttpService, + randTeacher, +} from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; -import { CardType } from '../../model/card.model'; +import { CardRowDirective } from '../../ui/card/card-row.directive'; import { CardComponent } from '../../ui/card/card.component'; +import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-teacher-card', template: ` + class="bg-light-red" + [items]="teachers()" + (addNew)="handleAddNew()"> + student + + + + {{ teacher.firstName }} + + + `, styles: [ ` - ::ng-deep .bg-light-red { + .bg-light-red { background-color: rgba(250, 0, 0, 0.1); } `, ], - changeDetection: ChangeDetectionStrategy.Eager, - imports: [CardComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CardComponent, CardRowDirective, ListItemComponent], }) -export class TeacherCardComponent implements OnInit { +export class TeacherCardComponent { 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)); + constructor() { + this.http.fetchTeachers$.subscribe(this.store.addAll); + } + + handleAddNew(): void { + this.store.addOne(randTeacher()); + } + + handleDelete(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); diff --git a/apps/angular/1-projection/src/app/model/card.model.ts b/apps/angular/1-projection/src/app/model/card.model.ts index 740cd2ae4..e69de29bb 100644 --- a/apps/angular/1-projection/src/app/model/card.model.ts +++ b/apps/angular/1-projection/src/app/model/card.model.ts @@ -1,5 +0,0 @@ -export enum CardType { - TEACHER, - STUDENT, - CITY, -} diff --git a/apps/angular/1-projection/src/app/ui/card/card-row.directive.ts b/apps/angular/1-projection/src/app/ui/card/card-row.directive.ts new file mode 100644 index 000000000..9832e17eb --- /dev/null +++ b/apps/angular/1-projection/src/app/ui/card/card-row.directive.ts @@ -0,0 +1,18 @@ +import { Directive, input } from '@angular/core'; + +interface CardRowContext { + $implicit: T; +} + +// eslint-disable-next-line @angular-eslint/directive-selector +@Directive({ selector: 'ng-template[cardRow]', standalone: true }) +export class CardRowDirective { + cardRow = input.required(); + + static ngTemplateContextGuard( + dir: CardRowDirective, + ctx: unknown, + ): ctx is CardRowContext { + 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 d395474e2..20f82bde7 100644 --- a/apps/angular/1-projection/src/app/ui/card/card.component.ts +++ b/apps/angular/1-projection/src/app/ui/card/card.component.ts @@ -1,64 +1,44 @@ -import { NgOptimizedImage } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, - inject, + contentChild, input, + output, + TemplateRef, } 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 { CardRowDirective } from './card-row.directive'; @Component({ selector: 'app-card', template: ` -
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - - } + -
- @for (item of list(); track item) { - - } -
+
+ @for (item of items(); track item.id) { + + } +
- -
+ `, - changeDetection: ChangeDetectionStrategy.Eager, - imports: [ListItemComponent, NgOptimizedImage], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + host: { + class: 'flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4', + }, + imports: [NgTemplateOutlet], }) -export class CardComponent { - private teacherStore = inject(TeacherStore); - private studentStore = inject(StudentStore); - - readonly list = input(null); - readonly type = input.required(); - readonly customClass = input(''); +export class CardComponent { + items = input.required(); - CardType = CardType; + addNew = output(); - addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } - } + rowTemplate = contentChild.required(CardRowDirective, { read: TemplateRef }); } 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..ad2a2b7e5 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,39 +1,18 @@ -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() }} -
`, + standalone: true, 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 delete = output(); } diff --git a/apps/performance/12-optimize-change-detection/src/app/app.component.ts b/apps/performance/12-optimize-change-detection/src/app/app.component.ts index d22dcab39..1fe9ad2a9 100644 --- a/apps/performance/12-optimize-change-detection/src/app/app.component.ts +++ b/apps/performance/12-optimize-change-detection/src/app/app.component.ts @@ -1,9 +1,12 @@ import { ChangeDetectionStrategy, Component, - HostListener, + inject, + NgZone, signal, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { fromEvent } from 'rxjs'; @Component({ selector: 'app-root', @@ -15,7 +18,8 @@ import { } `, - changeDetection: ChangeDetectionStrategy.Eager, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, styles: [ ` :host { @@ -36,14 +40,19 @@ import { ], }) export class AppComponent { - title = 'scroll-cd'; + private ngZone = inject(NgZone); public displayButton = signal(false); - @HostListener('window:scroll') - onScroll() { - const pos = window.scrollY; - this.displayButton.set(pos > 50); + constructor() { + this.ngZone.runOutsideAngular(() => { + fromEvent(window, 'scroll') + .pipe(takeUntilDestroyed()) + .subscribe(() => { + const pos = window.scrollY; + this.displayButton.set(pos > 50); + }); + }); } goToTop() {