Skip to content

Commit 12dd8ad

Browse files
committed
feat(projection): solve challenge 1 using ng-content and ngTemplateOutlet
1 parent 543770b commit 12dd8ad

7 files changed

Lines changed: 142 additions & 87 deletions

File tree

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
inject,
5+
OnInit,
6+
} from '@angular/core';
7+
import { CityStore } from '../../data-access/city.store';
8+
import {
9+
FakeHttpService,
10+
randomCity,
11+
} from '../../data-access/fake-http.service';
12+
import { CardItemDirective } from '../../ui/card/card-item.directive';
13+
import { CardComponent } from '../../ui/card/card.component';
14+
import { ListItemComponent } from '../../ui/list-item/list-item.component';
215

316
@Component({
417
selector: 'app-city-card',
5-
template: 'TODO City',
6-
imports: [],
18+
template: `
19+
<app-card
20+
[list]="store.cities()"
21+
customClass="bg-blue-100"
22+
(add)="store.addOne(randomCity())">
23+
<ng-template appCardItem let-city>
24+
<app-list-item
25+
[id]="$any(city).id"
26+
[name]="$any(city).name"
27+
(delete)="store.deleteOne($event)" />
28+
</ng-template>
29+
</app-card>
30+
`,
31+
imports: [CardComponent, CardItemDirective, ListItemComponent],
732
changeDetection: ChangeDetectionStrategy.OnPush,
833
})
9-
export class CityCardComponent {}
34+
export class CityCardComponent implements OnInit {
35+
private http = inject(FakeHttpService);
36+
protected store = inject(CityStore);
37+
38+
randomCity = randomCity;
39+
40+
ngOnInit(): void {
41+
this.http.fetchCities$.subscribe((c) => this.store.addAll(c));
42+
}
43+
}

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
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 {
9+
FakeHttpService,
10+
randStudent,
11+
} from '../../data-access/fake-http.service';
812
import { StudentStore } from '../../data-access/student.store';
9-
import { CardType } from '../../model/card.model';
13+
import { CardItemDirective } from '../../ui/card/card-item.directive';
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',
1419
template: `
1520
<app-card
16-
[list]="students()"
17-
[type]="cardType"
18-
customClass="bg-light-green" />
21+
[list]="store.students()"
22+
customClass="bg-green-100"
23+
(add)="store.addOne(randStudent())">
24+
<img
25+
card-image
26+
ngSrc="assets/img/student.webp"
27+
width="200"
28+
height="200"
29+
alt="" />
30+
<ng-template appCardItem let-student>
31+
<app-list-item
32+
[id]="$any(student).id"
33+
[name]="$any(student).firstName"
34+
(delete)="store.deleteOne($event)" />
35+
</ng-template>
36+
</app-card>
1937
`,
20-
styles: [
21-
`
22-
::ng-deep .bg-light-green {
23-
background-color: rgba(0, 250, 0, 0.1);
24-
}
25-
`,
38+
imports: [
39+
CardComponent,
40+
CardItemDirective,
41+
ListItemComponent,
42+
NgOptimizedImage,
2643
],
27-
imports: [CardComponent],
2844
changeDetection: ChangeDetectionStrategy.OnPush,
2945
})
3046
export class StudentCardComponent implements OnInit {
3147
private http = inject(FakeHttpService);
32-
private store = inject(StudentStore);
48+
protected store = inject(StudentStore);
3349

34-
students = this.store.students;
35-
cardType = CardType.STUDENT;
50+
randStudent = randStudent;
3651

3752
ngOnInit(): void {
3853
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
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 {
4+
FakeHttpService,
5+
randTeacher,
6+
} from '../../data-access/fake-http.service';
37
import { TeacherStore } from '../../data-access/teacher.store';
4-
import { CardType } from '../../model/card.model';
8+
import { CardItemDirective } from '../../ui/card/card-item.directive';
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',
914
template: `
1015
<app-card
11-
[list]="teachers()"
12-
[type]="cardType"
13-
customClass="bg-light-red"></app-card>
16+
[list]="store.teachers()"
17+
customClass="bg-red-100"
18+
(add)="store.addOne(randTeacher())">
19+
<img
20+
card-image
21+
ngSrc="assets/img/teacher.png"
22+
width="200"
23+
height="200"
24+
alt="" />
25+
<ng-template appCardItem let-teacher>
26+
<app-list-item
27+
[id]="$any(teacher).id"
28+
[name]="$any(teacher).firstName"
29+
(delete)="store.deleteOne($event)" />
30+
</ng-template>
31+
</app-card>
1432
`,
15-
styles: [
16-
`
17-
::ng-deep .bg-light-red {
18-
background-color: rgba(250, 0, 0, 0.1);
19-
}
20-
`,
33+
imports: [
34+
CardComponent,
35+
CardItemDirective,
36+
ListItemComponent,
37+
NgOptimizedImage,
2138
],
22-
imports: [CardComponent],
2339
})
2440
export class TeacherCardComponent implements OnInit {
2541
private http = inject(FakeHttpService);
26-
private store = inject(TeacherStore);
42+
protected store = inject(TeacherStore);
2743

28-
teachers = this.store.teachers;
29-
cardType = CardType.TEACHER;
44+
randTeacher = randTeacher;
3045

3146
ngOnInit(): void {
3247
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));

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);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Directive, inject, TemplateRef } from '@angular/core';
2+
3+
export interface CardItemContext<T> {
4+
$implicit: T;
5+
}
6+
7+
@Directive({
8+
selector: '[appCardItem]',
9+
})
10+
export class CardItemDirective<T> {
11+
templateRef = inject<TemplateRef<CardItemContext<T>>>(TemplateRef);
12+
13+
static ngTemplateContextGuard<T>(
14+
_dir: CardItemDirective<T>,
15+
ctx: unknown,
16+
): ctx is CardItemContext<T> {
17+
return true;
18+
}
19+
}
Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,44 @@
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+
ChangeDetectionStrategy,
4+
Component,
5+
contentChild,
6+
input,
7+
output,
8+
} from '@angular/core';
9+
import { CardItemDirective } from './card-item.directive';
810

911
@Component({
1012
selector: 'app-card',
1113
template: `
1214
<div
1315
class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"
1416
[class]="customClass()">
15-
@if (type() === CardType.TEACHER) {
16-
<img ngSrc="assets/img/teacher.png" width="200" height="200" alt="" />
17-
}
18-
@if (type() === CardType.STUDENT) {
19-
<img ngSrc="assets/img/student.webp" width="200" height="200" alt="" />
20-
}
17+
<ng-content select="[card-image]" />
2118
2219
<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>
20+
@for (item of list(); track $index) {
21+
<ng-container
22+
[ngTemplateOutlet]="itemTemplate().templateRef"
23+
[ngTemplateOutletContext]="{ $implicit: item }" />
2824
}
2925
</section>
3026
3127
<button
3228
class="rounded-sm border border-blue-500 bg-blue-300 p-2"
33-
(click)="addNewItem()">
29+
(click)="add.emit()">
3430
Add
3531
</button>
3632
</div>
3733
`,
38-
imports: [ListItemComponent, NgOptimizedImage],
34+
imports: [NgTemplateOutlet],
35+
changeDetection: ChangeDetectionStrategy.OnPush,
3936
})
40-
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>();
37+
export class CardComponent<T> {
38+
readonly list = input<readonly T[]>([]);
4639
readonly customClass = input('');
4740

48-
CardType = CardType;
41+
protected itemTemplate = contentChild.required(CardItemDirective);
4942

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-
}
57-
}
43+
add = output<void>();
5844
}
Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,25 @@
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',
1310
template: `
1411
<div class="flex justify-between border border-gray-300 px-2 py-1">
1512
{{ name() }}
16-
<button (click)="delete(id())">
13+
<button (click)="delete.emit(id())">
1714
<img class="h-5" src="assets/svg/trash.svg" alt="trash" />
1815
</button>
1916
</div>
2017
`,
2118
changeDetection: ChangeDetectionStrategy.OnPush,
2219
})
2320
export class ListItemComponent {
24-
private teacherStore = inject(TeacherStore);
25-
private studentStore = inject(StudentStore);
26-
2721
readonly id = input.required<number>();
2822
readonly name = input.required<string>();
29-
readonly type = input.required<CardType>();
3023

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-
}
38-
}
24+
delete = output<number>();
3925
}

0 commit comments

Comments
 (0)