Angular Development Skill
When to Use This Skill
Use this skill when working with Angular applications, including:
- Building modern Angular applications with standalone components
- Creating reactive UIs with Angular's component system
- Implementing dependency injection patterns
- Setting up routing with lazy loading and guards
- Building reactive forms with validation
- Managing state with Signals and RxJS
- Creating custom directives and pipes
- Implementing HTTP client integrations
- Migrating from older Angular patterns to modern approaches
- Optimizing Angular applications for performance
- Setting up Angular projects with best practices
Core Concepts
Components
Components are the fundamental building blocks of Angular applications. They control a portion of the screen called a view.
Modern Standalone Component Pattern:
typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule],
template: `
<div class="profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="updateProfile()">Update</button>
</div>
`,
styles: [`
.profile {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserProfileComponent {
user = {
name: 'John Doe',
email: 'john@example.com'
};
updateProfile() {
console.log('Updating profile...');
}
}
Component Lifecycle Hooks:
typescript
import { Component, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<div>{{ message }}</div>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy, AfterViewInit {
message = '';
private subscription?: Subscription;
ngOnInit() {
// Called once after component initialization
console.log('Component initialized');
this.message = 'Component ready';
}
ngAfterViewInit() {
// Called after view initialization
console.log('View initialized');
}
ngOnDestroy() {
// Called before component destruction
console.log('Component destroyed');
this.subscription?.unsubscribe();
}
}
Services and Dependency Injection
Services provide shared functionality across components. Angular's dependency injection system makes services available throughout your application.
Modern Injectable Service with inject() Function:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root' // Singleton service available app-wide
})
export class UserService {
// Modern inject() function instead of constructor injection
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/users';
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: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
updateUser(id: number, user: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Using Services in Components:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-list">
<h2>Users</h2>
@if (loading) {
<p>Loading...</p>
} @else if (error) {
<p class="error">{{ error }}</p>
} @else {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
}
</div>
`
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
loading = false;
error = '';
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users';
this.loading = false;
console.error(err);
}
});
}
}
Signals - Modern Reactive State Management
Signals provide a new way to manage reactive state in Angular with fine-grained reactivity.
typescript
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<div class="counter">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<p>Status: {{ status() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal - automatically updates when count changes
doubleCount = computed(() => this.count() * 2);
status = computed(() => {
const value = this.count();
if (value < 0) return 'Negative';
if (value === 0) return 'Zero';
return 'Positive';
});
constructor() {
// Effect runs whenever signals it reads change
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}
Advanced Signals Pattern - Shopping Cart:
typescript
import { Injectable, signal, computed } from '@angular/core';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private items = signal<CartItem[]>([]);
// Computed values
totalItems = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
// Read-only access to items
getItems = this.items.asReadonly();
addItem(item: Omit<CartItem, 'quantity'>) {
this.items.update(currentItems => {
const existing = currentItems.find(i => i.id === item.id);
if (existing) {
return currentItems.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...currentItems, { ...item, quantity: 1 }];
});
}
removeItem(id: number) {
this.items.update(currentItems =>
currentItems.filter(item => item.id !== id)
);
}
updateQuantity(id: number, quantity: number) {
if (quantity <= 0) {
this.removeItem(id);
return;
}
this.items.update(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
clear() {
this.items.set([]);
}
}
Routing
Angular's router enables navigation between views and lazy loading of feature modules.
Modern Route Configuration with Lazy Loading:
typescript
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
},
{
path: 'users/:id',
loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent)
},
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
canActivate: [(route, state) => inject(AuthGuard).canActivate(route, state)]
},
{
path: '**',
loadComponent: () => import('./not-found/not-found.component').then(m => m.NotFoundComponent)
}
];
Route Guards with inject() Function:
typescript
import { Injectable, inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
// Functional guard (modern approach)
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Class-based guard (traditional approach)
@Injectable({
providedIn: 'root'
})
export class AuthGuard {
private authService = inject(AuthService);
private router = inject(Router);
canActivate(route: any, state: any): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}
Router with Route Parameters:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { UserService, User } from '../services/user.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-detail',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-detail">
@if (user) {
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<button (click)="goBack()">Back</button>
<button (click)="editUser()">Edit</button>
} @else {
<p>Loading user...</p>
}
</div>
`
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private userService = inject(UserService);
user?: User;
ngOnInit() {
this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
return this.userService.getUserById(id);
})
).subscribe(user => {
this.user = user;
});
}
goBack() {
this.router.navigate(['/users']);
}
editUser() {
this.router.navigate(['/users', this.user?.id, 'edit']);
}
}
Reactive Forms
Reactive forms provide a model-driven approach to handling form inputs with built-in validation.
Form with Validation:
typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';