How to build a notes app in Angular
A notes app combines CRUD operations, real-time search filtering, and rich text display in an intuitive interface that teaches essential Angular patterns.
As the creator of CoreUI with Angular development experience since 2014, I use this project structure in CoreUI Angular templates as the starting point for more complex content management features.
The architecture uses a reactive service with BehaviorSubject for state, the async pipe for memory-safe subscriptions, and combineLatest to derive the filtered list from both the notes array and the search query.
This reactive approach avoids imperative subscription management and keeps the component template simple.
Create the notes service with reactive state.
// notes.service.ts
import { Injectable } from '@angular/core'
import { BehaviorSubject, combineLatest, map } from 'rxjs'
export interface Note {
id: number
title: string
content: string
color: string
createdAt: Date
}
const COLORS = ['#fff9c4', '#f8bbd0', '#b3e5fc', '#c8e6c9', '#ffe0b2']
@Injectable({ providedIn: 'root' })
export class NotesService {
private nextId = 1
private notes$ = new BehaviorSubject<Note[]>([])
private search$ = new BehaviorSubject<string>('')
readonly search = this.search$.asObservable()
readonly filteredNotes$ = combineLatest([this.notes$, this.search$]).pipe(
map(([notes, query]) => {
if (!query.trim()) return notes
const q = query.toLowerCase()
return notes.filter(n =>
n.title.toLowerCase().includes(q) ||
n.content.toLowerCase().includes(q)
)
})
)
add(title: string, content: string): void {
const color = COLORS[Math.floor(Math.random() * COLORS.length)]
const notes = [
{ id: this.nextId++, title, content, color, createdAt: new Date() },
...this.notes$.value
]
this.notes$.next(notes)
}
remove(id: number): void {
this.notes$.next(this.notes$.value.filter(n => n.id !== id))
}
setSearch(query: string): void {
this.search$.next(query)
}
}
combineLatest emits whenever either notes$ or search$ changes, deriving the filtered list reactively. The async pipe in the template subscribes and unsubscribes automatically.
Notes App Component
Build the main page with search and masonry grid layout.
// notes-app.component.ts
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule, FormControl } from '@angular/forms'
import { CardModule, FormModule, ButtonModule, GridModule } from '@coreui/angular'
import { NotesService } from './notes.service'
@Component({
selector: 'app-notes',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, CardModule, FormModule, ButtonModule, GridModule],
template: `
<div class="mb-4 d-flex gap-3 align-items-center">
<h1 class="mb-0">Notes</h1>
<input cFormControl placeholder="Search notes..." [formControl]="searchControl"
style="max-width: 300px" />
</div>
<!-- Add note form -->
<c-card class="mb-4">
<c-card-body>
<input cFormControl [formControl]="titleControl" placeholder="Title" class="mb-2" />
<textarea cFormControl [formControl]="contentControl" rows="3"
placeholder="Note content..."></textarea>
<button cButton color="primary" class="mt-2" (click)="addNote()">Add Note</button>
</c-card-body>
</c-card>
<!-- Notes grid -->
<c-row>
<c-col sm="6" md="4" lg="3"
*ngFor="let note of (notesService.filteredNotes$ | async)"
class="mb-3"
>
<c-card [style.background]="note.color">
<c-card-body>
<h5>{{ note.title }}</h5>
<p class="small">{{ note.content }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">{{ note.createdAt | date:'MMM d' }}</small>
<button cButton color="ghost" size="sm" (click)="notesService.remove(note.id)">
✕
</button>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
<p *ngIf="(notesService.filteredNotes$ | async)?.length === 0" class="text-muted text-center mt-4">
No notes found.
</p>
`
})
export class NotesAppComponent {
titleControl = new FormControl('')
contentControl = new FormControl('')
searchControl = new FormControl('')
constructor(public notesService: NotesService) {
this.searchControl.valueChanges.subscribe(query => {
this.notesService.setSearch(query ?? '')
})
}
addNote(): void {
const title = this.titleControl.value?.trim()
const content = this.contentControl.value?.trim()
if (!title || !content) return
this.notesService.add(title, content)
this.titleControl.reset()
this.contentControl.reset()
}
}
valueChanges on the search control connects form input to the service’s search state reactively. The async pipe handles subscription and renders the filtered list automatically whenever it changes.
Best Practice Note
This is the same notes app structure used in CoreUI Angular templates as a demonstration of reactive state management. Add localStorage persistence in the service to survive page reloads. For a production notes app, connect to the notes API in Node.js backend for multi-device sync. Rich text editing can be added with a library like Quill or TipTap.



