Skip to content

Commit e58a789

Browse files
committed
feat(challenge1): modernization
1 parent 9c7a370 commit e58a789

10 files changed

Lines changed: 83 additions & 95 deletions

File tree

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
22

33
@Component({
44
selector: 'app-city-card',
55
template: 'TODO City',
66
imports: [],
7+
changeDetection: ChangeDetectionStrategy.OnPush,
78
})
8-
export class CityCardComponent implements OnInit {
9-
constructor() {}
10-
11-
ngOnInit(): void {}
12-
}
9+
export class CityCardComponent {}
Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
inject,
5+
OnInit,
6+
} from '@angular/core';
27
import { FakeHttpService } from '../../data-access/fake-http.service';
38
import { StudentStore } from '../../data-access/student.store';
49
import { CardType } from '../../model/card.model';
5-
import { Student } from '../../model/student.model';
610
import { CardComponent } from '../../ui/card/card.component';
711

812
@Component({
913
selector: 'app-student-card',
1014
template: `
1115
<app-card
12-
[list]="students"
16+
[list]="students()"
1317
[type]="cardType"
14-
customClass="bg-light-green"></app-card>
18+
customClass="bg-light-green" />
1519
`,
1620
styles: [
1721
`
@@ -21,19 +25,16 @@ import { CardComponent } from '../../ui/card/card.component';
2125
`,
2226
],
2327
imports: [CardComponent],
28+
changeDetection: ChangeDetectionStrategy.OnPush,
2429
})
2530
export class StudentCardComponent implements OnInit {
26-
students: Student[] = [];
27-
cardType = CardType.STUDENT;
31+
private http = inject(FakeHttpService);
32+
private store = inject(StudentStore);
2833

29-
constructor(
30-
private http: FakeHttpService,
31-
private store: StudentStore,
32-
) {}
34+
students = this.store.students;
35+
cardType = CardType.STUDENT;
3336

3437
ngOnInit(): void {
3538
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
36-
37-
this.store.students$.subscribe((s) => (this.students = s));
3839
}
3940
}
Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, inject, OnInit } from '@angular/core';
22
import { FakeHttpService } from '../../data-access/fake-http.service';
33
import { TeacherStore } from '../../data-access/teacher.store';
44
import { CardType } from '../../model/card.model';
5-
import { Teacher } from '../../model/teacher.model';
65
import { CardComponent } from '../../ui/card/card.component';
76

87
@Component({
98
selector: 'app-teacher-card',
109
template: `
1110
<app-card
12-
[list]="teachers"
11+
[list]="teachers()"
1312
[type]="cardType"
1413
customClass="bg-light-red"></app-card>
1514
`,
@@ -23,17 +22,13 @@ import { CardComponent } from '../../ui/card/card.component';
2322
imports: [CardComponent],
2423
})
2524
export class TeacherCardComponent implements OnInit {
26-
teachers: Teacher[] = [];
27-
cardType = CardType.TEACHER;
25+
private http = inject(FakeHttpService);
26+
private store = inject(TeacherStore);
2827

29-
constructor(
30-
private http: FakeHttpService,
31-
private store: TeacherStore,
32-
) {}
28+
teachers = this.store.teachers;
29+
cardType = CardType.TEACHER;
3330

3431
ngOnInit(): void {
3532
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
36-
37-
this.store.teachers$.subscribe((t) => (this.teachers = t));
3833
}
3934
}
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import { Injectable } from '@angular/core';
2-
import { BehaviorSubject } from 'rxjs';
1+
import { Injectable, signal } from '@angular/core';
32
import { City } from '../model/city.model';
43

54
@Injectable({
65
providedIn: 'root',
76
})
87
export class CityStore {
9-
private cities = new BehaviorSubject<City[]>([]);
10-
cities$ = this.cities.asObservable();
8+
private cities = signal<City[]>([]);
119

1210
addAll(cities: City[]) {
13-
this.cities.next(cities);
11+
this.cities.set(cities);
1412
}
1513

1614
addOne(student: City) {
17-
this.cities.next([...this.cities.value, student]);
15+
this.cities.set([...this.cities(), student]);
1816
}
1917

2018
deleteOne(id: number) {
21-
this.cities.next(this.cities.value.filter((s) => s.id !== id));
19+
this.cities.set(this.cities().filter((s) => s.id !== id));
2220
}
2321
}
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import { Injectable } from '@angular/core';
2-
import { BehaviorSubject } from 'rxjs';
1+
import { Injectable, signal } from '@angular/core';
32
import { Student } from '../model/student.model';
43

54
@Injectable({
65
providedIn: 'root',
76
})
87
export class StudentStore {
9-
private students = new BehaviorSubject<Student[]>([]);
10-
students$ = this.students.asObservable();
8+
public students = signal<Student[]>([]);
119

1210
addAll(students: Student[]) {
13-
this.students.next(students);
11+
this.students.set(students);
1412
}
1513

1614
addOne(student: Student) {
17-
this.students.next([...this.students.value, student]);
15+
this.students.set([...this.students(), student]);
1816
}
1917

2018
deleteOne(id: number) {
21-
this.students.next(this.students.value.filter((s) => s.id !== id));
19+
this.students.set(this.students().filter((s) => s.id !== id));
2220
}
2321
}
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import { Injectable } from '@angular/core';
2-
import { BehaviorSubject } from 'rxjs';
1+
import { Injectable, signal } from '@angular/core';
32
import { Teacher } from '../model/teacher.model';
43

54
@Injectable({
65
providedIn: 'root',
76
})
87
export class TeacherStore {
9-
private teachers = new BehaviorSubject<Teacher[]>([]);
10-
teachers$ = this.teachers.asObservable();
8+
public teachers = signal<Teacher[]>([]);
119

1210
addAll(teachers: Teacher[]) {
13-
this.teachers.next(teachers);
11+
this.teachers.set(teachers);
1412
}
1513

1614
addOne(teacher: Teacher) {
17-
this.teachers.next([...this.teachers.value, teacher]);
15+
this.teachers.set([...this.teachers(), teacher]);
1816
}
1917

2018
deleteOne(id: number) {
21-
this.teachers.next(this.teachers.value.filter((t) => t.id !== id));
19+
this.teachers.set(this.teachers().filter((t) => t.id !== id));
2220
}
2321
}

apps/angular/1-projection/src/app/ui/card/card.component.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { NgFor, NgIf } from '@angular/common';
2-
import { Component, Input } from '@angular/core';
1+
import { NgOptimizedImage } from '@angular/common';
2+
import { Component, inject, input } from '@angular/core';
33
import { randStudent, randTeacher } from '../../data-access/fake-http.service';
44
import { StudentStore } from '../../data-access/student.store';
55
import { TeacherStore } from '../../data-access/teacher.store';
@@ -11,22 +11,21 @@ import { ListItemComponent } from '../list-item/list-item.component';
1111
template: `
1212
<div
1313
class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"
14-
[class]="customClass">
15-
<img
16-
*ngIf="type === CardType.TEACHER"
17-
src="assets/img/teacher.png"
18-
width="200px" />
19-
<img
20-
*ngIf="type === CardType.STUDENT"
21-
src="assets/img/student.webp"
22-
width="200px" />
14+
[class]="customClass()">
15+
@if (type() === CardType.TEACHER) {
16+
<img ngSrc="assets/img/teacher.png" width="200" height="200" />
17+
}
18+
@if (type() === CardType.STUDENT) {
19+
<img ngSrc="assets/img/student.webp" width="200" height="200" />
20+
}
2321
2422
<section>
25-
<app-list-item
26-
*ngFor="let item of list"
27-
[name]="item.firstName"
28-
[id]="item.id"
29-
[type]="type"></app-list-item>
23+
@for (item of list(); track item) {
24+
<app-list-item
25+
[name]="item.firstName"
26+
[id]="item.id"
27+
[type]="type()"></app-list-item>
28+
}
3029
</section>
3130
3231
<button
@@ -36,24 +35,23 @@ import { ListItemComponent } from '../list-item/list-item.component';
3635
</button>
3736
</div>
3837
`,
39-
imports: [NgIf, NgFor, ListItemComponent],
38+
imports: [ListItemComponent, NgOptimizedImage],
4039
})
4140
export class CardComponent {
42-
@Input() list: any[] | null = null;
43-
@Input() type!: CardType;
44-
@Input() customClass = '';
41+
private teacherStore = inject(TeacherStore);
42+
private studentStore = inject(StudentStore);
4543

46-
CardType = CardType;
44+
readonly list = input<any[] | null>(null);
45+
readonly type = input.required<CardType>();
46+
readonly customClass = input('');
4747

48-
constructor(
49-
private teacherStore: TeacherStore,
50-
private studentStore: StudentStore,
51-
) {}
48+
CardType = CardType;
5249

5350
addNewItem() {
54-
if (this.type === CardType.TEACHER) {
51+
const type = this.type();
52+
if (type === CardType.TEACHER) {
5553
this.teacherStore.addOne(randTeacher());
56-
} else if (this.type === CardType.STUDENT) {
54+
} else if (type === CardType.STUDENT) {
5755
this.studentStore.addOne(randStudent());
5856
}
5957
}

apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Component, Input } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
inject,
5+
input,
6+
} from '@angular/core';
27
import { StudentStore } from '../../data-access/student.store';
38
import { TeacherStore } from '../../data-access/teacher.store';
49
import { CardType } from '../../model/card.model';
@@ -7,28 +12,28 @@ import { CardType } from '../../model/card.model';
712
selector: 'app-list-item',
813
template: `
914
<div class="border-grey-300 flex justify-between border px-2 py-1">
10-
{{ name }}
11-
<button (click)="delete(id)">
15+
{{ name() }}
16+
<button (click)="delete(id())">
1217
<img class="h-5" src="assets/svg/trash.svg" />
1318
</button>
1419
</div>
1520
`,
1621
standalone: true,
22+
changeDetection: ChangeDetectionStrategy.OnPush,
1723
})
1824
export class ListItemComponent {
19-
@Input() id!: number;
20-
@Input() name!: string;
21-
@Input() type!: CardType;
25+
private teacherStore = inject(TeacherStore);
26+
private studentStore = inject(StudentStore);
2227

23-
constructor(
24-
private teacherStore: TeacherStore,
25-
private studentStore: StudentStore,
26-
) {}
28+
readonly id = input.required<number>();
29+
readonly name = input.required<string>();
30+
readonly type = input.required<CardType>();
2731

2832
delete(id: number) {
29-
if (this.type === CardType.TEACHER) {
33+
const type = this.type();
34+
if (type === CardType.TEACHER) {
3035
this.teacherStore.deleteOne(id);
31-
} else if (this.type === CardType.STUDENT) {
36+
} else if (type === CardType.STUDENT) {
3237
this.studentStore.deleteOne(id);
3338
}
3439
}

docs/src/content/docs/challenges/angular/1-projection.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ While the application works, the developer experience is far from being optimal.
4141
## Constraints
4242

4343
- You <b>must</b> refactor the `CardComponent` and `ListItemComponent`.
44-
- The `NgFor` directive must be declared and remain inside the `CardComponent`. You might be tempted to move it to the `ParentCardComponent` like `TeacherCardComponent`.
45-
- `CardComponent` should not contain any `NgIf` or `NgSwitch`.
44+
- The `@for` must be declared and remain inside the `CardComponent`. You might be tempted to move it to the `ParentCardComponent` like `TeacherCardComponent`.
45+
- `CardComponent` should not contain any conditions.
4646
- CSS: try to avoid using `::ng-deep`. Find a better way to handle CSS styling.
4747

4848
## Bonus Challenges
4949

50-
- Try to work with the new built-in control flow syntax for loops and conditionals (documentation [here](https://angular.dev/guide/templates/control-flow))
5150
- Use the signal API to manage your components state (documentation [here](https://angular.dev/guide/signals))
5251
- To reference the template, use a directive instead of magic strings ([What is wrong with magic strings?](https://softwareengineering.stackexchange.com/a/365344))

docs/src/content/docs/fr/challenges/angular/1-projection.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@ Bien que l'application fonctionne, l'expérience développeur est loin d'être o
3737
## Contraintes
3838

3939
- Vous <b>devez</b> refactoriser le `CardComponent` et le `ListItemComponent`.
40-
- 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`.
41-
- Le composant `CardComponent` ne doit contenir aucun `NgIf` ni `NgSwitch`.
40+
- 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`.
41+
- Le composant `CardComponent` ne doit contenir aucune condition.
4242
- CSS: essayez d'éviter d'utiliser `::ng-deep`. Trouvez un meilleur moyen de gérer le style CSS.
4343

4444
## Challenges Bonus
4545

46-
- 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))
4746
- Utilisez l'API des signals pour gérer l'état de vos composants (documentation [ici](https://angular.dev/guide/signals))
4847
- 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))

0 commit comments

Comments
 (0)