From 93c6df0817279bb7e424f3d77059a10ea1412610 Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Thu, 9 Apr 2026 13:41:04 +0200
Subject: [PATCH 1/7] test: projection
---
.../1-projection/src/app/app.component.ts | 1 +
.../city-card/city-card.component.ts | 64 +++++++++++++++-
.../student-card/student-card.component.ts | 46 ++++++++++--
.../teacher-card/teacher-card.component.ts | 47 ++++++++++--
.../src/app/data-access/city.store.ts | 2 +-
.../src/app/ui/card/card.component.ts | 73 ++++++++++---------
.../app/ui/list-item/list-item.component.ts | 31 ++------
7 files changed, 188 insertions(+), 76 deletions(-)
diff --git a/apps/angular/1-projection/src/app/app.component.ts b/apps/angular/1-projection/src/app/app.component.ts
index df654bbc2..75dc670bc 100644
--- a/apps/angular/1-projection/src/app/app.component.ts
+++ b/apps/angular/1-projection/src/app/app.component.ts
@@ -5,6 +5,7 @@ import { TeacherCardComponent } from './component/teacher-card/teacher-card.comp
@Component({
selector: 'app-root',
+ standalone: true,
template: `
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..867a70e92 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,65 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { NgOptimizedImage } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ OnInit,
+ ViewEncapsulation,
+} from '@angular/core';
+import { CityStore } from '../../data-access/city.store';
+import {
+ FakeHttpService,
+ randomCity,
+} from '../../data-access/fake-http.service';
+import { CardComponent, CardItemDirective } from '../../ui/card/card.component';
+import { ListItemComponent } from '../../ui/list-item/list-item.component';
@Component({
selector: 'app-city-card',
- template: 'TODO City',
- imports: [],
+ standalone: true,
+ template: `
+
+
+
+
+ {{ city.name }}
+
+
+
+ `,
+ styles: [
+ `
+ .bg-pink {
+ background-color: rgba(111, 20, 20, 0.5);
+ }
+ `,
+ ],
+ imports: [
+ CardComponent,
+ CardItemDirective,
+ NgOptimizedImage,
+ ListItemComponent,
+ ],
+ encapsulation: ViewEncapsulation.None,
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((c) => this.store.addAll(c));
+ }
+ addNewItem() {
+ this.store.addOne(randomCity());
+ }
+
+ deleteItem(id: number) {
+ this.store.deleteOne(id);
+ }
+}
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..15e367110 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
@@ -1,30 +1,56 @@
+import { NgOptimizedImage } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
+ ViewEncapsulation,
} from '@angular/core';
-import { FakeHttpService } from '../../data-access/fake-http.service';
+import {
+ FakeHttpService,
+ randStudent,
+} 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 { CardComponent, CardItemDirective } from '../../ui/card/card.component';
+import { ListItemComponent } from '../../ui/list-item/list-item.component';
@Component({
selector: 'app-student-card',
+ standalone: true,
template: `
+ (added)="addNewItem()"
+ customClass="bg-light-green">
+
![student]()
+
+
+
+ {{ student.firstName }}
+
+
+
`,
styles: [
`
- ::ng-deep .bg-light-green {
+ .bg-light-green {
background-color: rgba(0, 250, 0, 0.1);
}
`,
],
- imports: [CardComponent],
+ imports: [
+ CardComponent,
+ CardItemDirective,
+ NgOptimizedImage,
+ ListItemComponent,
+ ],
+ encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StudentCardComponent implements OnInit {
@@ -37,4 +63,12 @@ export class StudentCardComponent implements OnInit {
ngOnInit(): void {
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
}
+
+ addNewItem() {
+ this.store.addOne(randStudent());
+ }
+
+ deleteItem(id: number) {
+ this.store.deleteOne(id);
+ }
}
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..3de6d98fa 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
@@ -1,25 +1,50 @@
-import { Component, inject, OnInit } from '@angular/core';
-import { FakeHttpService } from '../../data-access/fake-http.service';
+import { NgOptimizedImage } from '@angular/common';
+import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
+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 { CardComponent, CardItemDirective } from '../../ui/card/card.component';
+import { ListItemComponent } from '../../ui/list-item/list-item.component';
@Component({
selector: 'app-teacher-card',
+ standalone: true,
template: `
+ (added)="addNewItem()"
+ customClass="bg-light-red">
+
![teacher]()
+
+
+ {{ teacher.firstName }}
+
+
+
`,
styles: [
`
- ::ng-deep .bg-light-red {
+ .bg-light-red {
background-color: rgba(250, 0, 0, 0.1);
}
`,
],
- imports: [CardComponent],
+ encapsulation: ViewEncapsulation.None,
+ imports: [
+ CardComponent,
+ CardItemDirective,
+ NgOptimizedImage,
+ ListItemComponent,
+ ],
})
export class TeacherCardComponent implements OnInit {
private http = inject(FakeHttpService);
@@ -31,4 +56,12 @@ export class TeacherCardComponent implements OnInit {
ngOnInit(): void {
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
}
+
+ addNewItem() {
+ this.store.addOne(randTeacher());
+ }
+
+ deleteItem(id: number) {
+ this.store.deleteOne(id);
+ }
}
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 166d9c49a..2832dfbcc 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,31 +1,38 @@
-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 { NgTemplateOutlet } from '@angular/common';
+import {
+ Component,
+ contentChild,
+ Directive,
+ input,
+ output,
+ TemplateRef,
+} from '@angular/core';
+
+@Directive({
+ selector: 'ng-template[cardItem]',
+ standalone: true,
+})
+export class CardItemDirective {}
@Component({
selector: 'app-card',
+ standalone: true,
template: `
- @if (type() === CardType.TEACHER) {
-
![]()
- }
- @if (type() === CardType.STUDENT) {
-
![]()
- }
+
- @for (item of list(); track item) {
-
- }
+
+ @for (item of list(); track item) {
+
+ }
+
`,
- imports: [ListItemComponent, NgOptimizedImage],
+ imports: [NgTemplateOutlet],
})
-export class CardComponent {
- private teacherStore = inject(TeacherStore);
- private studentStore = inject(StudentStore);
-
- readonly list = input(null);
- readonly type = input.required();
+export class CardComponent {
+ readonly list = input([]);
readonly customClass = input('');
-
- CardType = CardType;
+ readonly added = output();
+ readonly deleted = output();
+ readonly itemTemplate = contentChild.required(CardItemDirective, {
+ read: TemplateRef,
+ });
addNewItem() {
- const type = this.type();
- if (type === CardType.TEACHER) {
- this.teacherStore.addOne(randTeacher());
- } else if (type === CardType.STUDENT) {
- this.studentStore.addOne(randStudent());
- }
+ this.added.emit();
+ }
+
+ deleteItem(event: number) {
+ this.deleted.emit(event);
}
}
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 c04a29c05..a4d8b9d1e 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
@@ -1,19 +1,12 @@
-import {
- ChangeDetectionStrategy,
- Component,
- inject,
- input,
-} from '@angular/core';
-import { StudentStore } from '../../data-access/student.store';
-import { TeacherStore } from '../../data-access/teacher.store';
-import { CardType } from '../../model/card.model';
+import { ChangeDetectionStrategy, Component, output } from '@angular/core';
@Component({
selector: 'app-list-item',
+ standalone: true,
template: `
- {{ name() }}
-
@@ -21,19 +14,9 @@ import { CardType } from '../../model/card.model';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ListItemComponent {
- private teacherStore = inject(TeacherStore);
- private studentStore = inject(StudentStore);
+ readonly deleted = output();
- readonly id = input.required();
- readonly name = input.required();
- readonly type = input.required();
-
- delete(id: number) {
- const type = this.type();
- if (type === CardType.TEACHER) {
- this.teacherStore.deleteOne(id);
- } else if (type === CardType.STUDENT) {
- this.studentStore.deleteOne(id);
- }
+ delete() {
+ this.deleted.emit();
}
}
From 427cf40d72ace0ee4f7f0ea44a3eadda818bdc48 Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Sat, 11 Apr 2026 14:01:17 +0200
Subject: [PATCH 2/7] test: crud addplication
---
apps/angular/5-crud-application/api.config.ts | 16 +++
apps/angular/5-crud-application/app.config.ts | 7 ++
apps/angular/5-crud-application/project.json | 4 +
.../5-crud-application/proxy.conf.json | 11 ++
.../src/app/app.component.ts | 69 +++++------
.../5-crud-application/src/app/app.config.ts | 3 +-
.../src/app/core/provide-api-config.ts | 18 +++
.../5-crud-application/src/app/todo.model.ts | 6 +
.../src/app/todo.service.spec.ts | 84 ++++++++++++++
.../src/app/todo.service.ts | 107 ++++++++++++++++++
apps/angular/5-crud-application/src/main.ts | 5 +-
11 files changed, 292 insertions(+), 38 deletions(-)
create mode 100644 apps/angular/5-crud-application/api.config.ts
create mode 100644 apps/angular/5-crud-application/app.config.ts
create mode 100644 apps/angular/5-crud-application/proxy.conf.json
create mode 100644 apps/angular/5-crud-application/src/app/core/provide-api-config.ts
create mode 100644 apps/angular/5-crud-application/src/app/todo.model.ts
create mode 100644 apps/angular/5-crud-application/src/app/todo.service.spec.ts
create mode 100644 apps/angular/5-crud-application/src/app/todo.service.ts
diff --git a/apps/angular/5-crud-application/api.config.ts b/apps/angular/5-crud-application/api.config.ts
new file mode 100644
index 000000000..9955cdff0
--- /dev/null
+++ b/apps/angular/5-crud-application/api.config.ts
@@ -0,0 +1,16 @@
+import { InjectionToken, makeEnvironmentProviders } from '@angular/core';
+
+export interface ApiConfig {
+ baseUrl: string;
+}
+
+export const API_CONFIG = new InjectionToken('API_CONFIG');
+
+export function provideApiConfig(config: ApiConfig = { baseUrl: '/api' }) {
+ return makeEnvironmentProviders([
+ {
+ provide: API_CONFIG,
+ useValue: config,
+ },
+ ]);
+}
diff --git a/apps/angular/5-crud-application/app.config.ts b/apps/angular/5-crud-application/app.config.ts
new file mode 100644
index 000000000..f3d63a66e
--- /dev/null
+++ b/apps/angular/5-crud-application/app.config.ts
@@ -0,0 +1,7 @@
+import { provideHttpClient } from '@angular/common/http';
+import { ApplicationConfig } from '@angular/core';
+import { provideApiConfig } from './api.config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideHttpClient(), provideApiConfig()],
+};
diff --git a/apps/angular/5-crud-application/project.json b/apps/angular/5-crud-application/project.json
index a718f02b6..6db9f53c6 100644
--- a/apps/angular/5-crud-application/project.json
+++ b/apps/angular/5-crud-application/project.json
@@ -56,6 +56,10 @@
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "buildTarget": "angular-crud-application:build:development",
+ "proxyConfig": "apps/angular/5-crud-application/proxy.conf.json"
+ },
"configurations": {
"production": {
"buildTarget": "angular-crud-application:build:production"
diff --git a/apps/angular/5-crud-application/proxy.conf.json b/apps/angular/5-crud-application/proxy.conf.json
new file mode 100644
index 000000000..4bed70c4c
--- /dev/null
+++ b/apps/angular/5-crud-application/proxy.conf.json
@@ -0,0 +1,11 @@
+{
+ "/api": {
+ "target": "https://jsonplaceholder.typicode.com",
+ "secure": false,
+ "changeOrigin": true,
+ "pathRewrite": {
+ "^/api": ""
+ },
+ "logLevel": "debug"
+ }
+}
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..193e6bfe0 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,50 @@
-import { HttpClient } from '@angular/common/http';
-import { Component, inject, OnInit } from '@angular/core';
+import { Component, computed, inject } from '@angular/core';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { randText } from '@ngneat/falso';
+import { Todo } from './todo.model';
+import { TodoService } from './todo.service';
@Component({
- imports: [],
+ imports: [MatProgressSpinnerModule],
selector: 'app-root',
template: `
- @for (todo of todos; track todo.id) {
- {{ todo.title }}
- Update
+ @if (loadingState()) {
+ <
+
+ } @else if (errorState()) {
+ {{ errorState() }}
+ } @else {
+ Todo List:
+
+ @for (todo of todoList(); track todo.id) {
+ -
+ {{ todo.title }}
+ Update
+ delete
+
+ }
+
}
`,
styles: [],
})
-export class AppComponent implements OnInit {
- private http = inject(HttpClient);
-
- todos!: any[];
+export class AppComponent {
+ private readonly todoService = inject(TodoService);
+ readonly todos = this.todoService.todos;
+ protected readonly todoList = computed(() => this.todoService.todos() || []);
+ loadingState = this.todoService.loadingState;
+ errorState = this.todoService.errorState;
ngOnInit(): void {
- this.http
- .get('https://jsonplaceholder.typicode.com/todos')
- .subscribe((todos) => {
- this.todos = todos;
- });
+ this.todoService.loadTodos();
+ }
+
+ update(todo: Todo): void {
+ const updatedTodo = { ...todo, title: randText() };
+ this.todoService.updateTodo(updatedTodo);
}
- 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(id: number): void {
+ this.todoService.deleteTodo(id);
}
}
diff --git a/apps/angular/5-crud-application/src/app/app.config.ts b/apps/angular/5-crud-application/src/app/app.config.ts
index 1c0c9422f..42fef0c5b 100644
--- a/apps/angular/5-crud-application/src/app/app.config.ts
+++ b/apps/angular/5-crud-application/src/app/app.config.ts
@@ -1,6 +1,7 @@
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
+import { provideApiConfig } from './core/provide-api-config';
export const appConfig: ApplicationConfig = {
- providers: [provideHttpClient()],
+ providers: [provideHttpClient(), provideApiConfig()],
};
diff --git a/apps/angular/5-crud-application/src/app/core/provide-api-config.ts b/apps/angular/5-crud-application/src/app/core/provide-api-config.ts
new file mode 100644
index 000000000..bf979476a
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/core/provide-api-config.ts
@@ -0,0 +1,18 @@
+import { InjectionToken, makeEnvironmentProviders } from '@angular/core';
+
+export interface ApiConfig {
+ baseUrl: string;
+}
+
+export const API_CONFIG = new InjectionToken('API_CONFIG');
+
+export function provideApiConfig() {
+ return makeEnvironmentProviders([
+ {
+ provide: API_CONFIG,
+ useValue: {
+ baseUrl: '/api',
+ } satisfies ApiConfig,
+ },
+ ]);
+}
diff --git a/apps/angular/5-crud-application/src/app/todo.model.ts b/apps/angular/5-crud-application/src/app/todo.model.ts
new file mode 100644
index 000000000..db525ce1d
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/todo.model.ts
@@ -0,0 +1,6 @@
+export interface Todo {
+ userId: number;
+ id: number;
+ title: string;
+ completed: boolean;
+}
diff --git a/apps/angular/5-crud-application/src/app/todo.service.spec.ts b/apps/angular/5-crud-application/src/app/todo.service.spec.ts
new file mode 100644
index 000000000..b886192dd
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/todo.service.spec.ts
@@ -0,0 +1,84 @@
+import { provideHttpClient } from '@angular/common/http';
+import {
+ HttpTestingController,
+ provideHttpClientTesting,
+} from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { provideApiConfig } from './core/provide-api-config';
+import { Todo } from './todo.model';
+import { TodoService } from './todo.service';
+
+describe('TodoService', () => {
+ let service: TodoService;
+ let httpTesting: HttpTestingController;
+
+ const todos: Todo[] = [
+ { id: 1, userId: 1, title: 'First todo', completed: false },
+ { id: 2, userId: 1, title: 'Second todo', completed: false },
+ { id: 3, userId: 1, title: 'Third todo', completed: true },
+ ];
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ provideApiConfig(),
+ ],
+ });
+
+ service = TestBed.inject(TodoService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should load todos', () => {
+ service.loadTodos();
+
+ const request = httpTesting.expectOne('/api/todos');
+
+ expect(request.request.method).toBe('GET');
+
+ request.flush(todos);
+
+ expect(service.todos()).toEqual(todos);
+ expect(service.loadingState()).toBe(false);
+ expect(service.errorState()).toBeNull();
+ });
+
+ it('should update a todo without changing the list order', () => {
+ service.setTodo(todos);
+
+ const updatedTodo: Todo = {
+ ...todos[1],
+ title: 'Updated todo',
+ };
+
+ service.updateTodo(updatedTodo);
+
+ const request = httpTesting.expectOne('/api/todos/2');
+
+ expect(request.request.method).toBe('PUT');
+
+ request.flush(updatedTodo);
+
+ expect(service.todos()).toEqual([todos[0], updatedTodo, todos[2]]);
+ });
+
+ it('should delete a todo', () => {
+ service.setTodo(todos);
+
+ service.deleteTodo(2);
+
+ const request = httpTesting.expectOne('/api/todos/2');
+
+ expect(request.request.method).toBe('DELETE');
+
+ request.flush(null);
+
+ expect(service.todos()).toEqual([todos[0], todos[2]]);
+ });
+});
diff --git a/apps/angular/5-crud-application/src/app/todo.service.ts b/apps/angular/5-crud-application/src/app/todo.service.ts
new file mode 100644
index 000000000..b62ece0e0
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/todo.service.ts
@@ -0,0 +1,107 @@
+import { HttpClient } from '@angular/common/http';
+import { inject, Injectable, signal } from '@angular/core';
+import { API_CONFIG } from './core/provide-api-config';
+import { Todo } from './todo.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TodoService {
+ private readonly http = inject(HttpClient);
+ private readonly apiConfig = inject(API_CONFIG);
+ private readonly apiUrl = `${this.apiConfig.baseUrl}/todos`;
+ readonly todoList = signal(null);
+ readonly loadingState = signal(false);
+ readonly errorState = signal(null);
+ private readonly processingIds = signal>(new Set());
+
+ readonly todos = this.todoList.asReadonly();
+
+ loadTodos(): void {
+ this.loadingState.set(true);
+ this.errorState.set(null);
+ this.http.get(this.apiUrl).subscribe({
+ next: (todos) => {
+ this.todoList.set(todos);
+ this.loadingState.set(false);
+ },
+ error: (error) => {
+ this.errorState.set('Failed to load todos');
+ this.loadingState.set(false);
+ },
+ });
+ }
+
+ updateTodo(todo: Todo): void {
+ this.loadingState.set(true);
+ this.setProcessing(todo.id, true);
+ this.http
+ .put(`${this.apiUrl}/${todo.id}`, todo, {
+ headers: {
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
+ })
+ .subscribe({
+ next: (updatedTodo) => {
+ const currentTodos = this.todoList();
+ if (currentTodos) {
+ const index = currentTodos.findIndex(
+ (t) => t.id === updatedTodo.id,
+ );
+ if (index !== -1) {
+ const updatedTodos = [...currentTodos];
+ updatedTodos[index] = updatedTodo;
+ this.todoList.set(updatedTodos);
+ }
+ }
+ this.setProcessing(todo.id, false);
+ this.loadingState.set(false);
+ },
+ error: () => {
+ this.errorState.set('Failed to update todo');
+ this.setProcessing(todo.id, false);
+ },
+ });
+ }
+
+ deleteTodo(id: number): void {
+ this.setProcessing(id, true);
+ this.loadingState.set(true);
+ this.http.delete(`${this.apiUrl}/${id}`).subscribe({
+ next: () => {
+ const currentTodos = this.todoList();
+ if (currentTodos) {
+ this.todoList.set(currentTodos.filter((t) => t.id !== id));
+ }
+ this.setProcessing(id, false);
+ this.loadingState.set(false);
+ },
+ error: () => {
+ this.errorState.set('Failed to delete todo');
+ this.setProcessing(id, false);
+ },
+ });
+ }
+
+ setTodo(userValue: Todo[] | null): void {
+ this.todoList.set(userValue);
+ }
+
+ isProcessing(id: number): boolean {
+ return this.processingIds().has(id);
+ }
+
+ private setProcessing(id: number, processing: boolean): void {
+ this.processingIds.update((ids) => {
+ const nextIds = new Set(ids);
+
+ if (processing) {
+ nextIds.add(id);
+ } else {
+ nextIds.delete(id);
+ }
+
+ return nextIds;
+ });
+ }
+}
diff --git a/apps/angular/5-crud-application/src/main.ts b/apps/angular/5-crud-application/src/main.ts
index 866d45959..ed35c8ca6 100644
--- a/apps/angular/5-crud-application/src/main.ts
+++ b/apps/angular/5-crud-application/src/main.ts
@@ -1,10 +1,9 @@
import { provideZoneChangeDetection } from '@angular/core';
-import { appConfig } from './app/app.config';
-
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, {
...appConfig,
- providers: [provideZoneChangeDetection(), ...appConfig.providers],
+ providers: [provideZoneChangeDetection(), ...(appConfig.providers ?? [])],
}).catch((err) => console.error(err));
From 3b1786a98575af4d11338117e44adfff93d73069 Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Mon, 13 Apr 2026 16:04:29 +0200
Subject: [PATCH 3/7] test: 8-Pure Pipe
---
.../8-pure-pipe/src/app/app.component.ts | 10 +++-------
.../src/app/heavy-computation.pipe.spec.ts | 20 +++++++++++++++++++
.../src/app/heavy-computation.pipe.ts | 16 +++++++++++++++
3 files changed, 39 insertions(+), 7 deletions(-)
create mode 100644 apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.spec.ts
create mode 100644 apps/angular/8-pure-pipe/src/app/heavy-computation.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..3623b8ef5 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,14 @@
import { Component } from '@angular/core';
-
+import { HeavyComputationPipe } from './heavy-computation.pipe';
@Component({
+ imports: [HeavyComputationPipe],
selector: 'app-root',
template: `
@for (person of persons; track person) {
- {{ heavyComputation(person, $index) }}
+ {{ person | heavyComputation: $index }}
}
`,
})
export class AppComponent {
persons = ['toto', 'jack'];
-
- heavyComputation(name: string, index: number) {
- // very heavy computation
- return `${name} - ${index}`;
- }
}
diff --git a/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.spec.ts b/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.spec.ts
new file mode 100644
index 000000000..adea7e6fe
--- /dev/null
+++ b/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.spec.ts
@@ -0,0 +1,20 @@
+import { HeavyComputationPipe } from './heavy-computation.pipe';
+
+describe('HeavyComputationPipe', () => {
+ let pipe: HeavyComputationPipe;
+ beforeEach(() => {
+ pipe = new HeavyComputationPipe();
+ });
+ it('create an instance', () => {
+ expect(pipe).toBeTruthy();
+ });
+
+ it('should return formatted name with index', () => {
+ expect(pipe.transform('toto', 0)).toBe('toto - 0');
+ expect(pipe.transform('jack', 1)).toBe('jack - 1');
+ });
+
+ it('should return the original value when value is not a string', () => {
+ expect(pipe.transform(123, 0)).toBe(123);
+ });
+});
diff --git a/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.ts b/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.ts
new file mode 100644
index 000000000..993645589
--- /dev/null
+++ b/apps/angular/8-pure-pipe/src/app/heavy-computation.pipe.ts
@@ -0,0 +1,16 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'heavyComputation',
+})
+export class HeavyComputationPipe implements PipeTransform {
+ transform(value: unknown, ...args: unknown[]): unknown {
+ if (typeof value === 'string' && typeof args[0] === 'number') {
+ const name = value;
+ const index = args[0];
+ // very heavy computation
+ return `${name} - ${index}`;
+ }
+ return value;
+ }
+}
From a014efaa80c3e31e0d099493c78b09b6d2c8bbd3 Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:03:58 +0200
Subject: [PATCH 4/7] test: 21 anchor scrolling with RouterLink
---
.../21-anchor-navigation/src/app/app.config.ts | 12 ++++++++++--
.../21-anchor-navigation/src/app/foo.component.ts | 2 +-
.../src/app/nav-button.component.ts | 11 +++++++++--
3 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/apps/angular/21-anchor-navigation/src/app/app.config.ts b/apps/angular/21-anchor-navigation/src/app/app.config.ts
index 66ab7f73f..ba519ab03 100644
--- a/apps/angular/21-anchor-navigation/src/app/app.config.ts
+++ b/apps/angular/21-anchor-navigation/src/app/app.config.ts
@@ -1,6 +1,14 @@
import { ApplicationConfig } from '@angular/core';
-import { provideRouter } from '@angular/router';
+import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
- providers: [provideRouter(appRoutes)],
+ providers: [
+ provideRouter(
+ appRoutes,
+ withInMemoryScrolling({
+ anchorScrolling: 'enabled',
+ scrollPositionRestoration: 'enabled',
+ }),
+ ),
+ ],
};
diff --git a/apps/angular/21-anchor-navigation/src/app/foo.component.ts b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
index 6744c3662..f85d4d1f8 100644
--- a/apps/angular/21-anchor-navigation/src/app/foo.component.ts
+++ b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
@@ -6,7 +6,7 @@ import { NavButtonComponent } from './nav-button.component';
selector: 'app-foo',
template: `
Welcome to foo page
- Home Page
+ Home Page
section 1
section 2
`,
diff --git a/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
index 7a22c7f38..8a2e9bc7a 100644
--- a/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
+++ b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
@@ -1,10 +1,12 @@
/* eslint-disable @angular-eslint/component-selector */
-import { Component, input } from '@angular/core';
+import { Component, computed, input } from '@angular/core';
+import { RouterLinkWithHref } from '@angular/router';
@Component({
selector: 'nav-button',
+ imports: [RouterLinkWithHref],
template: `
-
+
`,
@@ -14,4 +16,9 @@ import { Component, input } from '@angular/core';
})
export class NavButtonComponent {
href = input('');
+ isAnchor = computed(() =>
+ this.href().startsWith('#') ? this.href().substring(1) : '',
+ );
+ link = computed(() => (this.isAnchor() ? [] : this.href()));
+ fragment = computed(() => (this.isAnchor() ? this.isAnchor() : undefined));
}
From 1fa96d03c6f98981d5c49522c54154b2130faf8d Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:24:11 +0200
Subject: [PATCH 5/7] test: RouterInput strategy
---
.../22-router-input/src/app/app.config.ts | 4 ++--
.../22-router-input/src/app/test.component.ts | 18 +++++++-----------
2 files changed, 9 insertions(+), 13 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..152adc1bf 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,17 @@
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']));
+ testId = input('');
+ permission = input('');
+ user = input('');
}
From ffb703c6c981aaab9cbb305ddaa33cc6c4ca1373 Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Tue, 14 Apr 2026 14:07:05 +0200
Subject: [PATCH 6/7] feat(31-module-to-standalone): 31-module-to-standalone
---
.../src/app/app.component.ts | 3 ++-
.../src/app/app.module.ts | 11 --------
.../31-module-to-standalone/src/main.ts | 19 ++++++++-----
.../admin/feature/src/index.ts | 2 +-
.../feature/src/lib/admin-feature.module.ts | 27 -------------------
.../feature/src/lib/admin-features.routes.ts | 18 +++++++++++++
.../lib/create-user/create-user.component.ts | 14 +++-------
.../src/lib/dashboard/dashboard.component.ts | 14 +++-------
.../forbidden/src/index.ts | 2 +-
.../forbidden/src/lib/forbidden.component.ts | 1 -
.../forbidden/src/lib/forbidden.module.ts | 13 ---------
.../forbidden/src/lib/forbidden.routes.ts | 6 +++++
libs/module-to-standalone/home/src/index.ts | 2 +-
.../home/src/lib/home.component.ts | 7 ++---
.../home/src/lib/home.module.ts | 13 ---------
.../home/src/lib/home.router.ts | 4 +++
libs/module-to-standalone/shell/src/index.ts | 2 +-
.../shell/src/lib/main-shell.module.ts | 11 --------
.../shell/src/lib/main-shell.routes.ts | 15 ++++++-----
.../contact/src/contact-feature.routes.ts | 18 +++++++++++++
.../user/contact/src/index.ts | 2 +-
.../contact/src/lib/contact-feature.module.ts | 27 -------------------
.../create-contact.component.ts | 6 ++---
.../src/lib/dashboard/dashboard.component.ts | 14 +++-------
.../user/home/src/index.ts | 2 +-
.../user/home/src/lib/home.component.ts | 1 -
.../user/home/src/lib/home.module.ts | 11 --------
.../user/home/src/lib/user.home.routes.ts | 9 +++++++
.../user/shell/src/index.ts | 2 +-
.../shell/src/lib/user-shell.component.ts | 3 ++-
.../user/shell/src/lib/user-shell.module.ts | 13 ---------
.../user/shell/src/lib/user-shell.routes.ts | 12 ++++++---
32 files changed, 112 insertions(+), 192 deletions(-)
delete mode 100644 apps/angular/31-module-to-standalone/src/app/app.module.ts
delete mode 100644 libs/module-to-standalone/admin/feature/src/lib/admin-feature.module.ts
create mode 100644 libs/module-to-standalone/admin/feature/src/lib/admin-features.routes.ts
delete mode 100644 libs/module-to-standalone/forbidden/src/lib/forbidden.module.ts
create mode 100644 libs/module-to-standalone/forbidden/src/lib/forbidden.routes.ts
delete mode 100644 libs/module-to-standalone/home/src/lib/home.module.ts
create mode 100644 libs/module-to-standalone/home/src/lib/home.router.ts
delete mode 100644 libs/module-to-standalone/shell/src/lib/main-shell.module.ts
create mode 100644 libs/module-to-standalone/user/contact/src/contact-feature.routes.ts
delete mode 100644 libs/module-to-standalone/user/contact/src/lib/contact-feature.module.ts
delete mode 100644 libs/module-to-standalone/user/home/src/lib/home.module.ts
create mode 100644 libs/module-to-standalone/user/home/src/lib/user.home.routes.ts
delete mode 100644 libs/module-to-standalone/user/shell/src/lib/user-shell.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..d96233615 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,6 @@ import { Component } from '@angular/core';
host: {
class: 'flex flex-col p-4 gap-3',
},
- standalone: false,
+ imports: [RouterLink, RouterOutlet],
})
export class AppComponent {}
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 8cd87d53b..916f78679 100644
--- a/apps/angular/31-module-to-standalone/src/main.ts
+++ b/apps/angular/31-module-to-standalone/src/main.ts
@@ -1,9 +1,14 @@
+import { provideToken } from '@angular-challenges/module-to-standalone/core/providers';
+import { appRoutes } from '@angular-challenges/module-to-standalone/shell';
import { provideZoneChangeDetection } from '@angular/core';
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-import { AppModule } from './app/app.module';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { provideRouter } from '@angular/router';
+import { AppComponent } from './app/app.component';
-platformBrowserDynamic()
- .bootstrapModule(AppModule, {
- applicationProviders: [provideZoneChangeDetection()],
- })
- .catch((err) => console.error(err));
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideZoneChangeDetection(),
+ provideRouter(appRoutes),
+ provideToken('main-shell-token'),
+ ],
+}).catch((err) => console.error(err));
diff --git a/libs/module-to-standalone/admin/feature/src/index.ts b/libs/module-to-standalone/admin/feature/src/index.ts
index 76c8330f9..cda67dde5 100644
--- a/libs/module-to-standalone/admin/feature/src/index.ts
+++ b/libs/module-to-standalone/admin/feature/src/index.ts
@@ -1 +1 @@
-export * from './lib/admin-feature.module';
+export * from './lib/admin-features.routes';
diff --git a/libs/module-to-standalone/admin/feature/src/lib/admin-feature.module.ts b/libs/module-to-standalone/admin/feature/src/lib/admin-feature.module.ts
deleted file mode 100644
index 84e893eeb..000000000
--- a/libs/module-to-standalone/admin/feature/src/lib/admin-feature.module.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-@NgModule({
- declarations: [],
- imports: [
- CommonModule,
- RouterModule.forChild([
- {
- path: '',
- loadChildren: () =>
- import('./dashboard/dashboard.component').then(
- (m) => m.DashboardModule,
- ),
- },
- {
- path: 'create-user',
- loadChildren: () =>
- import('./create-user/create-user.component').then(
- (m) => m.CreateUserModule,
- ),
- },
- ]),
- ],
-})
-export class AdminFeatureModule {}
diff --git a/libs/module-to-standalone/admin/feature/src/lib/admin-features.routes.ts b/libs/module-to-standalone/admin/feature/src/lib/admin-features.routes.ts
new file mode 100644
index 000000000..eb203224d
--- /dev/null
+++ b/libs/module-to-standalone/admin/feature/src/lib/admin-features.routes.ts
@@ -0,0 +1,18 @@
+import { Route } from '@angular/router';
+
+export const adminFeatureRoutes: Route[] = [
+ {
+ path: '',
+ loadComponent: () =>
+ import('./dashboard/dashboard.component').then(
+ (m) => m.DashboardComponent,
+ ),
+ },
+ {
+ path: 'create-user',
+ loadComponent: () =>
+ import('./create-user/create-user.component').then(
+ (m) => m.CreateUserComponent,
+ ),
+ },
+];
diff --git a/libs/module-to-standalone/admin/feature/src/lib/create-user/create-user.component.ts b/libs/module-to-standalone/admin/feature/src/lib/create-user/create-user.component.ts
index 6a2d4cfd4..d3087f358 100644
--- a/libs/module-to-standalone/admin/feature/src/lib/create-user/create-user.component.ts
+++ b/libs/module-to-standalone/admin/feature/src/lib/create-user/create-user.component.ts
@@ -1,5 +1,5 @@
-import { Component, NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
@Component({
selector: 'lib-create-user',
@@ -12,14 +12,6 @@ import { RouterModule } from '@angular/router';
Back
`,
- standalone: false,
+ imports: [RouterLink],
})
export class CreateUserComponent {}
-
-@NgModule({
- imports: [
- RouterModule.forChild([{ path: '', component: CreateUserComponent }]),
- ],
- declarations: [CreateUserComponent],
-})
-export class CreateUserModule {}
diff --git a/libs/module-to-standalone/admin/feature/src/lib/dashboard/dashboard.component.ts b/libs/module-to-standalone/admin/feature/src/lib/dashboard/dashboard.component.ts
index 801efb520..ed5014701 100644
--- a/libs/module-to-standalone/admin/feature/src/lib/dashboard/dashboard.component.ts
+++ b/libs/module-to-standalone/admin/feature/src/lib/dashboard/dashboard.component.ts
@@ -1,5 +1,5 @@
-import { Component, NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
@Component({
selector: 'lib-dashboard',
@@ -12,14 +12,6 @@ import { RouterModule } from '@angular/router';
Create User
`,
- standalone: false,
+ imports: [RouterLink],
})
export class DashboardComponent {}
-
-@NgModule({
- imports: [
- RouterModule.forChild([{ path: '', component: DashboardComponent }]),
- ],
- declarations: [DashboardComponent],
-})
-export class DashboardModule {}
diff --git a/libs/module-to-standalone/forbidden/src/index.ts b/libs/module-to-standalone/forbidden/src/index.ts
index 672622f64..8057feea2 100644
--- a/libs/module-to-standalone/forbidden/src/index.ts
+++ b/libs/module-to-standalone/forbidden/src/index.ts
@@ -1 +1 @@
-export * from './lib/forbidden.module';
+export * from './lib/forbidden.routes';
diff --git a/libs/module-to-standalone/forbidden/src/lib/forbidden.component.ts b/libs/module-to-standalone/forbidden/src/lib/forbidden.component.ts
index a5e6e2d77..755cd1516 100644
--- a/libs/module-to-standalone/forbidden/src/lib/forbidden.component.ts
+++ b/libs/module-to-standalone/forbidden/src/lib/forbidden.component.ts
@@ -5,6 +5,5 @@ import { Component } from '@angular/core';
template: `
Forbidden component
`,
- standalone: false,
})
export class ForbiddenComponent {}
diff --git a/libs/module-to-standalone/forbidden/src/lib/forbidden.module.ts b/libs/module-to-standalone/forbidden/src/lib/forbidden.module.ts
deleted file mode 100644
index 0b363bf04..000000000
--- a/libs/module-to-standalone/forbidden/src/lib/forbidden.module.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { ForbiddenComponent } from './forbidden.component';
-
-@NgModule({
- declarations: [ForbiddenComponent],
- imports: [
- CommonModule,
- RouterModule.forChild([{ path: '', component: ForbiddenComponent }]),
- ],
-})
-export class ForbiddenModule {}
diff --git a/libs/module-to-standalone/forbidden/src/lib/forbidden.routes.ts b/libs/module-to-standalone/forbidden/src/lib/forbidden.routes.ts
new file mode 100644
index 000000000..777c66446
--- /dev/null
+++ b/libs/module-to-standalone/forbidden/src/lib/forbidden.routes.ts
@@ -0,0 +1,6 @@
+import { Routes } from '@angular/router';
+import { ForbiddenComponent } from './forbidden.component';
+
+export const forbiddenRoutes: Routes = [
+ { path: '', component: ForbiddenComponent },
+];
diff --git a/libs/module-to-standalone/home/src/index.ts b/libs/module-to-standalone/home/src/index.ts
index fe97ad579..8d2233ecb 100644
--- a/libs/module-to-standalone/home/src/index.ts
+++ b/libs/module-to-standalone/home/src/index.ts
@@ -1 +1 @@
-export * from './lib/home.module';
+export * from './lib/home.router';
diff --git a/libs/module-to-standalone/home/src/lib/home.component.ts b/libs/module-to-standalone/home/src/lib/home.component.ts
index bf350d47d..b6f73e25a 100644
--- a/libs/module-to-standalone/home/src/lib/home.component.ts
+++ b/libs/module-to-standalone/home/src/lib/home.component.ts
@@ -1,5 +1,6 @@
import { TOKEN } from '@angular-challenges/module-to-standalone/core/providers';
import { AuthorizationService } from '@angular-challenges/module-to-standalone/core/service';
+import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
@Component({
@@ -9,10 +10,10 @@ import { Component, inject } from '@angular/core';
Authorization :
-
+
Authorize
-
+
Forbid
(isAuthorized: {{ authorizeService.isAuthorized$ | async }})
@@ -20,7 +21,7 @@ import { Component, inject } from '@angular/core';
`,
- standalone: false,
+ imports: [AsyncPipe],
})
export class HomeComponent {
public authorizeService = inject(AuthorizationService);
diff --git a/libs/module-to-standalone/home/src/lib/home.module.ts b/libs/module-to-standalone/home/src/lib/home.module.ts
deleted file mode 100644
index 30ae1ea8a..000000000
--- a/libs/module-to-standalone/home/src/lib/home.module.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { HomeComponent } from './home.component';
-
-@NgModule({
- declarations: [HomeComponent],
- imports: [
- RouterModule.forChild([{ path: '', component: HomeComponent }]),
- CommonModule,
- ],
-})
-export class ModuleToStandaloneHomeModule {}
diff --git a/libs/module-to-standalone/home/src/lib/home.router.ts b/libs/module-to-standalone/home/src/lib/home.router.ts
new file mode 100644
index 000000000..7fca2c06e
--- /dev/null
+++ b/libs/module-to-standalone/home/src/lib/home.router.ts
@@ -0,0 +1,4 @@
+import { Routes } from '@angular/router';
+import { HomeComponent } from './home.component';
+
+export const homeRoutes: Routes = [{ path: '', component: HomeComponent }];
diff --git a/libs/module-to-standalone/shell/src/index.ts b/libs/module-to-standalone/shell/src/index.ts
index 494d4096f..902420448 100644
--- a/libs/module-to-standalone/shell/src/index.ts
+++ b/libs/module-to-standalone/shell/src/index.ts
@@ -1 +1 @@
-export * from './lib/main-shell.module';
+export * from './lib/main-shell.routes';
diff --git a/libs/module-to-standalone/shell/src/lib/main-shell.module.ts b/libs/module-to-standalone/shell/src/lib/main-shell.module.ts
deleted file mode 100644
index e886da911..000000000
--- a/libs/module-to-standalone/shell/src/lib/main-shell.module.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { provideToken } from '@angular-challenges/module-to-standalone/core/providers';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { appRoutes } from './main-shell.routes';
-
-@NgModule({
- imports: [RouterModule.forRoot(appRoutes)],
- exports: [RouterModule],
- providers: [provideToken('main-shell-token')],
-})
-export class MainShellModule {}
diff --git a/libs/module-to-standalone/shell/src/lib/main-shell.routes.ts b/libs/module-to-standalone/shell/src/lib/main-shell.routes.ts
index 84f9a8d03..6f52a5f60 100644
--- a/libs/module-to-standalone/shell/src/lib/main-shell.routes.ts
+++ b/libs/module-to-standalone/shell/src/lib/main-shell.routes.ts
@@ -2,12 +2,16 @@ import { IsAuthorizedGuard } from '@angular-challenges/module-to-standalone/admi
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
- { path: '', redirectTo: 'home', pathMatch: 'full' },
+ {
+ path: '',
+ redirectTo: 'home',
+ pathMatch: 'full',
+ },
{
path: 'home',
loadChildren: () =>
import('@angular-challenges/module-to-standalone/home').then(
- (m) => m.ModuleToStandaloneHomeModule,
+ (m) => m.homeRoutes,
),
},
{
@@ -15,22 +19,21 @@ export const appRoutes: Route[] = [
canActivate: [IsAuthorizedGuard],
loadChildren: () =>
import('@angular-challenges/module-to-standalone/admin/feature').then(
- (m) => m.AdminFeatureModule,
+ (m) => m.adminFeatureRoutes,
),
},
{
path: 'user',
loadChildren: () =>
import('@angular-challenges/module-to-standalone/user/shell').then(
- (m) => m.UserShellModule,
+ (m) => m.userShellRoutes,
),
},
-
{
path: 'forbidden',
loadChildren: () =>
import('@angular-challenges/module-to-standalone/forbidden').then(
- (m) => m.ForbiddenModule,
+ (m) => m.forbiddenRoutes,
),
},
];
diff --git a/libs/module-to-standalone/user/contact/src/contact-feature.routes.ts b/libs/module-to-standalone/user/contact/src/contact-feature.routes.ts
new file mode 100644
index 000000000..155b7ce74
--- /dev/null
+++ b/libs/module-to-standalone/user/contact/src/contact-feature.routes.ts
@@ -0,0 +1,18 @@
+import { Routes } from '@angular/router';
+
+export const contactFeatureRoutes: Routes = [
+ {
+ path: '',
+ loadComponent: () =>
+ import('./lib/dashboard/dashboard.component').then(
+ (m) => m.ContactDashboardComponent,
+ ),
+ },
+ {
+ path: 'create-contact',
+ loadComponent: () =>
+ import('./lib/create-contact/create-contact.component').then(
+ (m) => m.CreateContactComponent,
+ ),
+ },
+];
diff --git a/libs/module-to-standalone/user/contact/src/index.ts b/libs/module-to-standalone/user/contact/src/index.ts
index 8561ea519..c3f79ba6e 100644
--- a/libs/module-to-standalone/user/contact/src/index.ts
+++ b/libs/module-to-standalone/user/contact/src/index.ts
@@ -1 +1 @@
-export * from './lib/contact-feature.module';
+export * from './contact-feature.routes';
diff --git a/libs/module-to-standalone/user/contact/src/lib/contact-feature.module.ts b/libs/module-to-standalone/user/contact/src/lib/contact-feature.module.ts
deleted file mode 100644
index 09da407c7..000000000
--- a/libs/module-to-standalone/user/contact/src/lib/contact-feature.module.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-@NgModule({
- declarations: [],
- imports: [
- CommonModule,
- RouterModule.forChild([
- {
- path: '',
- loadChildren: () =>
- import('./dashboard/dashboard.component').then(
- (m) => m.ContactDashboardModule,
- ),
- },
- {
- path: 'create-contact',
- loadChildren: () =>
- import('./create-contact/create-contact.component').then(
- (m) => m.CreateContactModule,
- ),
- },
- ]),
- ],
-})
-export class ContactFeatureModule {}
diff --git a/libs/module-to-standalone/user/contact/src/lib/create-contact/create-contact.component.ts b/libs/module-to-standalone/user/contact/src/lib/create-contact/create-contact.component.ts
index 9c5cea9ff..963eb84b1 100644
--- a/libs/module-to-standalone/user/contact/src/lib/create-contact/create-contact.component.ts
+++ b/libs/module-to-standalone/user/contact/src/lib/create-contact/create-contact.component.ts
@@ -1,5 +1,5 @@
import { Component, NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { RouterLink, RouterModule } from '@angular/router';
@Component({
selector: 'lib-create-contact',
@@ -12,14 +12,14 @@ import { RouterModule } from '@angular/router';
Back
`,
- standalone: false,
+ imports: [RouterLink],
})
export class CreateContactComponent {}
@NgModule({
imports: [
RouterModule.forChild([{ path: '', component: CreateContactComponent }]),
+ CreateContactComponent,
],
- declarations: [CreateContactComponent],
})
export class CreateContactModule {}
diff --git a/libs/module-to-standalone/user/contact/src/lib/dashboard/dashboard.component.ts b/libs/module-to-standalone/user/contact/src/lib/dashboard/dashboard.component.ts
index 8e56b721a..5e05f94ec 100644
--- a/libs/module-to-standalone/user/contact/src/lib/dashboard/dashboard.component.ts
+++ b/libs/module-to-standalone/user/contact/src/lib/dashboard/dashboard.component.ts
@@ -1,5 +1,5 @@
-import { Component, NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
@Component({
selector: 'lib-contact-dashboard',
@@ -12,14 +12,6 @@ import { RouterModule } from '@angular/router';
Create contact
`,
- standalone: false,
+ imports: [RouterLink],
})
export class ContactDashboardComponent {}
-
-@NgModule({
- imports: [
- RouterModule.forChild([{ path: '', component: ContactDashboardComponent }]),
- ],
- declarations: [ContactDashboardComponent],
-})
-export class ContactDashboardModule {}
diff --git a/libs/module-to-standalone/user/home/src/index.ts b/libs/module-to-standalone/user/home/src/index.ts
index fe97ad579..0095f2e67 100644
--- a/libs/module-to-standalone/user/home/src/index.ts
+++ b/libs/module-to-standalone/user/home/src/index.ts
@@ -1 +1 @@
-export * from './lib/home.module';
+export * from './lib/user.home.routes';
diff --git a/libs/module-to-standalone/user/home/src/lib/home.component.ts b/libs/module-to-standalone/user/home/src/lib/home.component.ts
index 253c76622..d5472cb10 100644
--- a/libs/module-to-standalone/user/home/src/lib/home.component.ts
+++ b/libs/module-to-standalone/user/home/src/lib/home.component.ts
@@ -5,6 +5,5 @@ import { Component } from '@angular/core';
template: `
User Home component
`,
- standalone: false,
})
export class UserHomeComponent {}
diff --git a/libs/module-to-standalone/user/home/src/lib/home.module.ts b/libs/module-to-standalone/user/home/src/lib/home.module.ts
deleted file mode 100644
index ceeb49511..000000000
--- a/libs/module-to-standalone/user/home/src/lib/home.module.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { UserHomeComponent } from './home.component';
-
-@NgModule({
- declarations: [UserHomeComponent],
- imports: [
- RouterModule.forChild([{ path: '', component: UserHomeComponent }]),
- ],
-})
-export class UserHomeModule {}
diff --git a/libs/module-to-standalone/user/home/src/lib/user.home.routes.ts b/libs/module-to-standalone/user/home/src/lib/user.home.routes.ts
new file mode 100644
index 000000000..b3c837b2d
--- /dev/null
+++ b/libs/module-to-standalone/user/home/src/lib/user.home.routes.ts
@@ -0,0 +1,9 @@
+import { Routes } from '@angular/router';
+
+export const userHomeRoutes: Routes = [
+ {
+ path: '',
+ loadComponent: () =>
+ import('./home.component').then((m) => m.UserHomeComponent),
+ },
+];
diff --git a/libs/module-to-standalone/user/shell/src/index.ts b/libs/module-to-standalone/user/shell/src/index.ts
index 641fd6817..bcebb64da 100644
--- a/libs/module-to-standalone/user/shell/src/index.ts
+++ b/libs/module-to-standalone/user/shell/src/index.ts
@@ -1 +1 @@
-export * from './lib/user-shell.module';
+export * from './lib/user-shell.routes';
diff --git a/libs/module-to-standalone/user/shell/src/lib/user-shell.component.ts b/libs/module-to-standalone/user/shell/src/lib/user-shell.component.ts
index 558c4069f..4fa20c126 100644
--- a/libs/module-to-standalone/user/shell/src/lib/user-shell.component.ts
+++ b/libs/module-to-standalone/user/shell/src/lib/user-shell.component.ts
@@ -1,5 +1,6 @@
import { TOKEN } from '@angular-challenges/module-to-standalone/core/providers';
import { Component, inject } from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'lib-user-shell',
@@ -27,7 +28,7 @@ import { Component, inject } from '@angular/core';
host: {
class: 'flex flex-col p-4 gap-3 border border-blue',
},
- standalone: false,
+ imports: [RouterLink, RouterOutlet],
})
export class UserShellComponent {
public token = inject(TOKEN);
diff --git a/libs/module-to-standalone/user/shell/src/lib/user-shell.module.ts b/libs/module-to-standalone/user/shell/src/lib/user-shell.module.ts
deleted file mode 100644
index 433d6f77b..000000000
--- a/libs/module-to-standalone/user/shell/src/lib/user-shell.module.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { provideToken } from '@angular-challenges/module-to-standalone/core/providers';
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { UserShellComponent } from './user-shell.component';
-import { userShellRoutes } from './user-shell.routes';
-
-@NgModule({
- imports: [CommonModule, RouterModule.forChild(userShellRoutes), RouterModule],
- declarations: [UserShellComponent],
- providers: [provideToken('user-token')],
-})
-export class UserShellModule {}
diff --git a/libs/module-to-standalone/user/shell/src/lib/user-shell.routes.ts b/libs/module-to-standalone/user/shell/src/lib/user-shell.routes.ts
index b5813e5d5..9594dc753 100644
--- a/libs/module-to-standalone/user/shell/src/lib/user-shell.routes.ts
+++ b/libs/module-to-standalone/user/shell/src/lib/user-shell.routes.ts
@@ -1,3 +1,4 @@
+import { provideToken } from '@angular-challenges/module-to-standalone/core/providers';
import { Route } from '@angular/router';
import { UserShellComponent } from './user-shell.component';
@@ -5,20 +6,25 @@ export const userShellRoutes: Route[] = [
{
path: '',
component: UserShellComponent,
+ providers: [provideToken('user-token')],
children: [
- { path: '', redirectTo: 'home', pathMatch: 'full' },
+ {
+ path: '',
+ redirectTo: 'home',
+ pathMatch: 'full',
+ },
{
path: 'home',
loadChildren: () =>
import('@angular-challenges/module-to-standalone/user/home').then(
- (m) => m.UserHomeModule,
+ (m) => m.userHomeRoutes,
),
},
{
path: 'contact',
loadChildren: () =>
import('@angular-challenges/module-to-standalone/user/contact').then(
- (m) => m.ContactFeatureModule,
+ (m) => m.contactFeatureRoutes,
),
},
],
From 76e7746f636773b646e738e882dc008114bc471d Mon Sep 17 00:00:00 2001
From: slimabdi <36376210+slimabdi@users.noreply.github.com>
Date: Wed, 15 Apr 2026 10:51:32 +0200
Subject: [PATCH 7/7] feat(13): highly-customizable-css
---
.../src/app/page.component.ts | 6 +--
.../src/app/static-text.component.ts | 46 ++++++++-----------
.../src/app/text.component.ts | 25 +++++++---
3 files changed, 42 insertions(+), 35 deletions(-)
diff --git a/apps/angular/13-highly-customizable-css/src/app/page.component.ts b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
index 029ca52d2..8307023f1 100644
--- a/apps/angular/13-highly-customizable-css/src/app/page.component.ts
+++ b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
@@ -8,9 +8,9 @@ import { TextComponent } from './text.component';
imports: [TextStaticComponent, TextComponent],
template: `
-
-
- This is a blue text
+
+
+ This is a blue text
`,
})
export class PageComponent {}
diff --git a/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
index 703e2a538..f06a9016a 100644
--- a/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
+++ b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
@@ -1,5 +1,5 @@
/* eslint-disable @angular-eslint/component-selector */
-import { Component, computed, input } from '@angular/core';
+import { Component } from '@angular/core';
import { TextComponent } from './text.component';
export type StaticTextType = 'normal' | 'warning' | 'error';
@@ -8,31 +8,25 @@ export type StaticTextType = 'normal' | 'warning' | 'error';
selector: 'static-text',
imports: [TextComponent],
template: `
- This is a static text
+ This is a static text
`,
-})
-export class TextStaticComponent {
- type = input('normal');
+ styles: [
+ `
+ :host-context(.error) {
+ --font: 30px;
+ --color: red;
+ }
- font = computed(() => {
- switch (this.type()) {
- case 'error':
- return 30;
- case 'warning':
- return 25;
- default:
- return 10;
- }
- });
+ :host-context(.warning) {
+ --font: 25px;
+ --color: orange;
+ }
- color = computed(() => {
- switch (this.type()) {
- case 'error':
- return 'red';
- case 'warning':
- return 'orange';
- default:
- return 'black';
- }
- });
-}
+ :host-context(.normal) {
+ --font: 10px;
+ --color: black;
+ }
+ `,
+ ],
+})
+export class TextStaticComponent {}
diff --git a/apps/angular/13-highly-customizable-css/src/app/text.component.ts b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
index 9682c6b87..ba38a945c 100644
--- a/apps/angular/13-highly-customizable-css/src/app/text.component.ts
+++ b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
@@ -1,15 +1,28 @@
/* eslint-disable @angular-eslint/component-selector */
-import { Component, input } from '@angular/core';
+import { Component } from '@angular/core';
@Component({
selector: 'text',
template: `
-
+
`,
+ styles: [
+ `
+ p {
+ font-size: var(--font, 10px);
+ color: var(--color, black);
+ }
+
+ :host-context(.font) {
+ --font: 15px;
+ }
+
+ :host-context(.color) {
+ --color: blue;
+ }
+ `,
+ ],
})
-export class TextComponent {
- font = input(10);
- color = input('black');
-}
+export class TextComponent {}