Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author