From 302e82fd06cb7af5893d86bb5df3f040d59b40db Mon Sep 17 00:00:00 2001 From: Ronali Senapati Date: Wed, 3 Dec 2025 17:23:16 +0100 Subject: [PATCH 1/5] chore: projection task done --- .../city-card/city-card.component.ts | 32 +++++++++-- .../student-card/student-card.component.ts | 9 +--- .../teacher-card/teacher-card.component.ts | 9 +--- .../src/app/data-access/city.store.ts | 2 +- .../src/app/ui/card/card.component.ts | 54 +++++++++++++------ .../app/ui/list-item/list-item.component.ts | 15 +++--- 6 files changed, 77 insertions(+), 44 deletions(-) diff --git a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts index 8895c8c84..253727dd8 100644 --- a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts +++ b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts @@ -1,9 +1,33 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, +} from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; +import { FakeHttpService } from '../../data-access/fake-http.service'; +import { CardType } from '../../model/card.model'; +import { CardComponent } from '../../ui/card/card.component'; @Component({ selector: 'app-city-card', - template: 'TODO City', - imports: [], + template: ` + + `, + imports: [CardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CityCardComponent {} +export class CityCardComponent implements OnInit { + private http = inject(FakeHttpService); + private store = inject(CityStore); + + cities = this.store.cities; + cardType = CardType.CITY; + + ngOnInit(): void { + this.http.fetchCities$.subscribe((c) => this.store.addAll(c)); + } +} diff --git a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts index bdfa4abd4..94a2ab1f0 100644 --- a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts +++ b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts @@ -15,15 +15,8 @@ import { CardComponent } from '../../ui/card/card.component'; + backgroundColor="rgba(0, 250, 0, 0.1)" /> `, - styles: [ - ` - ::ng-deep .bg-light-green { - background-color: rgba(0, 250, 0, 0.1); - } - `, - ], imports: [CardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts index adf0ad3c1..08fd97e6f 100644 --- a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts +++ b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts @@ -10,15 +10,8 @@ import { CardComponent } from '../../ui/card/card.component'; + backgroundColor="rgba(250, 0, 0, 0.1)"> `, - styles: [ - ` - ::ng-deep .bg-light-red { - background-color: rgba(250, 0, 0, 0.1); - } - `, - ], imports: [CardComponent], }) export class TeacherCardComponent implements OnInit { diff --git a/apps/angular/1-projection/src/app/data-access/city.store.ts b/apps/angular/1-projection/src/app/data-access/city.store.ts index a8b523569..9fbcb346b 100644 --- a/apps/angular/1-projection/src/app/data-access/city.store.ts +++ b/apps/angular/1-projection/src/app/data-access/city.store.ts @@ -5,7 +5,7 @@ import { City } from '../model/city.model'; providedIn: 'root', }) export class CityStore { - private cities = signal([]); + public cities = signal([]); addAll(cities: City[]) { this.cities.set(cities); diff --git a/apps/angular/1-projection/src/app/ui/card/card.component.ts b/apps/angular/1-projection/src/app/ui/card/card.component.ts index 1a6c3648c..32d731029 100644 --- a/apps/angular/1-projection/src/app/ui/card/card.component.ts +++ b/apps/angular/1-projection/src/app/ui/card/card.component.ts @@ -1,6 +1,11 @@ import { NgOptimizedImage } from '@angular/common'; -import { Component, inject, input } from '@angular/core'; -import { randStudent, randTeacher } from '../../data-access/fake-http.service'; +import { Component, computed, inject, input } from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; +import { + randomCity, + 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'; @@ -11,18 +16,13 @@ import { ListItemComponent } from '../list-item/list-item.component'; template: `
- @if (type() === CardType.TEACHER) { - - } - @if (type() === CardType.STUDENT) { - - } + [style.backgroundColor]="backgroundColor()"> +
@for (item of list(); track item) { } @@ -40,19 +40,39 @@ import { ListItemComponent } from '../list-item/list-item.component'; export class CardComponent { private teacherStore = inject(TeacherStore); private studentStore = inject(StudentStore); + private cityStore = inject(CityStore); readonly list = input(null); readonly type = input.required(); - readonly customClass = input(''); + readonly backgroundColor = input(''); CardType = CardType; + addHandler: Record void> = { + [CardType.TEACHER]: () => this.teacherStore.addOne(randTeacher()), + [CardType.STUDENT]: () => this.studentStore.addOne(randStudent()), + [CardType.CITY]: () => this.cityStore.addOne(randomCity()), + }; + + nameLookup: Record string> = { + [CardType.TEACHER]: (item: any) => item.firstName, + [CardType.STUDENT]: (item: any) => item.firstName, + [CardType.CITY]: (item: any) => item.name, + }; + addNewItem() { - const type = this.type(); - if (type === CardType.TEACHER) { - this.teacherStore.addOne(randTeacher()); - } else if (type === CardType.STUDENT) { - this.studentStore.addOne(randStudent()); - } + this.addHandler[this.type()](); + } + + itemName(item: any) { + return this.nameLookup[this.type()](item); } + + imageLookup: Record = { + [CardType.TEACHER]: 'assets/img/teacher.png', + [CardType.STUDENT]: 'assets/img/student.webp', + [CardType.CITY]: 'assets/img/city.png', + }; + + imageSrc = computed(() => this.imageLookup[this.type()]); } diff --git a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts index 5d504f372..0c5eadcd8 100644 --- a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts +++ b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts @@ -4,6 +4,7 @@ import { inject, input, } from '@angular/core'; +import { CityStore } from '../../data-access/city.store'; import { StudentStore } from '../../data-access/student.store'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardType } from '../../model/card.model'; @@ -23,17 +24,19 @@ import { CardType } from '../../model/card.model'; export class ListItemComponent { private teacherStore = inject(TeacherStore); private studentStore = inject(StudentStore); + private cityStore = inject(CityStore); readonly id = input.required(); readonly name = input.required(); readonly type = input.required(); + deleteItem: Record void> = { + [CardType.TEACHER]: (id: number) => this.teacherStore.deleteOne(id), + [CardType.STUDENT]: (id: number) => this.studentStore.deleteOne(id), + [CardType.CITY]: (id: number) => this.cityStore.deleteOne(id), + }; + 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[this.type()](id); } } From 4d6370cb91bf2bb7ae481119d62fdc671d3dcef4 Mon Sep 17 00:00:00 2001 From: Ronali Senapati Date: Wed, 3 Dec 2025 18:49:12 +0100 Subject: [PATCH 2/5] chore: crud application done --- .../src/app/app.component.ts | 160 ++++++++++++++---- .../src/app/data-access/todo-list.service.ts | 56 ++++++ .../src/app/model/todo.model.ts | 7 + 3 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 apps/angular/5-crud-application/src/app/data-access/todo-list.service.ts create mode 100644 apps/angular/5-crud-application/src/app/model/todo.model.ts diff --git a/apps/angular/5-crud-application/src/app/app.component.ts b/apps/angular/5-crud-application/src/app/app.component.ts index 73ba0dc34..2621d8923 100644 --- a/apps/angular/5-crud-application/src/app/app.component.ts +++ b/apps/angular/5-crud-application/src/app/app.component.ts @@ -1,49 +1,139 @@ -import { HttpClient } from '@angular/common/http'; -import { Component, inject, OnInit } from '@angular/core'; -import { randText } from '@ngneat/falso'; +import { Component, OnInit, inject } from '@angular/core'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { TodoListService } from './data-access/todo-list.service'; +import { Todo } from './model/todo.model'; @Component({ - imports: [], selector: 'app-root', + imports: [MatProgressSpinnerModule], template: ` - @for (todo of todos; track todo.id) { - {{ todo.title }} - + @if (loading()) { +
+ +
} + +
+

Todo list

+ + @if (todos().length === 0) { +

Loading...

+ } @else { +
    + @for (todo of todos(); track todo.id) { +
  • +
    +

    #{{ todo.id }} ยท User {{ todo.userId }}

    +

    {{ todo.title }}

    +
    +
    + + +
    +
  • + } +
+ } +
`, - styles: [], + styles: [ + ` + :host { + display: block; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + sans-serif; + color: #0f172a; + padding: 16px; + } + .page { + max-width: 720px; + margin: 0 auto; + } + h1 { + margin: 0 0 10px; + font-size: 24px; + } + .muted { + margin: 0; + color: #64748b; + } + .list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 8px; + } + .item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + padding: 12px; + border-radius: 10px; + border: 1px solid #e5e7eb; + background: #f8fafc; + } + .title { + margin: 6px 0 0; + font-weight: 600; + } + .actions { + display: flex; + gap: 8px; + } + .btn { + border: none; + background: #475569; + color: #fff; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + font-weight: 600; + transition: opacity 120ms ease; + } + .btn-delete { + background: #ef4444; + } + .btn:hover { + opacity: 0.9; + } + .overlay { + position: fixed; + inset: 0; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(2px); + z-index: 10; + } + `, + ], }) export class AppComponent implements OnInit { - private http = inject(HttpClient); - - todos!: any[]; + private todoService = inject(TodoListService); + todos = this.todoService.todos; + loading = this.todoService.loading; ngOnInit(): void { - this.http - .get('https://jsonplaceholder.typicode.com/todos') - .subscribe((todos) => { - this.todos = todos; - }); + this.todoService.loadTodos(); + } + + update(todo: Todo) { + this.todoService.updateTodo(todo); } - update(todo: any) { - this.http - .put( - `https://jsonplaceholder.typicode.com/todos/${todo.id}`, - JSON.stringify({ - todo: todo.id, - title: randText(), - body: todo.body, - userId: todo.userId, - }), - { - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }, - ) - .subscribe((todoUpdated: any) => { - this.todos[todoUpdated.id - 1] = todoUpdated; - }); + delete(todo: Todo) { + this.todoService.deleteTodo(todo); } } diff --git a/apps/angular/5-crud-application/src/app/data-access/todo-list.service.ts b/apps/angular/5-crud-application/src/app/data-access/todo-list.service.ts new file mode 100644 index 000000000..d50af6eae --- /dev/null +++ b/apps/angular/5-crud-application/src/app/data-access/todo-list.service.ts @@ -0,0 +1,56 @@ +import { HttpClient } from '@angular/common/http'; +import { computed, inject, Injectable, signal } from '@angular/core'; +import { randText } from '@ngneat/falso'; +import { finalize } from 'rxjs/operators'; +import { Todo } from '../model/todo.model'; + +@Injectable({ + providedIn: 'root', +}) +export class TodoListService { + private http = inject(HttpClient); + private _todos = signal([]); + private _loading = signal(false); + + todos = computed(() => this._todos()); + loading = computed(() => this._loading()); + + loadTodos() { + this._loading.set(true); + return this.http + .get('https://jsonplaceholder.typicode.com/todos') + .pipe(finalize(() => this._loading.set(false))) + .subscribe((todos) => this._todos.set(todos)); + } + + updateTodo(todo: Todo) { + this._loading.set(true); + return this.http + .put( + `https://jsonplaceholder.typicode.com/todos/${todo.id}`, + { + todo: todo.id, + title: randText(), + body: todo.body, + userId: todo.userId, + }, + { headers: { 'Content-type': 'application/json; charset=UTF-8' } }, + ) + .pipe(finalize(() => this._loading.set(false))) + .subscribe((todoUpdated) => + this._todos.update((todos) => + todos.map((t) => (t.id === todoUpdated.id ? todoUpdated : t)), + ), + ); + } + + deleteTodo(todo: Todo) { + this._loading.set(true); + return this.http + .delete(`https://jsonplaceholder.typicode.com/todos/${todo.id}`) + .pipe(finalize(() => this._loading.set(false))) + .subscribe(() => { + this._todos.update((todos) => todos.filter((t) => t.id !== todo.id)); + }); + } +} diff --git a/apps/angular/5-crud-application/src/app/model/todo.model.ts b/apps/angular/5-crud-application/src/app/model/todo.model.ts new file mode 100644 index 000000000..49a348cc1 --- /dev/null +++ b/apps/angular/5-crud-application/src/app/model/todo.model.ts @@ -0,0 +1,7 @@ +export interface Todo { + id: number; + userId: number; + title: string; + completed: boolean; + body?: string; +} From 1fd72e0b20f665766d7801e4a80570dedb9a6ca8 Mon Sep 17 00:00:00 2001 From: Ronali Senapati Date: Thu, 4 Dec 2025 10:40:26 +0100 Subject: [PATCH 3/5] chore: pure pipes done --- apps/angular/8-pure-pipe/src/app/app.component.ts | 11 ++++------- .../src/app/heavy-computation.transform.pipe.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 apps/angular/8-pure-pipe/src/app/heavy-computation.transform.pipe.ts diff --git a/apps/angular/8-pure-pipe/src/app/app.component.ts b/apps/angular/8-pure-pipe/src/app/app.component.ts index 930fe1313..4c869d81b 100644 --- a/apps/angular/8-pure-pipe/src/app/app.component.ts +++ b/apps/angular/8-pure-pipe/src/app/app.component.ts @@ -1,18 +1,15 @@ import { Component } from '@angular/core'; +import { HeavyComputationTransformPipe } from './heavy-computation.transform.pipe'; @Component({ selector: 'app-root', template: ` @for (person of persons; track person) { - {{ heavyComputation(person, $index) }} + {{ person | heavyComputation: $index }} } `, + imports: [HeavyComputationTransformPipe], }) export class AppComponent { - persons = ['toto', 'jack']; - - heavyComputation(name: string, index: number) { - // very heavy computation - return `${name} - ${index}`; - } + persons = ['toto', 'jack', 'marie', 'alice', 'bob']; } diff --git a/apps/angular/8-pure-pipe/src/app/heavy-computation.transform.pipe.ts b/apps/angular/8-pure-pipe/src/app/heavy-computation.transform.pipe.ts new file mode 100644 index 000000000..9d2268d15 --- /dev/null +++ b/apps/angular/8-pure-pipe/src/app/heavy-computation.transform.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'heavyComputation', + pure: true, +}) +export class HeavyComputationTransformPipe implements PipeTransform { + transform(name: string, index: number): string { + return `${name} - ${index}`; + } +} From d24d04335927b1e2d29d805ba358bd2fc0a19b21 Mon Sep 17 00:00:00 2001 From: Ronali Senapati Date: Thu, 4 Dec 2025 11:41:46 +0100 Subject: [PATCH 4/5] chore: router input done --- .../22-router-input/src/app/app.config.ts | 4 ++-- .../22-router-input/src/app/test.component.ts | 20 +++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/apps/angular/22-router-input/src/app/app.config.ts b/apps/angular/22-router-input/src/app/app.config.ts index ed404941f..a7c1007b9 100644 --- a/apps/angular/22-router-input/src/app/app.config.ts +++ b/apps/angular/22-router-input/src/app/app.config.ts @@ -1,7 +1,7 @@ import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; import { appRoutes } from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [provideRouter(appRoutes)], + providers: [provideRouter(appRoutes, withComponentInputBinding())], }; diff --git a/apps/angular/22-router-input/src/app/test.component.ts b/apps/angular/22-router-input/src/app/test.component.ts index 747ab4483..d99a15043 100644 --- a/apps/angular/22-router-input/src/app/test.component.ts +++ b/apps/angular/22-router-input/src/app/test.component.ts @@ -1,21 +1,15 @@ -import { AsyncPipe } from '@angular/common'; -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { map } from 'rxjs'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'app-subscription', - imports: [AsyncPipe], template: ` -
TestId: {{ testId$ | async }}
-
Permission: {{ permission$ | async }}
-
User: {{ user$ | async }}
+
TestId: {{ testId }}
+
Permission: {{ permission }}
+
User: {{ user }}
`, }) export default class TestComponent { - private activatedRoute = inject(ActivatedRoute); - - testId$ = this.activatedRoute.params.pipe(map((p) => p['testId'])); - permission$ = this.activatedRoute.data.pipe(map((d) => d['permission'])); - user$ = this.activatedRoute.queryParams.pipe(map((q) => q['user'])); + @Input() testId!: string | number; + @Input() permission!: string; + @Input() user: string | null = null; } From a8aaaead2c0fb8c4e259fcb1170e8f0c5645eda6 Mon Sep 17 00:00:00 2001 From: Ronali Senapati Date: Thu, 4 Dec 2025 16:27:09 +0100 Subject: [PATCH 5/5] chore: module to standalone done --- .../31-module-to-standalone/src/app/app.component.ts | 4 +++- .../31-module-to-standalone/src/app/app.config.ts | 11 +++++++++++ .../31-module-to-standalone/src/app/app.module.ts | 11 ----------- apps/angular/31-module-to-standalone/src/main.ts | 9 ++++----- 4 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 apps/angular/31-module-to-standalone/src/app/app.config.ts delete mode 100644 apps/angular/31-module-to-standalone/src/app/app.module.ts diff --git a/apps/angular/31-module-to-standalone/src/app/app.component.ts b/apps/angular/31-module-to-standalone/src/app/app.component.ts index 986df84b5..c4c7ecbfe 100644 --- a/apps/angular/31-module-to-standalone/src/app/app.component.ts +++ b/apps/angular/31-module-to-standalone/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { RouterLink, RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', @@ -25,6 +26,7 @@ import { Component } from '@angular/core'; host: { class: 'flex flex-col p-4 gap-3', }, - standalone: false, + standalone: true, + imports: [RouterLink, RouterOutlet], }) export class AppComponent {} diff --git a/apps/angular/31-module-to-standalone/src/app/app.config.ts b/apps/angular/31-module-to-standalone/src/app/app.config.ts new file mode 100644 index 000000000..ba8866e79 --- /dev/null +++ b/apps/angular/31-module-to-standalone/src/app/app.config.ts @@ -0,0 +1,11 @@ +import { provideToken } from '@angular-challenges/module-to-standalone/core/providers'; +import { ApplicationConfig } from '@angular/core'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { appRoutes } from 'libs/module-to-standalone/shell/src/lib/main-shell.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(appRoutes, withComponentInputBinding()), + provideToken('main-shell-token'), + ], +}; diff --git a/apps/angular/31-module-to-standalone/src/app/app.module.ts b/apps/angular/31-module-to-standalone/src/app/app.module.ts deleted file mode 100644 index c795a11b9..000000000 --- a/apps/angular/31-module-to-standalone/src/app/app.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MainShellModule } from '@angular-challenges/module-to-standalone/shell'; -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { AppComponent } from './app.component'; - -@NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, MainShellModule], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/apps/angular/31-module-to-standalone/src/main.ts b/apps/angular/31-module-to-standalone/src/main.ts index 16de2365d..a257eeb07 100644 --- a/apps/angular/31-module-to-standalone/src/main.ts +++ b/apps/angular/31-module-to-standalone/src/main.ts @@ -1,6 +1,5 @@ -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((e) => console.error(e));