How to build a todo app in Angular
Building a todo app in Angular is the ideal project for learning reactive state management, reactive forms, and component communication patterns in a realistic context.
As the creator of CoreUI with Angular development experience since 2014, I use this project structure as a reference for the correct separation of concerns between a service for state, a store service for data, and components for display.
The state lives in an injectable service using BehaviorSubject so any component in the tree can subscribe to todo changes reactively.
This pattern scales from a simple todo app to complex enterprise state management.
Create a todo service that manages state with BehaviorSubject.
// todo.service.ts
import { Injectable } from '@angular/core'
import { BehaviorSubject, map } from 'rxjs'
export interface Todo {
id: number
text: string
completed: boolean
createdAt: Date
}
@Injectable({ providedIn: 'root' })
export class TodoService {
private nextId = 1
private todos$ = new BehaviorSubject<Todo[]>([])
readonly all$ = this.todos$.asObservable()
readonly active$ = this.todos$.pipe(map(todos => todos.filter(t => !t.completed)))
readonly completed$ = this.todos$.pipe(map(todos => todos.filter(t => t.completed)))
readonly count$ = this.todos$.pipe(map(todos => todos.filter(t => !t.completed).length))
add(text: string): void {
if (!text.trim()) return
const current = this.todos$.value
this.todos$.next([
...current,
{ id: this.nextId++, text: text.trim(), completed: false, createdAt: new Date() }
])
}
toggle(id: number): void {
this.todos$.next(
this.todos$.value.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
)
}
remove(id: number): void {
this.todos$.next(this.todos$.value.filter(t => t.id !== id))
}
clearCompleted(): void {
this.todos$.next(this.todos$.value.filter(t => !t.completed))
}
}
BehaviorSubject holds the current state and emits to all subscribers when changed. Derived observables (active$, completed$, count$) automatically update when todos$ changes. No manual subscription management — use the async pipe in templates.
Todo App Component
Build the main component with add form and filter tabs.
// todo-app.component.ts
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms'
import { CardModule, FormModule, ButtonModule, BadgeComponent } from '@coreui/angular'
import { TodoService } from './todo.service'
import { TodoItemComponent } from './todo-item.component'
@Component({
selector: 'app-todo',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, CardModule, FormModule, ButtonModule, BadgeComponent, TodoItemComponent],
template: `
<c-card style="max-width: 600px; margin: 2rem auto">
<c-card-header class="d-flex justify-content-between align-items-center">
<span>Todos</span>
<c-badge color="primary">{{ todoService.count$ | async }} active</c-badge>
</c-card-header>
<c-card-body>
<form class="d-flex gap-2 mb-4" (ngSubmit)="addTodo()">
<input
cFormControl
[formControl]="newTodo"
placeholder="What needs to be done?"
/>
<button cButton color="primary" type="submit">Add</button>
</form>
<div class="d-flex gap-2 mb-3">
<button cButton [color]="filter === 'all' ? 'primary' : 'ghost'" (click)="filter = 'all'">All</button>
<button cButton [color]="filter === 'active' ? 'primary' : 'ghost'" (click)="filter = 'active'">Active</button>
<button cButton [color]="filter === 'completed' ? 'primary' : 'ghost'" (click)="filter = 'completed'">Completed</button>
</div>
<ng-container [ngSwitch]="filter">
<app-todo-item
*ngFor="let todo of (todoService.all$ | async)"
[todo]="todo"
*ngSwitchCase="'all'"
></app-todo-item>
<app-todo-item
*ngFor="let todo of (todoService.active$ | async)"
[todo]="todo"
*ngSwitchCase="'active'"
></app-todo-item>
<app-todo-item
*ngFor="let todo of (todoService.completed$ | async)"
[todo]="todo"
*ngSwitchCase="'completed'"
></app-todo-item>
</ng-container>
<button cButton color="link" class="mt-2" (click)="todoService.clearCompleted()">
Clear completed
</button>
</c-card-body>
</c-card>
`
})
export class TodoAppComponent {
filter: 'all' | 'active' | 'completed' = 'all'
newTodo = new FormControl('', Validators.required)
constructor(public todoService: TodoService) {}
addTodo(): void {
if (this.newTodo.valid && this.newTodo.value) {
this.todoService.add(this.newTodo.value)
this.newTodo.reset()
}
}
}
The async pipe subscribes to observables and automatically unsubscribes when the component destroys, preventing memory leaks. Using todoService as public makes it accessible in the template directly.
Best Practice Note
This is the same BehaviorSubject state pattern used in CoreUI Angular templates for lightweight local state. For persistence, add localStorage sync in the service constructor and on every todos$.next() call. To connect to a REST API, replace the in-memory operations with HttpClient calls and use RxJS operators to update the local state after each API response. See how to build a todo API in Node.js for the backend to connect to.



