- @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);
}
}
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;
}
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));
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 {
+
+ }
+
`,
- 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;
+}
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}`;
+ }
+}