TP Todo APP
01/05/2025Environ 6 minTravaux Pratiques
Créer une TODO App Angular 19 from scratch dans VS Code
🛠️ Prérequis et installation
1. Installer Node.js
# Vérifier si Node.js est installé
node --version
npm --version
# Si pas installé, télécharger depuis https://nodejs.org (version LTS recommandée)
2. Installer Angular CLI globalement
npm install -g @angular/cli
# Vérifier l'installation
ng version
3. Extensions VS Code recommandées
Dans VS Code, installer ces extensions :
- Angular Language Service (officielle)
- TypeScript Importer
- Auto Rename Tag
- Prettier - Code formatter
🚀 Étape 1 : Création du projet
1.1 Créer le projet Angular
# Ouvrir un terminal dans VS Code (Ctrl+`)
ng new todo-app
# Répondre aux questions :
# Would you like to add Angular routing? (y/N) → Y
# Which stylesheet format would you like to use? → CSS
1.2 Ouvrir le projet dans VS Code
cd todo-app
code .
1.3 Lancer le serveur de développement
ng serve
# Ou avec port spécifique
ng serve --port 4200
Ouvrez http://localhost:4200 dans votre navigateur pour vérifier que l'app fonctionne.

📁 Étape 2 : Structure du projet
Votre structure devrait ressembler à :
todo-app/
├── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── app.component.css
│ │ ├── app.config.ts (Angular 19)
│ │ └── app.routes.ts (Angular 19)
│ ├── assets/
│ ├── environments/
│ └── index.html
├── package.json
└── angular.json
🔧 Étape 3 : Créer l'interface Todo
3.1 Créer le dossier models
# Dans le terminal VS Code
mkdir src/app/models
3.2 Créer l'interface Todo
Fichier : src/app/models/todo.interface.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
🎯 Étape 4 : Créer le service Todo
4.1 Générer le service
ng generate service services/todo
# ou en raccourci
ng g s services/todo
4.2 Implémenter le TodoService
Fichier : src/app/services/todo.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Todo } from '../models/todo.interface';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private todos: Todo[] = [];
private todosSubject = new BehaviorSubject<Todo[]>([]);
public todos$: Observable<Todo[]> = this.todosSubject.asObservable();
constructor() {
this.loadFromStorage();
}
addTodo(title: string): void {
const newTodo: Todo = {
id: Date.now(),
title: title.trim(),
completed: false,
createdAt: new Date()
};
this.todos.push(newTodo);
this.updateTodos();
}
toggleTodo(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.updateTodos();
}
}
deleteTodo(id: number): void {
this.todos = this.todos.filter(t => t.id !== id);
this.updateTodos();
}
updateTodo(id: number, title: string): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.title = title.trim();
this.updateTodos();
}
}
getCompletedCount(): number {
return this.todos.filter(t => t.completed).length;
}
getPendingCount(): number {
return this.todos.filter(t => !t.completed).length;
}
clearCompleted(): void {
this.todos = this.todos.filter(t => !t.completed);
this.updateTodos();
}
private updateTodos(): void {
this.todosSubject.next([...this.todos]);
this.saveToStorage();
}
private saveToStorage(): void {
localStorage.setItem('angular-todos', JSON.stringify(this.todos));
}
private loadFromStorage(): void {
const stored = localStorage.getItem('angular-todos');
if (stored) {
this.todos = JSON.parse(stored);
this.updateTodos();
}
}
}
🎨 Étape 5 : Créer le composant Todo
5.1 Générer le composant
ng generate component components/todo-app
# ou en raccourci
ng g c components/todo-app
5.2 Implémenter le composant TodoApp
Fichier : src/app/components/todo-app/todo-app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TodoService } from '../../services/todo.service';
import { Todo } from '../../models/todo.interface';
@Component({
selector: 'app-todo-app',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './todo-app.component.html',
styleUrls: ['./todo-app.component.scss']
})
export class TodoAppComponent implements OnInit, OnDestroy {
todos: Todo[] = [];
newTodoTitle = '';
editingTodo: number | null = null;
editingTitle = '';
currentFilter: 'all' | 'pending' | 'completed' = 'all';
completedCount = 0;
pendingCount = 0;
filters = [
{ value: 'all' as const, label: 'Toutes' },
{ value: 'pending' as const, label: 'En cours' },
{ value: 'completed' as const, label: 'Terminées' }
];
private destroy$ = new Subject<void>();
constructor(private todoService: TodoService) { }
ngOnInit(): void {
this.todoService.todos$
.pipe(takeUntil(this.destroy$))
.subscribe(todos => {
this.todos = todos;
this.completedCount = this.todoService.getCompletedCount();
this.pendingCount = this.todoService.getPendingCount();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
addTodo(): void {
if (this.newTodoTitle.trim()) {
this.todoService.addTodo(this.newTodoTitle);
this.newTodoTitle = '';
}
}
toggleTodo(id: number): void {
this.todoService.toggleTodo(id);
}
deleteTodo(id: number): void {
if (confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) {
this.todoService.deleteTodo(id);
}
}
startEdit(todo: Todo): void {
this.editingTodo = todo.id;
this.editingTitle = todo.title;
// Focus automatique sur l'input d'édition
setTimeout(() => {
const input = document.querySelector('.edit-input') as HTMLInputElement;
if (input) {
input.focus();
input.select();
}
});
}
saveEdit(id: number): void {
if (this.editingTitle.trim()) {
this.todoService.updateTodo(id, this.editingTitle);
}
this.cancelEdit();
}
cancelEdit(): void {
this.editingTodo = null;
this.editingTitle = '';
}
clearCompleted(): void {
if (confirm(`Supprimer les ${this.completedCount} tâches terminées ?`)) {
this.todoService.clearCompleted();
}
}
getFilteredTodos(): Todo[] {
switch (this.currentFilter) {
case 'pending':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}
trackByTodo(index: number, todo: Todo): number {
return todo.id;
}
}
🎨 Étape 6 : Template HTML
Fichier : src/app/components/todo-app/todo-app.component.html
<div class="todo-app">
<header class="todo-header">
<h1>Todo App Angular</h1>
<div class="stats">
<span class="stat">Total: {{todos.length}}</span>
<span class="stat pending">En cours: {{pendingCount}}</span>
<span class="stat completed">Terminées: {{completedCount}}</span>
</div>
</header>
<div class="todo-form">
<input
type="text"
[(ngModel)]="newTodoTitle"
(keyup.enter)="addTodo()"
placeholder="Ajouter une nouvelle tâche..."
class="todo-input">
<button
(click)="addTodo()"
[disabled]="!newTodoTitle.trim()"
class="add-btn">
Ajouter
</button>
</div>
<div class="filter-tabs">
<button
*ngFor="let filter of filters"
(click)="currentFilter = filter.value"
[class.active]="currentFilter === filter.value"
class="filter-btn">
{{filter.label}}
</button>
</div>
<div class="todo-list">
<div
*ngFor="let todo of getFilteredTodos(); trackBy: trackByTodo"
class="todo-item"
[class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
class="todo-checkbox">
<div class="todo-content" (dblclick)="startEdit(todo)">
<span
*ngIf="editingTodo !== todo.id"
class="todo-title"
[class.completed-text]="todo.completed">
{{todo.title}}
</span>
<input
*ngIf="editingTodo === todo.id"
type="text"
[(ngModel)]="editingTitle"
(keyup.enter)="saveEdit(todo.id)"
(keyup.escape)="cancelEdit()"
(blur)="saveEdit(todo.id)"
class="edit-input"
#editInput>
</div>
<div class="todo-actions">
<button
(click)="startEdit(todo)"
class="edit-btn"
title="Modifier">
✏️
</button>
<button
(click)="deleteTodo(todo.id)"
class="delete-btn"
title="Supprimer">
🗑️
</button>
</div>
<div class="todo-date">
{{todo.createdAt | date:'dd/MM/yyyy HH:mm'}}
</div>
</div>
<div *ngIf="getFilteredTodos().length === 0" class="empty-state">
<p *ngIf="currentFilter === 'all'">Aucune tâche pour le moment</p>
<p *ngIf="currentFilter === 'pending'">Aucune tâche en cours</p>
<p *ngIf="currentFilter === 'completed'">Aucune tâche terminée</p>
</div>
</div>
<div class="todo-footer" *ngIf="todos.length > 0">
<button
(click)="clearCompleted()"
[disabled]="completedCount === 0"
class="clear-btn">
Supprimer les tâches terminées ({{completedCount}})
</button>
</div>
</div>
💅 Étape 7 : Styles CSS
Fichier : src/app/components/todo-app/todo-app.component.scss
.todo-app {
max-width: 600px;
margin: 20px auto;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}
.todo-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.todo-header h1 {
margin: 0 0 20px 0;
font-size: 2.5rem;
font-weight: 300;
}
.stats {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.stat {
background: rgba(255,255,255,0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
}
.stat.pending {
background: rgba(255,193,7,0.3);
}
.stat.completed {
background: rgba(40,167,69,0.3);
}
.todo-form {
padding: 20px;
display: flex;
gap: 10px;
border-bottom: 1px solid #eee;
}
.todo-input {
flex: 1;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
}
.todo-input:focus {
outline: none;
border-color: #667eea;
}
.add-btn {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
.add-btn:hover:not(:disabled) {
background: #5a6fd8;
}
.add-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.filter-tabs {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #eee;
}
.filter-btn {
flex: 1;
padding: 15px;
border: none;
background: transparent;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
font-weight: 500;
}
.filter-btn:hover {
background: #e9ecef;
}
.filter-btn.active {
background: #667eea;
color: white;
}
.todo-list {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.todo-item {
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-areas:
"checkbox content actions date";
gap: 15px;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
align-items: center;
transition: all 0.3s;
}
.todo-item:hover {
background: #f8f9fa;
}
.todo-item.completed {
opacity: 0.6;
}
.todo-checkbox {
grid-area: checkbox;
width: 18px;
height: 18px;
cursor: pointer;
}
.todo-content {
grid-area: content;
min-height: 24px;
display: flex;
align-items: center;
}
.todo-title {
font-size: 16px;
line-height: 1.4;
word-break: break-word;
}
.completed-text {
text-decoration: line-through;
color: #6c757d;
}
.edit-input {
width: 100%;
padding: 4px 8px;
border: 2px solid #667eea;
border-radius: 4px;
font-size: 16px;
}
.todo-actions {
grid-area: actions;
display: flex;
gap: 5px;
}
.edit-btn, .delete-btn {
background: none;
border: none;
padding: 5px;
cursor: pointer;
border-radius: 4px;
font-size: 16px;
transition: background 0.3s;
}
.edit-btn:hover {
background: #e3f2fd;
}
.delete-btn:hover {
background: #ffebee;
}
.todo-date {
grid-area: date;
font-size: 12px;
color: #6c757d;
white-space: nowrap;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-state p {
margin: 0;
font-size: 18px;
}
.todo-footer {
padding: 20px;
border-top: 1px solid #eee;
text-align: center;
}
.clear-btn {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s;
}
.clear-btn:hover:not(:disabled) {
background: #c82333;
}
.clear-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Responsive */
@media (max-width: 768px) {
.todo-item {
grid-template-columns: auto 1fr auto;
grid-template-areas:
"checkbox content actions"
". date .";
gap: 10px;
}
.stats {
flex-direction: column;
align-items: center;
}
.filter-tabs {
flex-direction: column;
}
}
🔗 Étape 8 : Intégrer dans l'app principale
Fichier : src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TodoAppComponent } from './components/todo-app/todo-app.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, TodoAppComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'todo-app';
}
Fichier : src/app/app.component.html
<div class="app-container">
<app-todo-app></app-todo-app>
</div>
Fichier : src/app/app.component.css
.app-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px 0;
}
🎯 Étape 9 : Styles globaux
Fichier : src/styles.css
/* Reset et styles globaux */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f8f9fa;
}
/* Amélioration des scrollbars */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
🚀 Étape 10 : Tester l'application
10.1 Lancer l'application
ng serve
10.2 Fonctionnalités à tester
- ✅ Ajouter une nouvelle tâche
- ✅ Marquer une tâche comme terminée
- ✅ Modifier une tâche (double-clic)
- ✅ Supprimer une tâche
- ✅ Filtrer les tâches (Toutes/En cours/Terminées)
- ✅ Supprimer toutes les tâches terminées
- ✅ Persistance des données (refresh de page)
🛠️ Étape 11 : Build pour la production
# Build de production
ng build
# Les fichiers seront dans le dossier dist/
🔧 Points clés pour Angular 19
Différences importantes :
- Composants Standalone : Angular 19 utilise par défaut des composants standalone
- Imports explicites :
CommonModule
etFormsModule
doivent être importés dans chaque composant - Pas de app.module.ts : Angular 19 utilise
app.config.ts
etapp.routes.ts
- Standalone: true : Tous les composants sont maintenant standalone par défaut
Troubleshooting courant :
- Si
*ngFor
ou*ngIf
ne fonctionnent pas → Vérifiez queCommonModule
est importé - Si
[(ngModel)]
ne fonctionne pas → Vérifiez queFormsModule
est importé - Si le composant n'est pas reconnu → Vérifiez les imports dans le composant parent
🎉 Félicitations !
Vous avez créé une application TODO complète avec Angular 19 !
Fonctionnalités implémentées :
- ✅ CRUD complet (Create, Read, Update, Delete)
- ✅ Persistance avec LocalStorage
- ✅ Filtres par statut
- ✅ Statistiques en temps réel
- ✅ Interface responsive
- ✅ Édition inline
- ✅ Animations et transitions
Concepts Angular 19 couverts :
- 🎯 Composants Standalone
- 🔧 Services et injection de dépendances
- 📊 Observables et RxJS
- 🎨 Data binding (one-way et two-way)
- 📋 Directives structurelles (*ngFor, *ngIf)
- 🔄 Cycle de vie des composants
- 💾 LocalStorage et persistance
- 🎭 Pipes pour le formatage
Cette application est maintenant prête à fonctionner avec Angular 19.2.13 ! 🚀