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.