TP done
This commit is contained in:
@@ -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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
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',
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user