feat(challenge1): modernization

This commit is contained in:
thomas
2025-01-27 21:39:33 +01:00
parent 9c7a37013e
commit e58a789133
10 changed files with 83 additions and 95 deletions

View File

@@ -1,12 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({ @Component({
selector: 'app-city-card', selector: 'app-city-card',
template: 'TODO City', template: 'TODO City',
imports: [], imports: [],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CityCardComponent implements OnInit { export class CityCardComponent {}
constructor() {}
ngOnInit(): void {}
}

View File

@@ -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 { FakeHttpService } from '../../data-access/fake-http.service';
import { StudentStore } from '../../data-access/student.store'; import { StudentStore } from '../../data-access/student.store';
import { CardType } from '../../model/card.model'; import { CardType } from '../../model/card.model';
import { Student } from '../../model/student.model';
import { CardComponent } from '../../ui/card/card.component'; import { CardComponent } from '../../ui/card/card.component';
@Component({ @Component({
selector: 'app-student-card', selector: 'app-student-card',
template: ` template: `
<app-card <app-card
[list]="students" [list]="students()"
[type]="cardType" [type]="cardType"
customClass="bg-light-green"></app-card> customClass="bg-light-green" />
`, `,
styles: [ styles: [
` `
@@ -21,19 +25,16 @@ import { CardComponent } from '../../ui/card/card.component';
`, `,
], ],
imports: [CardComponent], imports: [CardComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class StudentCardComponent implements OnInit { export class StudentCardComponent implements OnInit {
students: Student[] = []; private http = inject(FakeHttpService);
cardType = CardType.STUDENT; private store = inject(StudentStore);
constructor( students = this.store.students;
private http: FakeHttpService, cardType = CardType.STUDENT;
private store: StudentStore,
) {}
ngOnInit(): void { ngOnInit(): void {
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
this.store.students$.subscribe((s) => (this.students = s));
} }
} }

View File

@@ -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 { FakeHttpService } from '../../data-access/fake-http.service';
import { TeacherStore } from '../../data-access/teacher.store'; import { TeacherStore } from '../../data-access/teacher.store';
import { CardType } from '../../model/card.model'; import { CardType } from '../../model/card.model';
import { Teacher } from '../../model/teacher.model';
import { CardComponent } from '../../ui/card/card.component'; import { CardComponent } from '../../ui/card/card.component';
@Component({ @Component({
selector: 'app-teacher-card', selector: 'app-teacher-card',
template: ` template: `
<app-card <app-card
[list]="teachers" [list]="teachers()"
[type]="cardType" [type]="cardType"
customClass="bg-light-red"></app-card> customClass="bg-light-red"></app-card>
`, `,
@@ -23,17 +22,13 @@ import { CardComponent } from '../../ui/card/card.component';
imports: [CardComponent], imports: [CardComponent],
}) })
export class TeacherCardComponent implements OnInit { export class TeacherCardComponent implements OnInit {
teachers: Teacher[] = []; private http = inject(FakeHttpService);
cardType = CardType.TEACHER; private store = inject(TeacherStore);
constructor( teachers = this.store.teachers;
private http: FakeHttpService, cardType = CardType.TEACHER;
private store: TeacherStore,
) {}
ngOnInit(): void { ngOnInit(): void {
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
this.store.teachers$.subscribe((t) => (this.teachers = t));
} }
} }

View File

@@ -1,23 +1,21 @@
import { Injectable } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { City } from '../model/city.model'; import { City } from '../model/city.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class CityStore { export class CityStore {
private cities = new BehaviorSubject<City[]>([]); private cities = signal<City[]>([]);
cities$ = this.cities.asObservable();
addAll(cities: City[]) { addAll(cities: City[]) {
this.cities.next(cities); this.cities.set(cities);
} }
addOne(student: City) { addOne(student: City) {
this.cities.next([...this.cities.value, student]); this.cities.set([...this.cities(), student]);
} }
deleteOne(id: number) { deleteOne(id: number) {
this.cities.next(this.cities.value.filter((s) => s.id !== id)); this.cities.set(this.cities().filter((s) => s.id !== id));
} }
} }

View File

@@ -1,23 +1,21 @@
import { Injectable } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Student } from '../model/student.model'; import { Student } from '../model/student.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class StudentStore { export class StudentStore {
private students = new BehaviorSubject<Student[]>([]); public students = signal<Student[]>([]);
students$ = this.students.asObservable();
addAll(students: Student[]) { addAll(students: Student[]) {
this.students.next(students); this.students.set(students);
} }
addOne(student: Student) { addOne(student: Student) {
this.students.next([...this.students.value, student]); this.students.set([...this.students(), student]);
} }
deleteOne(id: number) { deleteOne(id: number) {
this.students.next(this.students.value.filter((s) => s.id !== id)); this.students.set(this.students().filter((s) => s.id !== id));
} }
} }

View File

@@ -1,23 +1,21 @@
import { Injectable } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Teacher } from '../model/teacher.model'; import { Teacher } from '../model/teacher.model';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TeacherStore { export class TeacherStore {
private teachers = new BehaviorSubject<Teacher[]>([]); public teachers = signal<Teacher[]>([]);
teachers$ = this.teachers.asObservable();
addAll(teachers: Teacher[]) { addAll(teachers: Teacher[]) {
this.teachers.next(teachers); this.teachers.set(teachers);
} }
addOne(teacher: Teacher) { addOne(teacher: Teacher) {
this.teachers.next([...this.teachers.value, teacher]); this.teachers.set([...this.teachers(), teacher]);
} }
deleteOne(id: number) { deleteOne(id: number) {
this.teachers.next(this.teachers.value.filter((t) => t.id !== id)); this.teachers.set(this.teachers().filter((t) => t.id !== id));
} }
} }

View File

@@ -1,5 +1,5 @@
import { NgFor, NgIf } from '@angular/common'; import { NgOptimizedImage } from '@angular/common';
import { Component, Input } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { randStudent, randTeacher } from '../../data-access/fake-http.service'; import { randStudent, randTeacher } from '../../data-access/fake-http.service';
import { StudentStore } from '../../data-access/student.store'; import { StudentStore } from '../../data-access/student.store';
import { TeacherStore } from '../../data-access/teacher.store'; import { TeacherStore } from '../../data-access/teacher.store';
@@ -11,22 +11,21 @@ import { ListItemComponent } from '../list-item/list-item.component';
template: ` template: `
<div <div
class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4" class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"
[class]="customClass"> [class]="customClass()">
<img @if (type() === CardType.TEACHER) {
*ngIf="type === CardType.TEACHER" <img ngSrc="assets/img/teacher.png" width="200" height="200" />
src="assets/img/teacher.png" }
width="200px" /> @if (type() === CardType.STUDENT) {
<img <img ngSrc="assets/img/student.webp" width="200" height="200" />
*ngIf="type === CardType.STUDENT" }
src="assets/img/student.webp"
width="200px" />
<section> <section>
<app-list-item @for (item of list(); track item) {
*ngFor="let item of list" <app-list-item
[name]="item.firstName" [name]="item.firstName"
[id]="item.id" [id]="item.id"
[type]="type"></app-list-item> [type]="type()"></app-list-item>
}
</section> </section>
<button <button
@@ -36,24 +35,23 @@ import { ListItemComponent } from '../list-item/list-item.component';
</button> </button>
</div> </div>
`, `,
imports: [NgIf, NgFor, ListItemComponent], imports: [ListItemComponent, NgOptimizedImage],
}) })
export class CardComponent { export class CardComponent {
@Input() list: any[] | null = null; private teacherStore = inject(TeacherStore);
@Input() type!: CardType; private studentStore = inject(StudentStore);
@Input() customClass = '';
readonly list = input<any[] | null>(null);
readonly type = input.required<CardType>();
readonly customClass = input('');
CardType = CardType; CardType = CardType;
constructor(
private teacherStore: TeacherStore,
private studentStore: StudentStore,
) {}
addNewItem() { addNewItem() {
if (this.type === CardType.TEACHER) { const type = this.type();
if (type === CardType.TEACHER) {
this.teacherStore.addOne(randTeacher()); this.teacherStore.addOne(randTeacher());
} else if (this.type === CardType.STUDENT) { } else if (type === CardType.STUDENT) {
this.studentStore.addOne(randStudent()); this.studentStore.addOne(randStudent());
} }
} }

View File

@@ -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 { StudentStore } from '../../data-access/student.store';
import { TeacherStore } from '../../data-access/teacher.store'; import { TeacherStore } from '../../data-access/teacher.store';
import { CardType } from '../../model/card.model'; import { CardType } from '../../model/card.model';
@@ -7,28 +12,28 @@ import { CardType } from '../../model/card.model';
selector: 'app-list-item', selector: 'app-list-item',
template: ` template: `
<div class="border-grey-300 flex justify-between border px-2 py-1"> <div class="border-grey-300 flex justify-between border px-2 py-1">
{{ name }} {{ name() }}
<button (click)="delete(id)"> <button (click)="delete(id())">
<img class="h-5" src="assets/svg/trash.svg" /> <img class="h-5" src="assets/svg/trash.svg" />
</button> </button>
</div> </div>
`, `,
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ListItemComponent { export class ListItemComponent {
@Input() id!: number; private teacherStore = inject(TeacherStore);
@Input() name!: string; private studentStore = inject(StudentStore);
@Input() type!: CardType;
constructor( readonly id = input.required<number>();
private teacherStore: TeacherStore, readonly name = input.required<string>();
private studentStore: StudentStore, readonly type = input.required<CardType>();
) {}
delete(id: number) { delete(id: number) {
if (this.type === CardType.TEACHER) { const type = this.type();
if (type === CardType.TEACHER) {
this.teacherStore.deleteOne(id); this.teacherStore.deleteOne(id);
} else if (this.type === CardType.STUDENT) { } else if (type === CardType.STUDENT) {
this.studentStore.deleteOne(id); this.studentStore.deleteOne(id);
} }
} }

View File

@@ -41,12 +41,11 @@ While the application works, the developer experience is far from being optimal.
## Constraints ## Constraints
- You <b>must</b> refactor the `CardComponent` and `ListItemComponent`. - You <b>must</b> 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`. - 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 `NgIf` or `NgSwitch`. - `CardComponent` should not contain any conditions.
- CSS: try to avoid using `::ng-deep`. Find a better way to handle CSS styling. - CSS: try to avoid using `::ng-deep`. Find a better way to handle CSS styling.
## Bonus Challenges ## 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)) - 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)) - To reference the template, use a directive instead of magic strings ([What is wrong with magic strings?](https://softwareengineering.stackexchange.com/a/365344))

View File

@@ -37,12 +37,11 @@ Bien que l'application fonctionne, l'expérience développeur est loin d'être o
## Contraintes ## Contraintes
- Vous <b>devez</b> refactoriser le `CardComponent` et le `ListItemComponent`. - Vous <b>devez</b> 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`. - 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 aucun `NgIf` ni `NgSwitch`. - 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. - CSS: essayez d'éviter d'utiliser `::ng-deep`. Trouvez un meilleur moyen de gérer le style CSS.
## Challenges Bonus ## 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)) - 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)) - 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))