This commit is contained in:
Johan
2025-12-17 09:54:42 +01:00
parent f5bcdd11e7
commit e9f21eeca9
5 changed files with 84 additions and 55 deletions

View File

@@ -5,21 +5,15 @@ import { Todo, TodoCreate } from '../models/todo.model';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TodoApiService { export class TodoApiService {
// TODO 1.1: Créer un signal privé pour stocker la liste de tâches. private readonly todos = signal<Todo[]>([
// Initialisez-le avec le tableau de données fourni dans les commentaires.
/*
[
{ id: 1, title: 'Apprendre les signaux Angular', completed: true }, { id: 1, title: 'Apprendre les signaux Angular', completed: true },
{ id: 2, title: 'Créer un service en mémoire', completed: false }, { id: 2, title: 'Créer un service en mémoire', completed: false },
{ id: 3, title: 'Préparer la transition vers HttpClient', completed: false }, { id: 3, title: 'Préparer la transition vers HttpClient', completed: false },
] ]);
*/
private readonly todos = signal<Todo[]>([]);
private nextId = 4; private nextId = 4;
// TODO 1.2: Exposer le signal `todos` en tant que propriété publique `todos$` // C'est la source de vérité pour les données. Les composants s'y connecteront.
// en lecture seule en utilisant `.asReadonly()`.
public readonly todos$: Signal<Todo[]> = this.todos.asReadonly(); public readonly todos$: Signal<Todo[]> = this.todos.asReadonly();
// Cette méthode sert uniquement à SIMULER un appel réseau pour le chargement initial. // Cette méthode sert uniquement à SIMULER un appel réseau pour le chargement initial.
@@ -31,23 +25,27 @@ export class TodoApiService {
} }
createTodo(todoData: TodoCreate): Observable<Todo> { createTodo(todoData: TodoCreate): Observable<Todo> {
// TODO 1.3: Implémenter la logique de création. const newTodo: Todo = {
// 1. Créez un objet `newTodo` avec un nouvel `id`, et les données de `todoData`. id: this.nextId++,
// 2. Mettez à jour le signal `todos` avec `.update()`. title: todoData.title,
// 3. Retournez le `newTodo` dans un `Observable` avec un `delay` de 300ms. completed: todoData.completed,
const newTodo: Todo = { id: 0, title: '', completed: false }; // A MODIFIER };
return of(newTodo); // A MODIFIER this.todos.update(currentTodos => [...currentTodos, newTodo]);
console.log(`SERVICE: Tâche "${newTodo.title}" créée.`);
return of(newTodo).pipe(delay(300));
} }
toggleTodo(id: number): void { toggleTodo(id: number): void {
// TODO 1.4: Implémenter la logique de bascule (toggle). this.todos.update(currentTodos =>
// Utilisez `this.todos.update()` et la méthode `.map()` pour inverser currentTodos.map(todo =>
// l'état `completed` de la tâche correspondante. todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
console.log(`SERVICE: Tâche ID ${id} basculée.`);
} }
deleteTodo(id: number): void { deleteTodo(id: number): void {
// TODO 1.4 (suite): Implémenter la logique de suppression. this.todos.update(currentTodos => currentTodos.filter(todo => todo.id !== id));
// Utilisez `this.todos.update()` et la méthode `.filter()` pour console.log(`SERVICE: Tâche ID ${id} supprimée.`);
// supprimer la tâche correspondante.
} }
} }

View File

@@ -4,10 +4,10 @@
<a routerLink="/todos" class="btn btn-secondary">Annuler</a> <a routerLink="/todos" class="btn btn-secondary">Annuler</a>
</div> </div>
<form> <!-- formGroup et ngSubmit à ajouter --> <form [formGroup]="todoForm" (ngSubmit)="saveTodo()">
<div class="form-group"> <div class="form-group">
<label for="title">Titre</label> <label for="title">Titre</label>
<input id="title" type="text" class="form-control"> <!-- formControlName à ajouter --> <input id="title" type="text" formControlName="title" class="form-control">
@if (todoForm.get('title')?.invalid && todoForm.get('title')?.touched) { @if (todoForm.get('title')?.invalid && todoForm.get('title')?.touched) {
<small class="form-error">Le titre est requis (2 caractères min).</small> <small class="form-error">Le titre est requis (2 caractères min).</small>
} }
@@ -15,14 +15,18 @@
<div class="form-group"> <div class="form-group">
<div class="form-check-group"> <div class="form-check-group">
<input id="completed" type="checkbox" class="form-check-input"> <!-- formControlName à ajouter --> <input id="completed" type="checkbox" formControlName="completed" class="form-check-input">
<label for="completed" class="form-check-label">Terminé ?</label> <label for="completed" class="form-check-label">Terminé ?</label>
</div> </div>
@if (todoForm.get('completed')?.invalid && todoForm.get('completed')?.touched) {
<small class="form-error">Ce champ est requis.</small>
}
</div> </div>
<div class="form-actions"> <div class="form-actions">
@if (isSaving()) { @if (isSaving()) {
<span>Enregistrement...</span> Chargement ...
} @else { } @else {
<button type="submit" class="btn btn-primary" [disabled]="todoForm.invalid"> <button type="submit" class="btn btn-primary" [disabled]="todoForm.invalid">
Enregistrer le todo Enregistrer le todo

View File

@@ -4,6 +4,7 @@ import { Router, RouterLink } from '@angular/router';
import {TodoApiService} from '../../../core/services/todo.service'; import {TodoApiService} from '../../../core/services/todo.service';
import {TodoCreate} from '../../../core/models/todo.model'; import {TodoCreate} from '../../../core/models/todo.model';
@Component({ @Component({
selector: 'app-todo-form', selector: 'app-todo-form',
templateUrl: './todo-form.component.html', templateUrl: './todo-form.component.html',
@@ -11,29 +12,45 @@ import { TodoCreate } from '../../../core/models/todo.model';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, RouterLink], imports: [ReactiveFormsModule, RouterLink],
}) })
export class TodoFormComponent { export class TodoFormComponent {
// TODO 3.1: Injecter le FormBuilder, le Router et le TodoApiService.
private readonly fb = inject(FormBuilder); private readonly fb = inject(FormBuilder);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly todoApiService = inject(TodoApiService); private readonly todoApiService = inject(TodoApiService);
readonly isSaving = signal(false); readonly isSaving = signal(false);
// TODO 3.2: Créer le formulaire `todoForm` avec le FormBuilder.
// Il doit contenir deux contrôles :
// - 'title': chaîne vide, validateurs 'required' et 'minLength(2)'
// - 'completed': booléen `false`, validateur 'required'
readonly todoForm = this.fb.nonNullable.group({ readonly todoForm = this.fb.nonNullable.group({
// À COMPLÉTER title: ['',[Validators.required, Validators.minLength(2)]],
completed: [false, Validators.required],
}); });
saveTodo(): void { saveTodo(): void {
// TODO 3.3: Implémenter la logique de sauvegarde. if (this.todoForm.invalid) {
// 1. Vérifier si `this.todoForm` est invalide. Si oui, marquer tous les champs comme "touchés" et arrêter. this.todoForm.markAllAsTouched();
// 2. Passer le signal `isSaving` à `true`. return;
// 3. Créer un `todoPayload` à partir des valeurs du formulaire.
// 4. Appeler `this.todoApiService.createTodo()` et souscrire à l'Observable.
// 5. Dans `next`, passer `isSaving` à `false` et naviguer vers '/todos'.
// 6. Dans `error`, passer `isSaving` à `false` et afficher une erreur en console.
} }
this.isSaving.set(true);
const formValue = this.todoForm.getRawValue();
const todoPayload : TodoCreate = {
title: formValue.title,
completed: formValue.completed,
};
this.todoApiService.createTodo(todoPayload).subscribe({
next: () => {
this.isSaving.set(false);
this.router.navigate(['/todos']);
}
, error: (err) => {
this.isSaving.set(false);
console.error('Erreur lors de la création du todo', err);
},
});
}
} }

View File

@@ -1,5 +1,5 @@
<div class="todo-container"> <div class="todo-container">
@if (false) { <!-- remplacer par isLoading() lorsque le loading sera implémenté --> @if (isLoading()) {
<app-spinner /> <app-spinner />
} @else { } @else {
@if (todos().length > 0) { @if (todos().length > 0) {

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, inject, Signal } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, signal, Signal } from '@angular/core';
import { TodoApiService } from '../../../core/services/todo.service'; import { TodoApiService } from '../../../core/services/todo.service'; // Assurez-vous que le chemin est correct
import { Todo } from '../../../core/models/todo.model'; import { Todo } from '../../../core/models/todo.model';
import { SpinnerComponent } from '../../../shared/components/spinner/spinner.component'; import { SpinnerComponent } from '../../../shared/components/spinner/spinner.component';
@@ -11,17 +11,27 @@ import { SpinnerComponent } from '../../../shared/components/spinner/spinner.com
imports: [SpinnerComponent], imports: [SpinnerComponent],
}) })
export class TodoListComponent { export class TodoListComponent {
// TODO 2.1: Injecter le TodoApiService. private readonly todoApiService = inject(TodoApiService);
private readonly todoApiService = inject(TodoApiService); // À COMPLÉTER
// TODO 2.2: Connecter le signal `todos` du composant au signal public `todos$` du service. // ÉTAPE 1: Gérer l'état de chargement localement dans le composant.
readonly todos: Signal<Todo[]> = this.todoApiService.todos$; // À COMPLÉTER readonly isLoading = signal(true);
// ÉTAPE 2: Se connecter DIRECTEMENT au signal du service pour les données.
// Ceci garantit que la liste sera toujours à jour.
readonly todos: Signal<Todo[]> = this.todoApiService.todos$;
constructor() {
// ÉTAPE 3: Déclencher le chargement initial et mettre à jour l'état `isLoading`.
this.todoApiService.fetchInitialTodos().subscribe(() => {
this.isLoading.set(false);
});
}
toggleTodo(id: number): void { toggleTodo(id: number): void {
// TODO 2.3: Appeler la méthode `toggleTodo` du service. this.todoApiService.toggleTodo(id);
} }
deleteTodo(id: number): void { deleteTodo(id: number): void {
// TODO 2.3 (suite): Appeler la méthode `deleteTodo` du service. this.todoApiService.deleteTodo(id);
} }
} }