From e58a789133a8af25fbe70ed2915e338fe9703415 Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 27 Jan 2025 21:39:33 +0100 Subject: [PATCH] feat(challenge1): modernization --- .../city-card/city-card.component.ts | 9 ++-- .../student-card/student-card.component.ts | 25 ++++----- .../teacher-card/teacher-card.component.ts | 17 +++--- .../src/app/data-access/city.store.ts | 12 ++--- .../src/app/data-access/student.store.ts | 12 ++--- .../src/app/data-access/teacher.store.ts | 12 ++--- .../src/app/ui/card/card.component.ts | 52 +++++++++---------- .../app/ui/list-item/list-item.component.ts | 29 ++++++----- .../docs/challenges/angular/1-projection.md | 5 +- .../fr/challenges/angular/1-projection.md | 5 +- 10 files changed, 83 insertions(+), 95 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 47b0896..8895c8c 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,12 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; @Component({ selector: 'app-city-card', template: 'TODO City', imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent implements OnInit { - constructor() {} - - ngOnInit(): void {} -} +export class CityCardComponent {} 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 dae48a2..bdfa4ab 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,17 +1,21 @@ -import { Component, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, +} from '@angular/core'; import { FakeHttpService } from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; import { CardType } from '../../model/card.model'; -import { Student } from '../../model/student.model'; import { CardComponent } from '../../ui/card/card.component'; @Component({ selector: 'app-student-card', template: ` + customClass="bg-light-green" /> `, styles: [ ` @@ -21,19 +25,16 @@ import { CardComponent } from '../../ui/card/card.component'; `, ], imports: [CardComponent], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent implements OnInit { - students: Student[] = []; - cardType = CardType.STUDENT; + private http = inject(FakeHttpService); + private store = inject(StudentStore); - constructor( - private http: FakeHttpService, - private store: StudentStore, - ) {} + students = this.store.students; + cardType = CardType.STUDENT; ngOnInit(): void { this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); - - this.store.students$.subscribe((s) => (this.students = 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 815cde9..adf0ad3 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,15 +1,14 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { FakeHttpService } from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; -import { Teacher } from '../../model/teacher.model'; import { CardComponent } from '../../ui/card/card.component'; @Component({ selector: 'app-teacher-card', template: ` `, @@ -23,17 +22,13 @@ import { CardComponent } from '../../ui/card/card.component'; imports: [CardComponent], }) export class TeacherCardComponent implements OnInit { - teachers: Teacher[] = []; - cardType = CardType.TEACHER; + private http = inject(FakeHttpService); + private store = inject(TeacherStore); - constructor( - private http: FakeHttpService, - private store: TeacherStore, - ) {} + teachers = this.store.teachers; + cardType = CardType.TEACHER; ngOnInit(): void { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); - - this.store.teachers$.subscribe((t) => (this.teachers = 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 711dad1..8a08086 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,23 +1,21 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { Injectable, signal } from '@angular/core'; import { City } from '../model/city.model'; @Injectable({ providedIn: 'root', }) export class CityStore { - private cities = new BehaviorSubject([]); - cities$ = this.cities.asObservable(); + private cities = signal([]); addAll(cities: City[]) { - this.cities.next(cities); + this.cities.set(cities); } addOne(student: City) { - this.cities.next([...this.cities.value, student]); + this.cities.set([...this.cities(), student]); } deleteOne(id: number) { - this.cities.next(this.cities.value.filter((s) => s.id !== id)); + this.cities.set(this.cities().filter((s) => s.id !== 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 7918118..6e7f570 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,23 +1,21 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { Injectable, signal } from '@angular/core'; import { Student } from '../model/student.model'; @Injectable({ providedIn: 'root', }) export class StudentStore { - private students = new BehaviorSubject([]); - students$ = this.students.asObservable(); + public students = signal([]); addAll(students: Student[]) { - this.students.next(students); + this.students.set(students); } addOne(student: Student) { - this.students.next([...this.students.value, student]); + this.students.set([...this.students(), student]); } deleteOne(id: number) { - this.students.next(this.students.value.filter((s) => s.id !== id)); + this.students.set(this.students().filter((s) => s.id !== 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 93f68c4..5f6dae9 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,23 +1,21 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { Injectable, signal } from '@angular/core'; import { Teacher } from '../model/teacher.model'; @Injectable({ providedIn: 'root', }) export class TeacherStore { - private teachers = new BehaviorSubject([]); - teachers$ = this.teachers.asObservable(); + public teachers = signal([]); addAll(teachers: Teacher[]) { - this.teachers.next(teachers); + this.teachers.set(teachers); } addOne(teacher: Teacher) { - this.teachers.next([...this.teachers.value, teacher]); + this.teachers.set([...this.teachers(), teacher]); } deleteOne(id: number) { - this.teachers.next(this.teachers.value.filter((t) => t.id !== id)); + this.teachers.set(this.teachers().filter((t) => t.id !== 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 ca3c661..1a6c364 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,5 +1,5 @@ -import { NgFor, NgIf } from '@angular/common'; -import { Component, Input } from '@angular/core'; +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'; @@ -11,22 +11,21 @@ import { ListItemComponent } from '../list-item/list-item.component'; template: `
- - + [class]="customClass()"> + @if (type() === CardType.TEACHER) { + + } + @if (type() === CardType.STUDENT) { + + }
- + @for (item of list(); track item) { + + }
`, - imports: [NgIf, NgFor, ListItemComponent], + imports: [ListItemComponent, NgOptimizedImage], }) export class CardComponent { - @Input() list: any[] | null = null; - @Input() type!: CardType; - @Input() customClass = ''; + private teacherStore = inject(TeacherStore); + private studentStore = inject(StudentStore); + + readonly list = input(null); + readonly type = input.required(); + readonly customClass = input(''); CardType = CardType; - constructor( - private teacherStore: TeacherStore, - private studentStore: StudentStore, - ) {} - addNewItem() { - if (this.type === CardType.TEACHER) { + const type = this.type(); + if (type === CardType.TEACHER) { this.teacherStore.addOne(randTeacher()); - } else if (this.type === CardType.STUDENT) { + } else if (type === CardType.STUDENT) { this.studentStore.addOne(randStudent()); } } 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 c0f9cff..cffabb4 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,4 +1,9 @@ -import { Component, Input } from '@angular/core'; +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'; @@ -7,28 +12,28 @@ import { CardType } from '../../model/card.model'; selector: 'app-list-item', template: `
- {{ name }} -
`, standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ListItemComponent { - @Input() id!: number; - @Input() name!: string; - @Input() type!: CardType; + private teacherStore = inject(TeacherStore); + private studentStore = inject(StudentStore); - constructor( - private teacherStore: TeacherStore, - private studentStore: StudentStore, - ) {} + readonly id = input.required(); + readonly name = input.required(); + readonly type = input.required(); delete(id: number) { - if (this.type === CardType.TEACHER) { + const type = this.type(); + if (type === CardType.TEACHER) { this.teacherStore.deleteOne(id); - } else if (this.type === CardType.STUDENT) { + } else if (type === CardType.STUDENT) { this.studentStore.deleteOne(id); } } diff --git a/docs/src/content/docs/challenges/angular/1-projection.md b/docs/src/content/docs/challenges/angular/1-projection.md index c260b6b..127530f 100644 --- a/docs/src/content/docs/challenges/angular/1-projection.md +++ b/docs/src/content/docs/challenges/angular/1-projection.md @@ -41,12 +41,11 @@ While the application works, the developer experience is far from being optimal. ## Constraints - You must refactor the `CardComponent` and `ListItemComponent`. -- The `NgFor` directive must be declared and remain inside the `CardComponent`. You might be tempted to move it to the `ParentCardComponent` like `TeacherCardComponent`. -- `CardComponent` should not contain any `NgIf` or `NgSwitch`. +- The `@for` must be declared and remain inside the `CardComponent`. You might be tempted to move it to the `ParentCardComponent` like `TeacherCardComponent`. +- `CardComponent` should not contain any conditions. - CSS: try to avoid using `::ng-deep`. Find a better way to handle CSS styling. ## Bonus Challenges -- Try to work with the new built-in control flow syntax for loops and conditionals (documentation [here](https://angular.dev/guide/templates/control-flow)) - Use the signal API to manage your components state (documentation [here](https://angular.dev/guide/signals)) - To reference the template, use a directive instead of magic strings ([What is wrong with magic strings?](https://softwareengineering.stackexchange.com/a/365344)) diff --git a/docs/src/content/docs/fr/challenges/angular/1-projection.md b/docs/src/content/docs/fr/challenges/angular/1-projection.md index 67d431f..509752c 100644 --- a/docs/src/content/docs/fr/challenges/angular/1-projection.md +++ b/docs/src/content/docs/fr/challenges/angular/1-projection.md @@ -37,12 +37,11 @@ Bien que l'application fonctionne, l'expérience développeur est loin d'être o ## Contraintes - Vous devez refactoriser le `CardComponent` et le `ListItemComponent`. -- La directive `NgFor` doit être déclarée et rester à l'intérieur du `CardComponent`. Vous pourriez être tenté de la déplacer dans le `ParentCardComponent` comme `TeacherCardComponent`. -- Le composant `CardComponent` ne doit contenir aucun `NgIf` ni `NgSwitch`. +- La boucle `@for` doit être déclarée et rester à l'intérieur du `CardComponent`. Vous pourriez être tenté de la déplacer dans le `ParentCardComponent` comme `TeacherCardComponent`. +- Le composant `CardComponent` ne doit contenir aucune condition. - CSS: essayez d'éviter d'utiliser `::ng-deep`. Trouvez un meilleur moyen de gérer le style CSS. ## Challenges Bonus -- Essayez de travailler avec la nouvelle syntaxe de contrôle de flux pour les boucles et les conditions (documentation [ici](https://angular.dev/guide/templates/control-flow)) - Utilisez l'API des signals pour gérer l'état de vos composants (documentation [ici](https://angular.dev/guide/signals)) - Pour référencer le template, utilisez une directive au lieu d'une magic string ([Qu'est-ce qui pose problème avec les magic string ?](https://softwareengineering.stackexchange.com/a/365344))