Skip to content

Commit 80a9d0e

Browse files
committed
feat(projection): use the container & presenter architecture
UI now presentational. Uses content projection and outputs for better reusability. Refs: Challenge 1
1 parent 418a2fd commit 80a9d0e

File tree

11 files changed

+164
-87
lines changed

11 files changed

+164
-87
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ package-lock.json
5151
npm-shrinkwrap.json
5252

5353
__screenshots__/
54+
.idea/

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TeacherCardComponent } from './component/teacher-card/teacher-card.comp
55

66
@Component({
77
selector: 'app-root',
8+
standalone: true,
89
template: `
910
<div class="grid grid-cols-3 gap-3">
1011
<app-teacher-card />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Directive } from '@angular/core';
2+
3+
@Directive({
4+
standalone: true,
5+
selector: '[appCardItem]',
6+
})
7+
export class CardItemDirective {}
Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,52 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import { NgOptimizedImage } from '@angular/common';
2+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
3+
import { CardItemDirective } from '../../card-item';
4+
import { CityStore } from '../../data-access/city.store';
5+
import {
6+
FakeHttpService,
7+
randomCity,
8+
} from '../../data-access/fake-http.service';
9+
import { CardComponent } from '../../ui/card/card.component';
10+
import { ListItemComponent } from '../../ui/list-item/list-item.component';
211

312
@Component({
413
selector: 'app-city-card',
5-
template: 'TODO City',
6-
imports: [],
14+
standalone: true,
15+
template: `
16+
<app-card
17+
(addClicked)="store.addOne(randCity())"
18+
[list]="cities()"
19+
customClass="bg-blue-100">
20+
<img
21+
ngSrc="../../../assets/img/city.png"
22+
width="200"
23+
height="200"
24+
alt="City Icon" />
25+
26+
<ng-template appCardItem let-item>
27+
<app-list-item
28+
[id]="item.id"
29+
[name]="item.name"
30+
(deleteClicked)="store.deleteOne(item.id)"></app-list-item>
31+
</ng-template>
32+
</app-card>
33+
`,
34+
imports: [
35+
CardComponent,
36+
ListItemComponent,
37+
NgOptimizedImage,
38+
CardItemDirective,
39+
],
740
changeDetection: ChangeDetectionStrategy.OnPush,
841
})
9-
export class CityCardComponent {}
42+
export class CityCardComponent {
43+
protected store = inject(CityStore);
44+
private http = inject(FakeHttpService);
45+
46+
cities = this.store.cities;
47+
protected readonly randCity = randomCity;
48+
49+
ngOnInit(): void {
50+
this.http.fetchCities$.subscribe((s) => this.store.addAll(s));
51+
}
52+
}

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,56 @@
1+
import { NgOptimizedImage } from '@angular/common';
12
import {
23
ChangeDetectionStrategy,
34
Component,
45
inject,
56
OnInit,
67
} from '@angular/core';
7-
import { FakeHttpService } from '../../data-access/fake-http.service';
8+
import { CardItemDirective } from '../../card-item';
9+
import {
10+
FakeHttpService,
11+
randStudent,
12+
} from '../../data-access/fake-http.service';
813
import { StudentStore } from '../../data-access/student.store';
9-
import { CardType } from '../../model/card.model';
1014
import { CardComponent } from '../../ui/card/card.component';
15+
import { ListItemComponent } from '../../ui/list-item/list-item.component';
1116

1217
@Component({
1318
selector: 'app-student-card',
19+
standalone: true,
1420
template: `
1521
<app-card
22+
(addClicked)="store.addOne(randStudent())"
1623
[list]="students()"
17-
[type]="cardType"
18-
customClass="bg-light-green" />
24+
customClass="bg-green-100">
25+
<img
26+
ngSrc="../../../assets/img/student.webp"
27+
width="200"
28+
height="200"
29+
alt="Student Icon" />
30+
31+
<ng-template appCardItem let-item>
32+
<app-list-item
33+
[id]="item.id"
34+
[name]="item.firstName"
35+
(deleteClicked)="store.deleteOne(item.id)"></app-list-item>
36+
</ng-template>
37+
</app-card>
1938
`,
20-
styles: [
21-
`
22-
::ng-deep .bg-light-green {
23-
background-color: rgba(0, 250, 0, 0.1);
24-
}
25-
`,
39+
styles: [],
40+
imports: [
41+
CardComponent,
42+
ListItemComponent,
43+
NgOptimizedImage,
44+
CardItemDirective,
2645
],
27-
imports: [CardComponent],
2846
changeDetection: ChangeDetectionStrategy.OnPush,
2947
})
3048
export class StudentCardComponent implements OnInit {
3149
private http = inject(FakeHttpService);
32-
private store = inject(StudentStore);
50+
protected store = inject(StudentStore);
3351

3452
students = this.store.students;
35-
cardType = CardType.STUDENT;
53+
protected readonly randStudent = randStudent;
3654

3755
ngOnInit(): void {
3856
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,54 @@
1+
import { NgOptimizedImage } from '@angular/common';
12
import { Component, inject, OnInit } from '@angular/core';
2-
import { FakeHttpService } from '../../data-access/fake-http.service';
3+
import { CardItemDirective } from '../../card-item';
4+
import {
5+
FakeHttpService,
6+
randTeacher,
7+
} from '../../data-access/fake-http.service';
38
import { TeacherStore } from '../../data-access/teacher.store';
4-
import { CardType } from '../../model/card.model';
59
import { CardComponent } from '../../ui/card/card.component';
10+
import { ListItemComponent } from '../../ui/list-item/list-item.component';
611

712
@Component({
813
selector: 'app-teacher-card',
14+
standalone: true,
915
template: `
1016
<app-card
17+
(addClicked)="store.addOne(randTeacher())"
1118
[list]="teachers()"
12-
[type]="cardType"
13-
customClass="bg-light-red"></app-card>
19+
customClass="bg-red-100">
20+
<img
21+
ngSrc="../../../assets/img/teacher.png"
22+
width="200"
23+
height="200"
24+
priority
25+
alt="Teacher Icon" />
26+
27+
<ng-template appCardItem let-item>
28+
<app-list-item
29+
[id]="item.id"
30+
[name]="item.firstName"
31+
(deleteClicked)="store.deleteOne(item.id)"></app-list-item>
32+
</ng-template>
33+
</app-card>
1434
`,
15-
styles: [
16-
`
17-
::ng-deep .bg-light-red {
18-
background-color: rgba(250, 0, 0, 0.1);
19-
}
20-
`,
35+
styles: [],
36+
imports: [
37+
CardComponent,
38+
ListItemComponent,
39+
NgOptimizedImage,
40+
CardItemDirective,
2141
],
22-
imports: [CardComponent],
2342
})
2443
export class TeacherCardComponent implements OnInit {
2544
private http = inject(FakeHttpService);
26-
private store = inject(TeacherStore);
45+
protected store = inject(TeacherStore);
2746

2847
teachers = this.store.teachers;
29-
cardType = CardType.TEACHER;
3048

3149
ngOnInit(): void {
3250
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
3351
}
52+
53+
protected readonly randTeacher = randTeacher;
3454
}

apps/angular/1-projection/src/app/data-access/city.store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { City } from '../model/city.model';
55
providedIn: 'root',
66
})
77
export class CityStore {
8-
private cities = signal<City[]>([]);
8+
public cities = signal<City[]>([]);
99

1010
addAll(cities: City[]) {
1111
this.cities.set(cities);

apps/angular/1-projection/src/app/model/card.model.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,50 @@
1-
import { NgOptimizedImage } from '@angular/common';
2-
import { Component, inject, input } from '@angular/core';
3-
import { randStudent, randTeacher } from '../../data-access/fake-http.service';
4-
import { StudentStore } from '../../data-access/student.store';
5-
import { TeacherStore } from '../../data-access/teacher.store';
6-
import { CardType } from '../../model/card.model';
7-
import { ListItemComponent } from '../list-item/list-item.component';
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import {
3+
Component,
4+
contentChild,
5+
input,
6+
output,
7+
TemplateRef,
8+
} from '@angular/core';
9+
import { CardItemDirective } from '../../card-item';
810

911
@Component({
1012
selector: 'app-card',
13+
standalone: true,
1114
template: `
1215
<div
1316
class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"
1417
[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-
}
18+
<ng-content></ng-content>
2119
2220
<section>
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>
21+
@for (item of list(); track item.id) {
22+
<ng-container
23+
*ngTemplateOutlet="
24+
itemTemplate();
25+
context: { $implicit: item }
26+
"></ng-container>
2827
}
2928
</section>
3029
3130
<button
3231
class="rounded-sm border border-blue-500 bg-blue-300 p-2"
33-
(click)="addNewItem()">
32+
(click)="onAddClicked()">
3433
Add
3534
</button>
3635
</div>
3736
`,
38-
imports: [ListItemComponent, NgOptimizedImage],
37+
imports: [NgTemplateOutlet],
3938
})
4039
export class CardComponent {
41-
private teacherStore = inject(TeacherStore);
42-
private studentStore = inject(StudentStore);
43-
44-
readonly list = input<any[] | null>(null);
45-
readonly type = input.required<CardType>();
40+
readonly list = input<any[]>();
41+
readonly itemTemplate = contentChild.required(CardItemDirective, {
42+
read: TemplateRef,
43+
});
4644
readonly customClass = input('');
4745

48-
CardType = CardType;
49-
50-
addNewItem() {
51-
const type = this.type();
52-
if (type === CardType.TEACHER) {
53-
this.teacherStore.addOne(randTeacher());
54-
} else if (type === CardType.STUDENT) {
55-
this.studentStore.addOne(randStudent());
56-
}
46+
addClicked = output<void>();
47+
onAddClicked() {
48+
this.addClicked.emit();
5749
}
5850
}
Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
11
import {
22
ChangeDetectionStrategy,
33
Component,
4-
inject,
54
input,
5+
output,
66
} from '@angular/core';
7-
import { StudentStore } from '../../data-access/student.store';
8-
import { TeacherStore } from '../../data-access/teacher.store';
9-
import { CardType } from '../../model/card.model';
107

118
@Component({
129
selector: 'app-list-item',
10+
standalone: true,
1311
template: `
1412
<div class="border-grey-300 flex justify-between border px-2 py-1">
1513
{{ name() }}
16-
<button (click)="delete(id())">
14+
<button (click)="onDeleteClicked()">
1715
<img class="h-5" src="assets/svg/trash.svg" />
1816
</button>
1917
</div>
2018
`,
2119
changeDetection: ChangeDetectionStrategy.OnPush,
2220
})
2321
export class ListItemComponent {
24-
private teacherStore = inject(TeacherStore);
25-
private studentStore = inject(StudentStore);
26-
2722
readonly id = input.required<number>();
2823
readonly name = input.required<string>();
29-
readonly type = input.required<CardType>();
3024

31-
delete(id: number) {
32-
const type = this.type();
33-
if (type === CardType.TEACHER) {
34-
this.teacherStore.deleteOne(id);
35-
} else if (type === CardType.STUDENT) {
36-
this.studentStore.deleteOne(id);
37-
}
25+
deleteClicked = output<number>();
26+
onDeleteClicked() {
27+
this.deleteClicked.emit(this.id());
3828
}
3929
}

0 commit comments

Comments
 (0)