Angular Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

Angular Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

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.

AI Agent
AI AgentFebruary 27, 2026
0 views
16 min read

Introduction

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.

Why Angular Exists

The Problem Before Angular

Before Angular, building large-scale web applications was challenging:

  • Lack of Structure: No standardized way to organize code in large applications
  • Two-Way Binding Issues: Manual synchronization between model and view was error-prone
  • Dependency Management: Managing dependencies between components was complex
  • Testing Difficulty: Testing large applications was cumbersome without proper architecture
  • Scalability: Applications became difficult to maintain as they grew
  • Code Reusability: Sharing logic across components was not straightforward

Angular's Solution

Google created Angular (originally AngularJS in 2010, then Angular 2+ in 2016) to solve these problems:

  • Opinionated Architecture: Provides a complete framework with clear structure and patterns
  • TypeScript First: Built with TypeScript for type safety and better tooling
  • Dependency Injection: Built-in DI container for managing dependencies
  • Modular Design: NgModule system for organizing code into logical units
  • Powerful Templating: Advanced template syntax with two-way binding
  • RxJS Integration: Reactive programming with Observables for handling async operations
  • Comprehensive Tooling: Angular CLI for scaffolding, building, and testing
  • Enterprise Ready: Designed for large teams and complex applications

Core Concepts

1. Modules (NgModule)

Modules are containers for a cohesive block of code dedicated to an application domain, workflow, or closely related set of capabilities.

Basic NgModule
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 { }

2. Components

Components are the basic building blocks of Angular applications. A component controls a patch of screen called a view.

Component with Decorator
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();
  }
}

3. Services and Dependency Injection

Services are classes with a focused purpose. They're used to share data and logic across components.

Service with Dependency Injection
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));
        })
      );
  }
}

4. Directives

Directives are classes that add new behavior to the DOM or modify existing behavior.

Structural and Attribute Directives
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>
*/

5. Pipes

Pipes transform data for display in templates.

Built-in and Custom Pipes
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>
*/

6. Forms

Angular provides two approaches to handling forms: Reactive Forms and Template-driven Forms.

Reactive 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();
    }
  }
}

7. HTTP Client

The HttpClient service is used to communicate with backend servers over HTTP.

HTTP Client Usage
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'));
  }
}

8. Routing

Angular Router enables navigation between components based on URL.

Routing Configuration
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;
  }
}

9. RxJS and Observables

Observables are a key part of Angular for handling asynchronous operations.

RxJS Observables and Operators
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$]);

10. Change Detection

Angular's change detection mechanism determines when to update the view.

Change Detection Strategies
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();
  }
}

Practical Application: Todo Management App

Let's build a complete todo management application that demonstrates all Angular fundamentals.

Project Structure

plaintext
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.json

Step 1: Define Models and Interfaces

src/app/models/todo.model.ts
export 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;
}

Step 2: Create TodoService with HTTP

src/app/services/todo.service.ts
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 });
  }
}

Step 3: Create TodoListComponent

src/app/components/todo-list/todo-list.component.ts
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;
  }
}

Step 4: Create TodoItemComponent

src/app/components/todo-item/todo-item.component.ts
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';
  }
}

Step 5: Create TodoFormComponent

src/app/components/todo-form/todo-form.component.ts
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);
        },
      });
    }
  }
}

Step 6: Create TodoFilterComponent

src/app/components/todo-filter/todo-filter.component.ts
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);
  }
}

Step 7: Setup Routing and Main AppComponent

src/app/app.component.ts
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';
}

Step 8: Complete Styling

src/styles.css
* {
  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; }

Best Practices

1. Component Design

Component Best Practices
// ✅ 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();
  }
}

2. Service Architecture

Service Best Practices
// ✅ 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));
  }
}

3. Reactive Programming

RxJS Best Practices
// ✅ 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>

4. Form Handling

Form Best Practices
// ✅ 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) {}
}

5. Testing

Testing Best Practices
// ✅ 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);
  });
});

Common Mistakes & Pitfalls

1. Memory Leaks from Unsubscribed Observables

❌ Wrong - Memory Leak
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();
  }
}

2. Incorrect Change Detection Strategy

❌ Wrong - Performance Issue
// ❌ 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;
}

3. Subscribing in Templates Without Async Pipe

❌ Wrong - Manual Subscription
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>

4. Not Handling HTTP Errors

❌ Wrong - No Error Handling
// ❌ 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;
});

5. Creating New Observables in Templates

❌ Wrong - New Observable Each Render
// ❌ 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>

Conclusion

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:

  1. Angular provides a complete, opinionated framework for large applications
  2. Modules organize code into logical, reusable units
  3. Components are the building blocks with clear separation of concerns
  4. Services and DI enable code reusability and testability
  5. RxJS and Observables handle asynchronous operations elegantly
  6. Reactive Forms provide powerful form handling capabilities
  7. Change detection strategy significantly impacts performance
  8. Proper error handling and subscription management prevent bugs
  9. Testing is built into Angular's architecture
  10. Following best practices leads to maintainable, scalable code

Next steps:

  1. Build small projects to practice fundamentals
  2. Learn state management with NgRx or Akita
  3. Explore advanced patterns (smart/dumb components, facade pattern)
  4. Master performance optimization techniques
  5. Learn testing strategies with Jasmine and Karma
  6. Explore Angular Universal for server-side rendering
  7. Study security best practices (XSS, CSRF protection)
  8. Learn deployment strategies and CI/CD integration

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.


Related Posts