Skip to content

Answer: Challenge-01 (projection, decouple card logic) #1358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,9 +1,49 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
} from '@angular/core';
import { CityStore } from '../../data-access/city.store';
import {
FakeHttpService,
randomCity,
} from '../../data-access/fake-http.service';
import { CardComponent } from '../../ui/card/card.component';
import { ListItemComponent } from '../../ui/list-item/list-item.component';

@Component({
selector: 'app-city-card',
template: 'TODO City',
imports: [],
template: `
<ng-template #cityTemplate let-item>
<app-list-item
[name]="item.name"
[id]="item.id"
(deleteItem)="deleteItem($event)"></app-list-item>
</ng-template>
<app-card
[list]="cities()"
[itemTemplate]="cityTemplate"
img="assets/img/city.png"
customClass="bg-light-purple"
(addNewItem)="addItem()"
(deleteItem)="deleteItem($event)" />
`,
imports: [CardComponent, ListItemComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CityCardComponent {}
export class CityCardComponent implements OnInit {
private http = inject(FakeHttpService);
private store = inject(CityStore);
cities = this.store.cities;

ngOnInit(): void {
this.http.fetchCities$.subscribe((s) => this.store.addAll(s));
}
addItem() {
this.store.addOne(randomCity());
}
deleteItem(id: number) {
this.store.deleteOne(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,43 @@ import {
} 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 { CardComponent } from '../../ui/card/card.component';

import { randStudent } from '../../data-access/fake-http.service';
import { ListItemComponent } from '../../ui/list-item/list-item.component';

@Component({
selector: 'app-student-card',
template: `
<ng-template #studentTemplate let-item>
<app-list-item
[name]="item.firstName"
[id]="item.id"
(deleteItem)="deleteItem($event)"></app-list-item>
</ng-template>
<app-card
[list]="students()"
[type]="cardType"
customClass="bg-light-green" />
[itemTemplate]="studentTemplate"
img="assets/img/student.webp"
customClass="bg-light-green"
(addNewItem)="addItem()"
(deleteItem)="deleteItem($event)" />
`,
styles: [
`
::ng-deep .bg-light-green {
background-color: rgba(0, 250, 0, 0.1);
}
`,
],
imports: [CardComponent],
imports: [CardComponent, ListItemComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StudentCardComponent implements OnInit {
private http = inject(FakeHttpService);
private store = inject(StudentStore);

students = this.store.students;
cardType = CardType.STUDENT;

ngOnInit(): void {
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
}
addItem() {
this.store.addOne(randStudent());
}
deleteItem(id: number) {
this.store.deleteOne(id);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { Component, inject, OnInit } from '@angular/core';
import { FakeHttpService } from '../../data-access/fake-http.service';
import {
FakeHttpService,
randTeacher,
} from '../../data-access/fake-http.service';
import { TeacherStore } from '../../data-access/teacher.store';
import { CardType } from '../../model/card.model';
import { CardComponent } from '../../ui/card/card.component';
import { ListItemComponent } from '../../ui/list-item/list-item.component';

@Component({
selector: 'app-teacher-card',
template: `
<ng-template #teacherTemplate let-item>
<app-list-item
[name]="item.firstName"
[id]="item.id"
(deleteItem)="deleteItem($event)"></app-list-item>
</ng-template>
<app-card
[list]="teachers()"
[type]="cardType"
customClass="bg-light-red"></app-card>
[itemTemplate]="teacherTemplate"
img="assets/img/teacher.png"
customClass="bg-light-red"
(addNewItem)="addItem()"
(deleteItem)="deleteItem($event)"></app-card>
`,
styles: [
`
::ng-deep .bg-light-red {
background-color: rgba(250, 0, 0, 0.1);
}
`,
],
imports: [CardComponent],
imports: [CardComponent, ListItemComponent],
})
export class TeacherCardComponent implements OnInit {
private http = inject(FakeHttpService);
private store = inject(TeacherStore);

teachers = this.store.teachers;
cardType = CardType.TEACHER;

addItem() {
this.store.addOne(randTeacher());
}
deleteItem(id: number) {
this.store.deleteOne(id);
}
ngOnInit(): void {
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { City } from '../model/city.model';
providedIn: 'root',
})
export class CityStore {
private cities = signal<City[]>([]);
public cities = signal<City[]>([]);

addAll(cities: City[]) {
this.cities.set(cities);
Expand Down
59 changes: 25 additions & 34 deletions apps/angular/1-projection/src/app/ui/card/card.component.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,49 @@
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';
import { CardType } from '../../model/card.model';
import { ListItemComponent } from '../list-item/list-item.component';
import { NgOptimizedImage, NgTemplateOutlet } from '@angular/common';
import { Component, input, output, TemplateRef } from '@angular/core';
import { City } from '../../model/city.model';
import { Student } from '../../model/student.model';
import { Teacher } from '../../model/teacher.model';

@Component({
selector: 'app-card',
template: `
<div
class="flex w-fit flex-col gap-3 rounded-md border-2 border-black p-4"
[class]="customClass()">
@if (type() === CardType.TEACHER) {
<img ngSrc="assets/img/teacher.png" width="200" height="200" />
}
@if (type() === CardType.STUDENT) {
<img ngSrc="assets/img/student.webp" width="200" height="200" />
}
<img [ngSrc]="img()" width="200" height="200" />

<section>
@for (item of list(); track item) {
<app-list-item
[name]="item.firstName"
[id]="item.id"
[type]="type()"></app-list-item>
<ng-container
*ngTemplateOutlet="
itemTemplate();
context: { $implicit: item }
"></ng-container>
}
</section>

<button
class="rounded-sm border border-blue-500 bg-blue-300 p-2"
(click)="addNewItem()">
(click)="addNewItemClick()">
Add
</button>
</div>
`,
imports: [ListItemComponent, NgOptimizedImage],
imports: [NgOptimizedImage, NgTemplateOutlet],
})
export class CardComponent {
private teacherStore = inject(TeacherStore);
private studentStore = inject(StudentStore);

readonly list = input<any[] | null>(null);
readonly type = input.required<CardType>();
readonly list = input<Teacher[] | City[] | Student[] | null>(null);
readonly customClass = input('');

CardType = CardType;

addNewItem() {
const type = this.type();
if (type === CardType.TEACHER) {
this.teacherStore.addOne(randTeacher());
} else if (type === CardType.STUDENT) {
this.studentStore.addOne(randStudent());
}
readonly img = input<string>('');
addNewItem = output<void>();
deleteItem = output<number>();
readonly itemTemplate = input.required<TemplateRef<{
$implicit: Teacher | Student | City;
}> | null>();
addNewItemClick() {
this.addNewItem.emit();
}
deleteItemClick(id: number) {
this.deleteItem.emit(id);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
output,
} from '@angular/core';
import { StudentStore } from '../../data-access/student.store';
import { TeacherStore } from '../../data-access/teacher.store';
import { CardType } from '../../model/card.model';

@Component({
selector: 'app-list-item',
Expand All @@ -21,19 +18,10 @@ import { CardType } from '../../model/card.model';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ListItemComponent {
private teacherStore = inject(TeacherStore);
private studentStore = inject(StudentStore);

readonly id = input.required<number>();
readonly name = input.required<string>();
readonly type = input.required<CardType>();

deleteItem = output<number>();
delete(id: number) {
const type = this.type();
if (type === CardType.TEACHER) {
this.teacherStore.deleteOne(id);
} else if (type === CardType.STUDENT) {
this.studentStore.deleteOne(id);
}
this.deleteItem.emit(id);
}
}
12 changes: 12 additions & 0 deletions apps/angular/1-projection/src/styles.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

.bg-light-purple {
background-color: rgba(150, 0, 250, 0.1);
}

.bg-light-green {
background-color: rgba(0, 250, 0, 0.1);
}

.bg-light-red {
background-color: rgba(250, 0, 0, 0.1);
}
Loading