Master Angular from the ground up. Learn why Angular was created, understand core concepts like modules, components, services, dependency injection, directives, pipes, forms, HTTP, routing, RxJS, and change detection. Build a complete production-ready todo application covering all Angular fundamentals with best practices.

Angular has become one of the most comprehensive and opinionated frontend frameworks for building large-scale, enterprise-grade web applications. But why does it exist, and what makes it different from other frameworks? Understanding Angular's philosophy and core concepts is essential for building scalable, maintainable, and production-ready applications.
In this article, we'll explore Angular's history, understand why it was created, dive deep into core concepts, and build a complete production-ready todo application that demonstrates all fundamental Angular patterns.
Before Angular, building large-scale web applications was challenging:
Google created Angular (originally AngularJS in 2010, then Angular 2+ in 2016) to solve these problems:
Modules are containers for a cohesive block of code dedicated to an application domain, workflow, or closely related set of capabilities.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { TodoListComponent } from './components/todo-list/todo-list.component';
@NgModule({
declarations: [
AppComponent,
TodoListComponent,
],
imports: [
BrowserModule,
HttpClientModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }Components are the basic building blocks of Angular applications. A component controls a patch of screen called a view.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-todo-item',
template: `
<div class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="onToggle()"
/>
<span [class.completed]="todo.completed">{{ todo.title }}</span>
<button (click)="onDelete()">Delete</button>
</div>
`,
styles: [`
.todo-item {
display: flex;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.completed {
text-decoration: line-through;
color: #999;
}
`],
})
export class TodoItemComponent implements OnInit {
@Input() todo: any;
@Output() toggle = new EventEmitter<void>();
@Output() delete = new EventEmitter<void>();
ngOnInit(): void {
console.log('TodoItemComponent initialized');
}
onToggle(): void {
this.toggle.emit();
}
onDelete(): void {
this.delete.emit();
}
}Services are classes with a focused purpose. They're used to share data and logic across components.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
export interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate: string;
createdAt: Date;
}
@Injectable({
providedIn: 'root',
})
export class TodoService {
private apiUrl = '/api/todos';
private todosSubject = new BehaviorSubject<Todo[]>([]);
public todos$ = this.todosSubject.asObservable();
constructor(private http: HttpClient) {
this.loadTodos();
}
loadTodos(): void {
this.http.get<Todo[]>(this.apiUrl)
.pipe(
tap(todos => this.todosSubject.next(todos))
)
.subscribe();
}
getTodos(): Observable<Todo[]> {
return this.todos$;
}
addTodo(todo: Omit<Todo, 'id' | 'createdAt'>): Observable<Todo> {
return this.http.post<Todo>(this.apiUrl, todo)
.pipe(
tap(newTodo => {
const current = this.todosSubject.value;
this.todosSubject.next([newTodo, ...current]);
})
);
}
updateTodo(id: string, updates: Partial<Todo>): Observable<Todo> {
return this.http.patch<Todo>(`${this.apiUrl}/${id}`, updates)
.pipe(
tap(updated => {
const current = this.todosSubject.value;
const index = current.findIndex(t => t.id === id);
if (index !== -1) {
current[index] = updated;
this.todosSubject.next([...current]);
}
})
);
}
deleteTodo(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`)
.pipe(
tap(() => {
const current = this.todosSubject.value;
this.todosSubject.next(current.filter(t => t.id !== id));
})
);
}
}Directives are classes that add new behavior to the DOM or modify existing behavior.
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
// Structural Directive - modifies DOM structure
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter(): void {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave(): void {
this.highlight('transparent');
}
private highlight(color: string): void {
this.el.nativeElement.style.backgroundColor = color;
}
}
// Usage in template
// <div appHighlight="lightblue">Hover me</div>
// Built-in Structural Directives
// *ngIf - conditionally render
// *ngFor - loop through arrays
// *ngSwitch - switch between templates
// Template example
/*
<div *ngIf="isVisible">Visible</div>
<div *ngFor="let item of items; let i = index">{{ i }}: {{ item }}</div>
<div [ngSwitch]="status">
<div *ngSwitchCase="'active'">Active</div>
<div *ngSwitchCase="'inactive'">Inactive</div>
<div *ngSwitchDefault>Unknown</div>
</div>
*/Pipes transform data for display in templates.
import { Pipe, PipeTransform } from '@angular/core';
// Custom Pipe
@Pipe({
name: 'truncate',
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50): string {
if (!value) return '';
return value.length > limit ? value.substring(0, limit) + '...' : value;
}
}
// Built-in Pipes
/*
{{ date | date: 'short' }}
{{ price | currency: 'USD' }}
{{ text | uppercase }}
{{ text | lowercase }}
{{ items | slice: 0:5 }}
{{ value | number: '1.2-2' }}
{{ obj | json }}
*/
// Template example
/*
<p>{{ todo.title | truncate: 30 }}</p>
<p>{{ todo.dueDate | date: 'MMM d, y' }}</p>
<p>{{ todo.priority | uppercase }}</p>
*/Angular provides two approaches to handling forms: Reactive Forms and Template-driven Forms.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-todo-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label>Title</label>
<input
type="text"
formControlName="title"
placeholder="Enter task title"
/>
<div *ngIf="form.get('title')?.invalid && form.get('title')?.touched">
Title is required
</div>
</div>
<div>
<label>Description</label>
<textarea
formControlName="description"
placeholder="Enter task description"
></textarea>
</div>
<div>
<label>Priority</label>
<select formControlName="priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<label>Due Date</label>
<input
type="date"
formControlName="dueDate"
/>
</div>
<button type="submit" [disabled]="form.invalid">Add Task</button>
</form>
`,
})
export class TodoFormComponent implements OnInit {
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3)]],
description: [''],
priority: ['medium'],
dueDate: [''],
});
}
onSubmit(): void {
if (this.form.valid) {
console.log(this.form.value);
this.form.reset();
}
}
}The HttpClient service is used to communicate with backend servers over HTTP.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, timeout } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class ApiService {
private apiUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
// GET request
getTodos(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/todos`)
.pipe(
timeout(5000),
retry(1),
catchError(this.handleError)
);
}
// POST request
createTodo(todo: any): Observable<any> {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
});
return this.http.post<any>(
`${this.apiUrl}/todos`,
todo,
{ headers }
).pipe(
catchError(this.handleError)
);
}
// PUT request
updateTodo(id: string, todo: any): Observable<any> {
return this.http.put<any>(
`${this.apiUrl}/todos/${id}`,
todo
).pipe(
catchError(this.handleError)
);
}
// DELETE request
deleteTodo(id: string): Observable<void> {
return this.http.delete<void>(
`${this.apiUrl}/todos/${id}`
).pipe(
catchError(this.handleError)
);
}
// Query parameters
searchTodos(query: string): Observable<any[]> {
const params = new HttpParams()
.set('q', query)
.set('limit', '10');
return this.http.get<any[]>(
`${this.apiUrl}/todos/search`,
{ params }
).pipe(
catchError(this.handleError)
);
}
private handleError(error: any): Observable<never> {
console.error('API Error:', error);
return throwError(() => new Error('API request failed'));
}
}Angular Router enables navigation between components based on URL.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoListComponent } from './components/todo-list/todo-list.component';
import { TodoDetailComponent } from './components/todo-detail/todo-detail.component';
import { AuthGuard } from './guards/auth.guard';
const routes: Routes = [
{
path: '',
redirectTo: '/todos',
pathMatch: 'full',
},
{
path: 'todos',
component: TodoListComponent,
canActivate: [AuthGuard],
},
{
path: 'todos/:id',
component: TodoDetailComponent,
canActivate: [AuthGuard],
},
{
path: '**',
redirectTo: '/todos',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
// Route Guard
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): boolean {
const isAuthenticated = !!localStorage.getItem('token');
if (!isAuthenticated) {
this.router.navigate(['/login']);
}
return isAuthenticated;
}
}Observables are a key part of Angular for handling asynchronous operations.
import { Observable, Subject, BehaviorSubject, interval } from 'rxjs';
import { map, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
// Creating Observables
const observable$ = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
// Subject - multicast observable
const subject$ = new Subject<number>();
subject$.subscribe(value => console.log('Observer 1:', value));
subject$.subscribe(value => console.log('Observer 2:', value));
subject$.next(42);
// BehaviorSubject - always has a current value
const behaviorSubject$ = new BehaviorSubject<string>('initial');
behaviorSubject$.subscribe(value => console.log(value));
behaviorSubject$.next('updated');
// Common Operators
const numbers$ = interval(1000).pipe(
// Transform values
map(x => x * 2),
// Filter values
filter(x => x > 5),
// Flatten nested observables
switchMap(x => new Observable(sub => sub.next(x * 10))),
// Debounce rapid emissions
debounceTime(300),
// Only emit if value changed
distinctUntilChanged()
);
// Combining Observables
import { combineLatest, merge, forkJoin } from 'rxjs';
const combined$ = combineLatest([observable1$, observable2$]);
const merged$ = merge(observable1$, observable2$);
const forked$ = forkJoin([observable1$, observable2$]);Angular's change detection mechanism determines when to update the view.
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
// Default Strategy - checks entire component tree
@Component({
selector: 'app-default',
template: `<p>{{ data }}</p>`,
})
export class DefaultComponent {
data = 'Initial';
}
// OnPush Strategy - only checks when inputs change or events occur
@Component({
selector: 'app-on-push',
template: `<p>{{ data }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OnPushComponent {
@Input() data: string = '';
}
// Manual Change Detection
@Component({
selector: 'app-manual',
template: `<p>{{ data }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ManualComponent {
data = 'Initial';
constructor(private cdr: ChangeDetectorRef) {}
updateData(): void {
this.data = 'Updated';
// Manually trigger change detection
this.cdr.markForCheck();
}
}Let's build a complete todo management application that demonstrates all Angular fundamentals.
todo-app/
├── src/
│ ├── app/
│ │ ├── models/
│ │ │ └── todo.model.ts
│ │ ├── services/
│ │ │ └── todo.service.ts
│ │ ├── components/
│ │ │ ├── todo-list/
│ │ │ ├── todo-item/
│ │ │ ├── todo-form/
│ │ │ └── todo-filter/
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ └── app-routing.module.ts
│ ├── styles.css
│ └── main.ts
├── angular.json
└── package.jsonexport interface Todo {
id: string;
title: string;
description: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate: string;
createdAt: Date;
}
export type FilterType = 'all' | 'active' | 'completed';
export interface TodoStats {
total: number;
active: number;
completed: number;
}import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
import { Todo } from '../models/todo.model';
@Injectable({
providedIn: 'root',
})
export class TodoService {
private apiUrl = '/api/todos';
private todosSubject = new BehaviorSubject<Todo[]>([]);
public todos$ = this.todosSubject.asObservable();
constructor(private http: HttpClient) {
this.loadTodos();
}
loadTodos(): void {
this.http.get<Todo[]>(this.apiUrl)
.pipe(
tap(todos => this.todosSubject.next(todos)),
catchError(error => {
console.error('Error loading todos:', error);
return throwError(() => error);
})
)
.subscribe();
}
getTodos(): Observable<Todo[]> {
return this.todos$;
}
addTodo(todo: Omit<Todo, 'id' | 'createdAt'>): Observable<Todo> {
return this.http.post<Todo>(this.apiUrl, todo)
.pipe(
tap(newTodo => {
const current = this.todosSubject.value;
this.todosSubject.next([newTodo, ...current]);
}),
catchError(error => {
console.error('Error adding todo:', error);
return throwError(() => error);
})
);
}
updateTodo(id: string, updates: Partial<Todo>): Observable<Todo> {
return this.http.patch<Todo>(`${this.apiUrl}/${id}`, updates)
.pipe(
tap(updated => {
const current = this.todosSubject.value;
const index = current.findIndex(t => t.id === id);
if (index !== -1) {
current[index] = updated;
this.todosSubject.next([...current]);
}
}),
catchError(error => {
console.error('Error updating todo:', error);
return throwError(() => error);
})
);
}
deleteTodo(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`)
.pipe(
tap(() => {
const current = this.todosSubject.value;
this.todosSubject.next(current.filter(t => t.id !== id));
}),
catchError(error => {
console.error('Error deleting todo:', error);
return throwError(() => error);
})
);
}
toggleTodo(id: string): Observable<Todo> {
const todo = this.todosSubject.value.find(t => t.id === id);
if (!todo) {
return throwError(() => new Error('Todo not found'));
}
return this.updateTodo(id, { completed: !todo.completed });
}
}import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TodoService } from '../../services/todo.service';
import { Todo, FilterType, TodoStats } from '../../models/todo.model';
import { TodoItemComponent } from '../todo-item/todo-item.component';
import { TodoFormComponent } from '../todo-form/todo-form.component';
import { TodoFilterComponent } from '../todo-filter/todo-filter.component';
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [
CommonModule,
TodoItemComponent,
TodoFormComponent,
TodoFilterComponent,
],
template: `
<div class="todo-container">
<header class="app-header">
<h1>Task Manager</h1>
<p>Organize your tasks efficiently</p>
</header>
<main class="app-main">
<app-todo-form (taskAdded)="onTaskAdded()"></app-todo-form>
<app-todo-filter
[activeFilter]="filter"
[stats]="stats"
(filterChanged)="onFilterChange($event)"
></app-todo-filter>
<div class="task-list" *ngIf="filteredTodos.length > 0">
<app-todo-item
*ngFor="let todo of filteredTodos; trackBy: trackByTodoId"
[todo]="todo"
(toggle)="onToggleTodo(todo.id)"
(delete)="onDeleteTodo(todo.id)"
></app-todo-item>
</div>
<div class="empty-state" *ngIf="filteredTodos.length === 0">
<p>No tasks found. {{ filter !== 'all' ? 'Try changing the filter.' : '' }}</p>
</div>
</main>
</div>
`,
styles: [`
.todo-container {
max-width: 800px;
margin: 0 auto;
}
.app-header {
text-align: center;
color: white;
margin-bottom: 40px;
}
.app-header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.app-header p {
font-size: 1.1rem;
opacity: 0.9;
}
.app-main {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
}
`],
})
export class TodoListComponent implements OnInit {
todos: Todo[] = [];
filter: FilterType = 'all';
stats: TodoStats = { total: 0, active: 0, completed: 0 };
get filteredTodos(): Todo[] {
return this.todos.filter(todo => {
if (this.filter === 'active') return !todo.completed;
if (this.filter === 'completed') return todo.completed;
return true;
});
}
constructor(private todoService: TodoService) {}
ngOnInit(): void {
this.todoService.getTodos().subscribe(todos => {
this.todos = todos;
this.updateStats();
});
}
onTaskAdded(): void {
this.todoService.loadTodos();
}
onToggleTodo(id: string): void {
this.todoService.toggleTodo(id).subscribe();
}
onDeleteTodo(id: string): void {
this.todoService.deleteTodo(id).subscribe();
}
onFilterChange(filter: FilterType): void {
this.filter = filter;
}
private updateStats(): void {
this.stats = {
total: this.todos.length,
active: this.todos.filter(t => !t.completed).length,
completed: this.todos.filter(t => t.completed).length,
};
}
trackByTodoId(index: number, todo: Todo): string {
return todo.id;
}
}import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Todo } from '../../models/todo.model';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule],
template: `
<div class="task-item" [class.completed]="todo.completed">
<div class="task-content">
<input
type="checkbox"
[checked]="todo.completed"
(change)="onToggle()"
class="task-checkbox"
/>
<div class="task-details">
<h3 class="task-title">{{ todo.title }}</h3>
<p class="task-description" *ngIf="todo.description">
{{ todo.description }}
</p>
<div class="task-meta">
<span
class="priority-badge"
[style.backgroundColor]="getPriorityColor(todo.priority)"
>
{{ todo.priority }}
</span>
<span class="due-date" *ngIf="todo.dueDate">
Due: {{ todo.dueDate | date: 'MMM d, y' }}
</span>
</div>
</div>
</div>
<button (click)="onDelete()" class="btn btn-danger btn-small">
Delete
</button>
</div>
`,
styles: [`
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f9fafb;
border-radius: 8px;
border-left: 4px solid #667eea;
transition: all 0.2s;
}
.task-item:hover {
background: #f3f4f6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.task-item.completed {
opacity: 0.6;
border-left-color: #10b981;
}
.task-item.completed .task-title {
text-decoration: line-through;
color: #9ca3af;
}
.task-content {
display: flex;
gap: 12px;
flex: 1;
}
.task-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
margin-top: 2px;
}
.task-details {
flex: 1;
}
.task-title {
font-size: 1rem;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
}
.task-description {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 8px;
}
.task-meta {
display: flex;
gap: 10px;
align-items: center;
}
.priority-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
.due-date {
font-size: 0.875rem;
color: #6b7280;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-small {
padding: 6px 12px;
font-size: 0.875rem;
}
`],
})
export class TodoItemComponent {
@Input() todo!: Todo;
@Output() toggle = new EventEmitter<void>();
@Output() delete = new EventEmitter<void>();
onToggle(): void {
this.toggle.emit();
}
onDelete(): void {
this.delete.emit();
}
getPriorityColor(priority: string): string {
const colors: Record<string, string> = {
low: '#3b82f6',
medium: '#f59e0b',
high: '#ef4444',
};
return colors[priority] || '#3b82f6';
}
}import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TodoService } from '../../services/todo.service';
@Component({
selector: 'app-todo-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
<div class="form-group">
<label for="title">Task Title *</label>
<input
id="title"
type="text"
formControlName="title"
placeholder="Enter task title"
class="form-input"
/>
<div class="error" *ngIf="form.get('title')?.invalid && form.get('title')?.touched">
Title is required and must be at least 3 characters
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
formControlName="description"
placeholder="Enter task description"
class="form-input"
rows="3"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="priority">Priority</label>
<select id="priority" formControlName="priority" class="form-input">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label for="dueDate">Due Date</label>
<input
id="dueDate"
type="date"
formControlName="dueDate"
class="form-input"
/>
</div>
</div>
<button type="submit" [disabled]="form.invalid" class="btn btn-primary">
Add Task
</button>
</form>
`,
styles: [`
.task-form {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #374151;
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
width: 100%;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #ef4444;
font-size: 0.875rem;
margin-top: 4px;
}
@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
}
`],
})
export class TodoFormComponent {
@Output() taskAdded = new EventEmitter<void>();
form: FormGroup;
constructor(
private fb: FormBuilder,
private todoService: TodoService
) {
this.form = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3)]],
description: [''],
priority: ['medium'],
dueDate: [''],
});
}
onSubmit(): void {
if (this.form.valid) {
this.todoService.addTodo(this.form.value).subscribe({
next: () => {
this.form.reset({ priority: 'medium' });
this.taskAdded.emit();
},
error: (error) => {
console.error('Error adding task:', error);
},
});
}
}
}import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FilterType, TodoStats } from '../../models/todo.model';
@Component({
selector: 'app-todo-filter',
standalone: true,
imports: [CommonModule],
template: `
<div class="task-filter">
<div class="filter-buttons">
<button
*ngFor="let filterOption of filters"
(click)="onFilterChange(filterOption)"
[class.active]="activeFilter === filterOption"
class="filter-btn"
>
{{ filterOption | titlecase }}
</button>
</div>
<div class="filter-stats">
<span>Total: {{ stats.total }}</span>
<span>Active: {{ stats.active }}</span>
<span>Completed: {{ stats.completed }}</span>
</div>
</div>
`,
styles: [`
.task-filter {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding: 15px;
background: #f9fafb;
border-radius: 8px;
}
.filter-buttons {
display: flex;
gap: 10px;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid #e5e7eb;
background: white;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.filter-btn.active {
border-color: #667eea;
background: #667eea;
color: white;
}
.filter-stats {
display: flex;
gap: 20px;
font-size: 0.875rem;
color: #6b7280;
}
@media (max-width: 640px) {
.task-filter {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.filter-stats {
width: 100%;
justify-content: space-around;
}
}
`],
})
export class TodoFilterComponent {
@Input() activeFilter: FilterType = 'all';
@Input() stats: TodoStats = { total: 0, active: 0, completed: 0 };
@Output() filterChanged = new EventEmitter<FilterType>();
filters: FilterType[] = ['all', 'active', 'completed'];
onFilterChange(filter: FilterType): void {
this.filterChanged.emit(filter);
}
}import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { TodoListComponent } from './components/todo-list/todo-list.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, TodoListComponent],
template: `
<div class="app-wrapper">
<app-todo-list></app-todo-list>
</div>
`,
styles: [`
.app-wrapper {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
`],
})
export class AppComponent {
title = 'todo-app';
}* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
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;
}
app-root {
display: block;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Focus Styles */
:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
/* Utility Classes */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.text-center {
text-align: center;
}
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 2rem; }
.p-1 { padding: 0.5rem; }
.p-2 { padding: 1rem; }
.p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; }// ✅ Keep components small and focused
// ✅ Use OnPush change detection for performance
// ✅ Implement OnDestroy to clean up subscriptions
// ✅ Use trackBy in *ngFor for performance
// ✅ Prefer composition over inheritance
// ✅ Use standalone components when possible
// ✅ Separate concerns: template, logic, styles
@Component({
selector: 'app-user-card',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent implements OnInit, OnDestroy {
@Input() user!: User;
private destroy$ = new Subject<void>();
ngOnInit(): void {
// Subscribe with takeUntil
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}// ✅ Use providedIn: 'root' for singleton services
// ✅ Separate API calls from business logic
// ✅ Use BehaviorSubject for state management
// ✅ Handle errors consistently
// ✅ Use typed responses
// ✅ Implement proper error handling
@Injectable({
providedIn: 'root',
})
export class DataService {
private dataSubject = new BehaviorSubject<Data[]>([]);
public data$ = this.dataSubject.asObservable();
constructor(private http: HttpClient) {}
loadData(): void {
this.http.get<Data[]>('/api/data')
.pipe(
catchError(error => {
console.error('Error loading data:', error);
return of([]);
})
)
.subscribe(data => this.dataSubject.next(data));
}
}// ✅ Use async pipe in templates
// ✅ Unsubscribe properly with takeUntil
// ✅ Use shareReplay for expensive operations
// ✅ Combine observables with combineLatest
// ✅ Use switchMap for dependent requests
export class SearchComponent {
searchTerm$ = new Subject<string>();
results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term)),
shareReplay(1)
);
constructor(private api: ApiService) {}
}
// Template
// <div *ngFor="let result of results$ | async">{{ result }}</div>// ✅ Use Reactive Forms for complex forms
// ✅ Validate on blur, not on every keystroke
// ✅ Show validation errors only when touched
// ✅ Use custom validators for business logic
// ✅ Disable submit button while form is invalid
@Component({
selector: 'app-form',
template: `
<form [formGroup]="form">
<input formControlName="email" />
<div *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
Invalid email
</div>
<button [disabled]="form.invalid">Submit</button>
</form>
`,
})
export class FormComponent {
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
constructor(private fb: FormBuilder) {}
}// ✅ Test component behavior, not implementation
// ✅ Use TestBed for component testing
// ✅ Mock services and HTTP calls
// ✅ Test user interactions
// ✅ Aim for high coverage
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoComponent } from './todo.component';
describe('TodoComponent', () => {
let component: TodoComponent;
let fixture: ComponentFixture<TodoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TodoComponent],
}).compileComponents();
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should add todo', () => {
component.addTodo('Test');
expect(component.todos.length).toBe(1);
});
});export class BadComponent implements OnInit {
constructor(private service: DataService) {}
ngOnInit(): void {
// ❌ Never unsubscribed - memory leak!
this.service.getData().subscribe(data => {
console.log(data);
});
}
}
// ✅ Correct - Using takeUntil
export class GoodComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(private service: DataService) {}
ngOnInit(): void {
this.service.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => console.log(data));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}// ❌ Default strategy checks entire tree on every event
@Component({
selector: 'app-item',
template: `<p>{{ item.name }}</p>`,
})
export class ItemComponent {
@Input() item: any;
}
// ✅ Correct - OnPush only checks when inputs change
@Component({
selector: 'app-item',
template: `<p>{{ item.name }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
@Input() item: any;
}export class BadComponent {
data: any;
constructor(private service: DataService) {}
ngOnInit(): void {
// ❌ Manual subscription, need to unsubscribe
this.service.getData().subscribe(data => {
this.data = data;
});
}
}
// ✅ Correct - Using async pipe
export class GoodComponent {
data$ = this.service.getData();
constructor(private service: DataService) {}
}
// Template
// <p>{{ data$ | async }}</p>// ❌ No error handling
this.http.get('/api/data').subscribe(data => {
this.data = data;
});
// ✅ Correct - Proper error handling
this.http.get('/api/data').pipe(
catchError(error => {
console.error('Error:', error);
return of([]); // Return default value
})
).subscribe(data => {
this.data = data;
});// ❌ Creates new observable on every change detection
export class BadComponent {
constructor(private service: DataService) {}
getData() {
return this.service.getData(); // Called every render!
}
}
// Template
// <div *ngFor="let item of getData() | async">{{ item }}</div>
// ✅ Correct - Observable created once
export class GoodComponent {
data$ = this.service.getData();
constructor(private service: DataService) {}
}
// Template
// <div *ngFor="let item of data$ | async">{{ item }}</div>Angular is a powerful, comprehensive framework designed for building large-scale, enterprise-grade web applications. Understanding its core concepts—modules, components, services, dependency injection, directives, pipes, forms, HTTP, routing, RxJS, and change detection—is fundamental to becoming a proficient Angular developer.
The todo management application we built demonstrates all these concepts in action. By mastering these fundamentals and following best practices, you'll be able to build scalable, maintainable, and performant Angular applications.
Key takeaways:
Next steps:
Angular is a journey, not a destination. Keep learning, building, and improving your skills. The framework continues to evolve, and staying updated with new features and best practices will make you a better developer.