All Articles
Angular

Angular Complete Guide: Building Modern Web Applications

Master Angular framework with components, services, and reactive programming

16-20 minutes

Angular Complete Guide: Building Modern Web Applications

Angular is a powerful TypeScript-based framework for building dynamic web applications. This comprehensive guide covers everything you need to know to build production-ready Angular applications.

Why Choose Angular?

  • 🎯 Complete Framework - Everything you need out of the box
  • 📦 TypeScript First - Strong typing and better tooling
  • 🔄 Reactive Programming - Built-in RxJS support
  • 🏗️ Component-Based - Reusable and maintainable code
  • 🚀 Performance - Ahead-of-Time compilation and lazy loading

Setting Up Your Angular Environment

# Install Angular CLI globally
npm install -g @angular/cli

# Create a new Angular project
ng new my-angular-app

# Navigate to project directory
cd my-angular-app

# Serve the application
ng serve

Components: The Building Blocks

Creating a Component

import { Component } from '@angular/core';

@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
  userName: string = 'John Doe';
  userEmail: string = 'john@example.com';
  isActive: boolean = true;

  toggleStatus(): void {
    this.isActive = !this.isActive;
  }
}

Component Template

<div class="user-card" [class.active]="isActive">
  <h2>{{ userName }}</h2>
  <p>{{ userEmail }}</p>
  <button (click)="toggleStatus()">
    {{ isActive ? 'Deactivate' : 'Activate' }}
  </button>
</div>

Services and Dependency Injection

Creating a Service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }

  updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

interface User {
  id: number;
  name: string;
  email: string;
}

Using the Service

import { Component, OnInit } from '@angular/core';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserListComponent implements OnInit {
  users: User[] = [];

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.userService.getUsers().subscribe({
      next: (data) => this.users = data,
      error: (error) => console.error('Error fetching users', error)
    });
  }
}

Reactive Forms

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html'
})
export class UserFormComponent implements OnInit {
  userForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      age: ['', [Validators.required, Validators.min(18)]],
      address: this.fb.group({
        street: [''],
        city: [''],
        zipCode: ['']
      })
    });
  }

  onSubmit(): void {
    if (this.userForm.valid) {
      console.log(this.userForm.value);
    }
  }

  get name() {
    return this.userForm.get('name');
  }

  get email() {
    return this.userForm.get('email');
  }
}

Form Template

<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div>
    <label>Name:</label>
    <input formControlName="name" />
    <div *ngIf="name?.invalid && name?.touched">
      <span *ngIf="name?.errors?.['required']">Name is required</span>
      <span *ngIf="name?.errors?.['minlength']">Minimum 3 characters</span>
    </div>
  </div>

  <div>
    <label>Email:</label>
    <input formControlName="email" type="email" />
    <div *ngIf="email?.invalid && email?.touched">
      <span *ngIf="email?.errors?.['required']">Email is required</span>
      <span *ngIf="email?.errors?.['email']">Invalid email format</span>
    </div>
  </div>

  <button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>

RxJS and Observables

import { Component, OnInit } from '@angular/core';
import { Observable, of, interval } from 'rxjs';
import { map, filter, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-rxjs-demo',
  template: `<input #searchBox (input)="search(searchBox.value)" />`
})
export class RxjsDemoComponent implements OnInit {
  searchResults$!: Observable<string[]>;

  ngOnInit(): void {
    // Example: Transform data with operators
    const numbers$ = of(1, 2, 3, 4, 5);

    numbers$.pipe(
      filter(n => n % 2 === 0),
      map(n => n * 2)
    ).subscribe(result => console.log(result));
  }

  search(term: string): void {
    // Debounce search input
    of(term).pipe(
      debounceTime(300),
      distinctUntilChanged()
    ).subscribe(searchTerm => {
      console.log('Searching for:', searchTerm);
    });
  }
}

Routing

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { AuthGuard } from './guards/auth.guard';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'users', component: UserListComponent },
  { path: 'users/:id', component: UserDetailComponent },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canActivate: [AuthGuard]
  },
  { path: '**', redirectTo: '' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

State Management with NgRx

// actions/user.actions.ts
import { createAction, props } from '@ngrx/store';

export const loadUsers = createAction('[User List] Load Users');
export const loadUsersSuccess = createAction(
  '[User List] Load Users Success',
  props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
  '[User List] Load Users Failure',
  props<{ error: any }>()
);

// reducers/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as UserActions from '../actions/user.actions';

export interface UserState {
  users: User[];
  loading: boolean;
  error: any;
}

const initialState: UserState = {
  users: [],
  loading: false,
  error: null
};

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, state => ({ ...state, loading: true })),
  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false
  })),
  on(UserActions.loadUsersFailure, (state, { error }) => ({
    ...state,
    error,
    loading: false
  }))
);

Best Practices

1. Use OnPush Change Detection

@Component({
  selector: 'app-optimized',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<div>{{ data }}</div>`
})
export class OptimizedComponent {
  @Input() data!: string;
}

2. Unsubscribe from Observables

import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-cleanup',
  template: `<div>Component with cleanup</div>`
})
export class CleanupComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit(): void {
    this.userService.getUsers()
      .pipe(takeUntil(this.destroy$))
      .subscribe(users => console.log(users));
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

3. Use Async Pipe

@Component({
  selector: 'app-async-demo',
  template: `
    <div *ngIf="users$ | async as users">
      <div *ngFor="let user of users">{{ user.name }}</div>
    </div>
  `
})
export class AsyncDemoComponent {
  users$ = this.userService.getUsers();

  constructor(private userService: UserService) {}
}

Testing

import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should fetch users', () => {
    const mockUsers = [{ id: 1, name: 'John', email: 'john@example.com' }];

    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('https://api.example.com/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });

  afterEach(() => {
    httpMock.verify();
  });
});

Conclusion

Angular provides a complete solution for building modern web applications with TypeScript, reactive programming, and a robust ecosystem. Master these concepts to build scalable, maintainable applications.

Resources

Enjoyed this article?

Explore more deep dives into architecture, performance, and modern .NET.

View All Articles