TP done
This commit is contained in:
@@ -5,21 +5,15 @@ import { Todo, TodoCreate } from '../models/todo.model';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TodoApiService {
|
||||
|
||||
// TODO 1.1: Créer un signal privé pour stocker la liste de tâches.
|
||||
// Initialisez-le avec le tableau de données fourni dans les commentaires.
|
||||
/*
|
||||
[
|
||||
private readonly todos = signal<Todo[]>([
|
||||
{ id: 1, title: 'Apprendre les signaux Angular', completed: true },
|
||||
{ id: 2, title: 'Créer un service en mémoire', completed: false },
|
||||
{ id: 3, title: 'Préparer la transition vers HttpClient', completed: false },
|
||||
]
|
||||
*/
|
||||
private readonly todos = signal<Todo[]>([]);
|
||||
]);
|
||||
|
||||
private nextId = 4;
|
||||
|
||||
// TODO 1.2: Exposer le signal `todos` en tant que propriété publique `todos$`
|
||||
// en lecture seule en utilisant `.asReadonly()`.
|
||||
// C'est la source de vérité pour les données. Les composants s'y connecteront.
|
||||
public readonly todos$: Signal<Todo[]> = this.todos.asReadonly();
|
||||
|
||||
// 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> {
|
||||
// TODO 1.3: Implémenter la logique de création.
|
||||
// 1. Créez un objet `newTodo` avec un nouvel `id`, et les données de `todoData`.
|
||||
// 2. Mettez à jour le signal `todos` avec `.update()`.
|
||||
// 3. Retournez le `newTodo` dans un `Observable` avec un `delay` de 300ms.
|
||||
const newTodo: Todo = { id: 0, title: '', completed: false }; // A MODIFIER
|
||||
return of(newTodo); // A MODIFIER
|
||||
const newTodo: Todo = {
|
||||
id: this.nextId++,
|
||||
title: todoData.title,
|
||||
completed: todoData.completed,
|
||||
};
|
||||
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 {
|
||||
// TODO 1.4: Implémenter la logique de bascule (toggle).
|
||||
// Utilisez `this.todos.update()` et la méthode `.map()` pour inverser
|
||||
// l'état `completed` de la tâche correspondante.
|
||||
this.todos.update(currentTodos =>
|
||||
currentTodos.map(todo =>
|
||||
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
||||
)
|
||||
);
|
||||
console.log(`SERVICE: Tâche ID ${id} basculée.`);
|
||||
}
|
||||
|
||||
deleteTodo(id: number): void {
|
||||
// TODO 1.4 (suite): Implémenter la logique de suppression.
|
||||
// Utilisez `this.todos.update()` et la méthode `.filter()` pour
|
||||
// supprimer la tâche correspondante.
|
||||
this.todos.update(currentTodos => currentTodos.filter(todo => todo.id !== id));
|
||||
console.log(`SERVICE: Tâche ID ${id} supprimée.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<a routerLink="/todos" class="btn btn-secondary">Annuler</a>
|
||||
</div>
|
||||
|
||||
<form> <!-- formGroup et ngSubmit à ajouter -->
|
||||
<form [formGroup]="todoForm" (ngSubmit)="saveTodo()">
|
||||
<div class="form-group">
|
||||
<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) {
|
||||
<small class="form-error">Le titre est requis (2 caractères min).</small>
|
||||
}
|
||||
@@ -15,14 +15,18 @@
|
||||
|
||||
<div class="form-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>
|
||||
</div>
|
||||
|
||||
@if (todoForm.get('completed')?.invalid && todoForm.get('completed')?.touched) {
|
||||
<small class="form-error">Ce champ est requis.</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
@if (isSaving()) {
|
||||
<span>Enregistrement...</span>
|
||||
Chargement ...
|
||||
} @else {
|
||||
<button type="submit" class="btn btn-primary" [disabled]="todoForm.invalid">
|
||||
Enregistrer le todo
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { TodoApiService } from '../../../core/services/todo.service';
|
||||
import { TodoCreate } from '../../../core/models/todo.model';
|
||||
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {TodoApiService} from '../../../core/services/todo.service';
|
||||
import {TodoCreate} from '../../../core/models/todo.model';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-todo-form',
|
||||
@@ -11,29 +12,45 @@ import { TodoCreate } from '../../../core/models/todo.model';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReactiveFormsModule, RouterLink],
|
||||
})
|
||||
|
||||
export class TodoFormComponent {
|
||||
// TODO 3.1: Injecter le FormBuilder, le Router et le TodoApiService.
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly router = inject(Router);
|
||||
private readonly todoApiService = inject(TodoApiService);
|
||||
|
||||
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({
|
||||
// À COMPLÉTER
|
||||
title: ['',[Validators.required, Validators.minLength(2)]],
|
||||
completed: [false, Validators.required],
|
||||
});
|
||||
|
||||
saveTodo(): void {
|
||||
// TODO 3.3: Implémenter la logique de sauvegarde.
|
||||
// 1. Vérifier si `this.todoForm` est invalide. Si oui, marquer tous les champs comme "touchés" et arrêter.
|
||||
// 2. Passer le signal `isSaving` à `true`.
|
||||
// 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.
|
||||
if (this.todoForm.invalid) {
|
||||
this.todoForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="todo-container">
|
||||
@if (false) { <!-- remplacer par isLoading() lorsque le loading sera implémenté -->
|
||||
@if (isLoading()) {
|
||||
<app-spinner />
|
||||
} @else {
|
||||
@if (todos().length > 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, inject, Signal } from '@angular/core';
|
||||
import { TodoApiService } from '../../../core/services/todo.service';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal, Signal } from '@angular/core';
|
||||
import { TodoApiService } from '../../../core/services/todo.service'; // Assurez-vous que le chemin est correct
|
||||
import { Todo } from '../../../core/models/todo.model';
|
||||
import { SpinnerComponent } from '../../../shared/components/spinner/spinner.component';
|
||||
|
||||
@@ -11,17 +11,27 @@ import { SpinnerComponent } from '../../../shared/components/spinner/spinner.com
|
||||
imports: [SpinnerComponent],
|
||||
})
|
||||
export class TodoListComponent {
|
||||
// TODO 2.1: Injecter le TodoApiService.
|
||||
private readonly todoApiService = inject(TodoApiService); // À COMPLÉTER
|
||||
private readonly todoApiService = inject(TodoApiService);
|
||||
|
||||
// TODO 2.2: Connecter le signal `todos` du composant au signal public `todos$` du service.
|
||||
readonly todos: Signal<Todo[]> = this.todoApiService.todos$; // À COMPLÉTER
|
||||
// ÉTAPE 1: Gérer l'état de chargement localement dans le composant.
|
||||
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 {
|
||||
// TODO 2.3: Appeler la méthode `toggleTodo` du service.
|
||||
this.todoApiService.toggleTodo(id);
|
||||
}
|
||||
|
||||
deleteTodo(id: number): void {
|
||||
// TODO 2.3 (suite): Appeler la méthode `deleteTodo` du service.
|
||||
this.todoApiService.deleteTodo(id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user