
Angular Development
- 325 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
Build Angular components, modules, routing, services, and reactive forms with idiomatic patterns for enterprise SPAs and maintainable feature delivery.
About
Guides Claude through Angular-specific implementation: standalone or NgModule components, routing guards, services, HttpClient usage, reactive forms, and RxJS streams. Helps teams ship typed, testable enterprise frontends with consistent folder structure and change detection best practices.
- Component and module structure
- Routing and lazy loading
- Reactive forms and RxJS
- Dependency injection patterns
- Enterprise SPA conventions
Angular Development by the numbers
- 325 all-time installs (skills.sh)
- +19 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #724 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill angular-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 325 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
What it does
Build Angular components, modules, routing, services, and reactive forms with idiomatic patterns for enterprise SPAs and maintainable feature delivery.
Files
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="user-form">
<div class="form-group">
<label for="name">Name:</label>
<input
id="name"
type="text"
formControlName="name"
[class.error]="name.invalid && name.touched"
>
@if (name.invalid && name.touched) {
<div class="error-message">
@if (name.errors?.['required']) {
<span>Name is required</span>
}
@if (name.errors?.['minlength']) {
<span>Name must be at least 3 characters</span>
}
</div>
}
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
type="email"
formControlName="email"
[class.error]="email.invalid && email.touched"
>
@if (email.invalid && email.touched) {
<div class="error-message">
@if (email.errors?.['required']) {
<span>Email is required</span>
}
@if (email.errors?.['email']) {
<span>Invalid email format</span>
}
</div>
}
</div>
<div class="form-group">
<label for="age">Age:</label>
<input
id="age"
type="number"
formControlName="age"
[class.error]="age.invalid && age.touched"
>
@if (age.invalid && age.touched) {
<div class="error-message">
@if (age.errors?.['min']) {
<span>Age must be at least 18</span>
}
@if (age.errors?.['max']) {
<span>Age must be less than 100</span>
}
</div>
}
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
<button type="button" (click)="resetForm()">Reset</button>
</form>
`
})
export class UserFormComponent {
private fb = inject(FormBuilder);
userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18), Validators.max(100)]]
});
// Convenience getters
get name() { return this.userForm.get('name')!; }
get email() { return this.userForm.get('email')!; }
get age() { return this.userForm.get('age')!; }
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// Handle form submission
}
}
resetForm() {
this.userForm.reset();
}
}Custom Validators:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export class CustomValidators {
static passwordMatch(passwordField: string, confirmField: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.get(passwordField);
const confirm = control.get(confirmField);
if (!password || !confirm) {
return null;
}
return password.value === confirm.value ? null : { passwordMismatch: true };
};
}
static noWhitespace(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasWhitespace = value.trim().length === 0;
return hasWhitespace ? { whitespace: true } : null;
};
}
static strongPassword(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasNumber = /\d/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const isLongEnough = value.length >= 8;
const valid = hasNumber && hasUpper && hasLower && hasSpecial && isLongEnough;
return valid ? null : {
weakPassword: {
hasNumber,
hasUpper,
hasLower,
hasSpecial,
isLongEnough
}
};
};
}
}Directives
Directives allow you to attach behavior to elements in the DOM.
Structural Directive:
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
private templateRef = inject(TemplateRef<any>);
private viewContainer = inject(ViewContainerRef);
@Input() set appRepeat(times: number) {
this.viewContainer.clear();
for (let i = 0; i < times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i
});
}
}
}
// Usage:
// <div *appRepeat="5; let i = index">Item {{ i }}</div>Attribute Directive:
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
private el = inject(ElementRef);
@Input() appHighlight = 'yellow';
@Input() defaultColor = 'transparent';
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// Usage:
// <p appHighlight="lightblue" defaultColor="white">Hover me!</p>Pipes
Pipes transform displayed values within templates.
Custom Pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, ellipsis = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}
// Usage:
// {{ longText | truncate:100:'...' }}Async Pipe with Observables:
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable, interval, map } from 'rxjs';
@Component({
selector: 'app-clock',
standalone: true,
imports: [CommonModule],
template: `
<div class="clock">
<h2>Current Time</h2>
<p>{{ time$ | async | date:'medium' }}</p>
</div>
`
})
export class ClockComponent {
time$: Observable<Date> = interval(1000).pipe(
map(() => new Date())
);
}RxJS Integration
Angular extensively uses RxJS for reactive programming patterns.
Observable Patterns:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { map, filter, debounceTime, distinctUntilChanged, switchMap, catchError, retry } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/products';
// BehaviorSubject for state management
private productsSubject = new BehaviorSubject<Product[]>([]);
products$ = this.productsSubject.asObservable();
// Subject for search queries
private searchSubject = new Subject<string>();
constructor() {
this.initializeSearch();
}
private initializeSearch() {
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.searchProducts(query))
).subscribe(products => {
this.productsSubject.next(products);
});
}
search(query: string) {
this.searchSubject.next(query);
}
private searchProducts(query: string): Observable<Product[]> {
return this.http.get<Product[]>(`${this.apiUrl}?q=${query}`).pipe(
retry(3),
catchError(error => {
console.error('Search failed:', error);
return [];
})
);
}
getProductsByCategory(category: string): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.category === category))
);
}
getExpensiveProducts(minPrice: number): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.price >= minPrice))
);
}
}Combining Multiple Observables:
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { combineLatest, map } from 'rxjs';
import { ProductService } from './product.service';
import { UserService } from './user.service';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="dashboard">
@if (dashboardData$ | async; as data) {
<h2>Welcome, {{ data.user.name }}</h2>
<p>Products: {{ data.productCount }}</p>
<p>Total Value: {{ data.totalValue | currency }}</p>
}
</div>
`
})
export class DashboardComponent implements OnInit {
private productService = inject(ProductService);
private userService = inject(UserService);
dashboardData$ = combineLatest([
this.userService.getCurrentUser(),
this.productService.products$
]).pipe(
map(([user, products]) => ({
user,
productCount: products.length,
totalValue: products.reduce((sum, p) => sum + p.price, 0)
}))
);
ngOnInit() {
// Data streams are automatically combined
}
}Modern Angular Patterns
Standalone Components
Standalone components eliminate the need for NgModules in most cases.
Standalone Component Application:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient()
]
}).catch(err => console.error(err));App Component:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<header>
<h1>My Angular App</h1>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>© 2024 My App</p>
</footer>
`,
styles: [`
header {
background: #1976d2;
color: white;
padding: 20px;
}
main {
min-height: 80vh;
padding: 20px;
}
footer {
background: #f5f5f5;
padding: 20px;
text-align: center;
}
`]
})
export class AppComponent {}Control Flow Syntax
Modern Angular uses new control flow syntax with @if, @for, and @switch.
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-control-flow-demo',
standalone: true,
imports: [CommonModule],
template: `
<div class="demo">
<!-- @if directive -->
@if (isLoggedIn()) {
<p>Welcome back!</p>
<button (click)="logout()">Logout</button>
} @else {
<p>Please log in</p>
<button (click)="login()">Login</button>
}
<!-- @for directive -->
<h3>Items:</h3>
@for (item of items(); track item.id) {
<div class="item">
<span>{{ item.name }}</span>
@if ($index === 0) {
<span class="badge">First</span>
}
</div>
} @empty {
<p>No items available</p>
}
<!-- @switch directive -->
<h3>Status: {{ status() }}</h3>
@switch (status()) {
@case ('loading') {
<p>Loading data...</p>
}
@case ('success') {
<p>Data loaded successfully!</p>
}
@case ('error') {
<p>Error loading data</p>
}
@default {
<p>Unknown status</p>
}
}
</div>
`
})
export class ControlFlowDemoComponent {
isLoggedIn = signal(false);
items = signal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
status = signal<'loading' | 'success' | 'error' | 'idle'>('idle');
login() {
this.isLoggedIn.set(true);
}
logout() {
this.isLoggedIn.set(false);
}
}Input and Output with Signals
Modern Angular supports signal-based inputs and outputs.
import { Component, input, output, model } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>{{ email() }}</p>
<p>Active: {{ isActive() }}</p>
<button (click)="handleClick()">Select</button>
<button (click)="toggleActive()">Toggle Active</button>
</div>
`
})
export class UserCardComponent {
// Signal-based input (read-only)
name = input.required<string>();
email = input<string>('');
// Two-way binding with model()
isActive = model(false);
// Signal-based output
userSelected = output<string>();
handleClick() {
this.userSelected.emit(this.name());
}
toggleActive() {
this.isActive.update(active => !active);
}
}
// Parent component usage:
// <app-user-card
// [name]="userName"
// [email]="userEmail"
// [(isActive)]="userActive"
// (userSelected)="onUserSelected($event)"
// />Best Practices from Context7 Research
1. Use Standalone Components
Prefer standalone components over NgModule-based components for better tree-shaking and simpler architecture.
// Good: Standalone component
@Component({
selector: 'app-feature',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class FeatureComponent {}
// Avoid: NgModule-based (legacy pattern)
@NgModule({
declarations: [FeatureComponent],
imports: [CommonModule, FormsModule]
})
export class FeatureModule {}2. Use inject() Function
Prefer the inject() function over constructor injection for cleaner code.
// Good: inject() function
export class MyComponent {
private http = inject(HttpClient);
private router = inject(Router);
}
// Avoid: Constructor injection (still valid but more verbose)
export class MyComponent {
constructor(
private http: HttpClient,
private router: Router
) {}
}3. Leverage Signals for State
Use Signals for reactive state management instead of manually managing observables.
// Good: Signals
export class TodoService {
private todos = signal<Todo[]>([]);
completedCount = computed(() => this.todos().filter(t => t.completed).length);
}
// Avoid: Manual observable management
export class TodoService {
private todosSubject = new BehaviorSubject<Todo[]>([]);
todos$ = this.todosSubject.asObservable();
completedCount$ = this.todos$.pipe(
map(todos => todos.filter(t => t.completed).length)
);
}4. Implement Lazy Loading
Use lazy loading for better performance and faster initial load times.
// Good: Lazy loaded routes
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
}
];
// Avoid: Eager loading everything
import { AdminComponent } from './admin/admin.component';
export const routes: Routes = [
{ path: 'admin', component: AdminComponent }
];5. Use Reactive Forms
Prefer reactive forms over template-driven forms for better testability and type safety.
// Good: Reactive forms
export class MyFormComponent {
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
}
// Avoid: Template-driven forms for complex scenarios
// <form #myForm="ngForm">
// <input name="name" ngModel required>
// </form>6. Unsubscribe from Observables
Always clean up subscriptions to prevent memory leaks.
// Good: Using takeUntilDestroyed (Angular 16+)
export class MyComponent {
private destroyed$ = inject(DestroyRef);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyed$))
.subscribe(data => this.data = data);
}
}
// Alternative: Using async pipe (automatically unsubscribes)
export class MyComponent {
data$ = this.dataService.getData();
}7. Use OnPush Change Detection
Optimize performance with OnPush change detection strategy.
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-optimized',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ data() }}`
})
export class OptimizedComponent {
data = signal('initial value');
}8. Implement Proper Error Handling
Always handle errors in HTTP requests and observables.
export class DataService {
private http = inject(HttpClient);
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Failed to fetch data:', error);
return of([]);
})
);
}
}9. Use TrackBy with ngFor
Improve rendering performance with trackBy functions.
// Good: With trackBy
@Component({
template: `
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
`
})
export class MyComponent {
items = [{ id: 1, name: 'Item 1' }];
}
// Old syntax with trackBy:
// *ngFor="let item of items; trackBy: trackById"10. Type Your Code
Leverage TypeScript's type system for better IDE support and fewer runtime errors.
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export class UserService {
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
updateUser(id: number, updates: Partial<User>): Observable<User> {
return this.http.patch<User>(`/api/users/${id}`, updates);
}
}Performance Optimization
Lazy Loading Modules
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
children: [
{
path: 'analytics',
loadComponent: () => import('./analytics/analytics.component')
.then(m => m.AnalyticsComponent)
}
]
}
];Virtual Scrolling
import { Component } from '@angular/core';
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-virtual-scroll',
standalone: true,
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
@for (item of items; track item) {
<div class="item">{{ item }}</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
}Memoization with Signals
export class DataProcessorService {
private rawData = signal<number[]>([]);
// Computed signals automatically memoize results
processedData = computed(() => {
const data = this.rawData();
// Expensive computation only runs when rawData changes
return data.map(n => n * 2).filter(n => n > 10).sort((a, b) => a - b);
});
statistics = computed(() => {
const data = this.processedData();
return {
count: data.length,
sum: data.reduce((a, b) => a + b, 0),
average: data.length ? data.reduce((a, b) => a + b, 0) / data.length : 0
};
});
}Testing Angular Applications
Component Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [
{ provide: UserService, useValue: userServiceSpy }
]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load users on init', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
userService.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges();
expect(component.users.length).toBe(2);
expect(userService.getUsers).toHaveBeenCalled();
});
});Service Testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' }
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(1);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});Migration Guide
From NgModules to Standalone
// Before: NgModule-based
@NgModule({
declarations: [MyComponent],
imports: [CommonModule, FormsModule],
exports: [MyComponent]
})
export class MyModule {}
// After: Standalone
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class MyComponent {}From Constructor to inject()
// Before: Constructor injection
export class MyService {
constructor(
private http: HttpClient,
private router: Router,
private auth: AuthService
) {}
}
// After: inject() function
export class MyService {
private http = inject(HttpClient);
private router = inject(Router);
private auth = inject(AuthService);
}From BehaviorSubject to Signals
// Before: BehaviorSubject
export class StateService {
private countSubject = new BehaviorSubject<number>(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
// After: Signals
export class StateService {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
}Common Patterns
Master-Detail Pattern
// List component
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="product-list">
@for (product of products(); track product.id) {
<div
class="product-item"
[class.selected]="selectedId() === product.id"
(click)="selectProduct(product.id)"
>
{{ product.name }} - {{ product.price | currency }}
</div>
}
</div>
`
})
export class ProductListComponent {
products = input.required<Product[]>();
selectedId = model<number | null>(null);
selectProduct(id: number) {
this.selectedId.set(id);
}
}
// Parent component
@Component({
selector: 'app-product-master-detail',
standalone: true,
imports: [ProductListComponent, ProductDetailComponent],
template: `
<div class="master-detail">
<app-product-list
[products]="products()"
[(selectedId)]="selectedProductId"
/>
@if (selectedProduct(); as product) {
<app-product-detail [product]="product" />
}
</div>
`
})
export class ProductMasterDetailComponent {
products = signal<Product[]>([]);
selectedProductId = signal<number | null>(null);
selectedProduct = computed(() => {
const id = this.selectedProductId();
return this.products().find(p => p.id === id);
});
}Smart/Presentational Pattern
// Presentational component (dumb)
@Component({
selector: 'app-user-card-presentational',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
<button (click)="edit.emit(user())">Edit</button>
<button (click)="delete.emit(user().id)">Delete</button>
</div>
`
})
export class UserCardPresentationalComponent {
user = input.required<User>();
edit = output<User>();
delete = output<number>();
}
// Smart component (container)
@Component({
selector: 'app-user-list-container',
standalone: true,
imports: [CommonModule, UserCardPresentationalComponent],
template: `
@for (user of users$ | async; track user.id) {
<app-user-card-presentational
[user]="user"
(edit)="handleEdit($event)"
(delete)="handleDelete($event)"
/>
}
`
})
export class UserListContainerComponent {
private userService = inject(UserService);
users$ = this.userService.getUsers();
handleEdit(user: User) {
// Business logic
this.userService.updateUser(user.id, user).subscribe();
}
handleDelete(id: number) {
// Business logic
this.userService.deleteUser(id).subscribe();
}
}Context7 Integration Summary
This skill incorporates best practices from the official Angular documentation (Context7 Trust Score: 8.9), including:
- Standalone Components: Modern approach eliminating NgModules
- inject() Function: Cleaner dependency injection
- Signals: Fine-grained reactive state management
- Control Flow Syntax: @if, @for, @switch directives
- Lazy Loading: Performance optimization patterns
- Reactive Forms: Type-safe form handling
- RxJS Patterns: Observable composition and operators
- Modern Routing: Functional guards and resolvers
- Change Detection: OnPush strategy for performance
- Testing: Component and service testing patterns
All examples follow the latest Angular best practices and patterns recommended in the official documentation, ensuring production-ready, maintainable, and performant Angular applications.
Angular Development Examples
A comprehensive collection of real-world Angular examples demonstrating modern patterns, best practices, and common use cases.
Table of Contents
1. Component Examples 2. Service Examples 3. Routing Examples 4. Forms Examples 5. RxJS Examples 6. Signals Examples 7. Directives Examples 8. Pipes Examples 9. HTTP Client Examples 10. State Management Examples 11. Performance Examples 12. Testing Examples
---
Component Examples
Example 1: Basic Standalone Component with Signals
A simple counter component demonstrating signals and computed values.
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<div class="counter-container">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<p>Is Even: {{ isEven() ? 'Yes' : 'No' }}</p>
<div class="button-group">
<button (click)="increment()" class="btn btn-primary">+</button>
<button (click)="decrement()" class="btn btn-secondary">-</button>
<button (click)="reset()" class="btn btn-danger">Reset</button>
</div>
<div class="history">
<h3>History:</h3>
<ul>
@for (entry of history(); track $index) {
<li>{{ entry }}</li>
}
</ul>
</div>
</div>
`,
styles: [`
.counter-container {
padding: 20px;
border: 2px solid #333;
border-radius: 8px;
max-width: 400px;
margin: 20px auto;
}
.button-group {
display: flex;
gap: 10px;
margin: 20px 0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary { background: #007bff; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
`]
})
export class CounterComponent {
count = signal(0);
history = signal<string[]>([]);
doubleCount = computed(() => this.count() * 2);
isEven = computed(() => this.count() % 2 === 0);
increment() {
this.count.update(n => n + 1);
this.addToHistory(`Incremented to ${this.count()}`);
}
decrement() {
this.count.update(n => n - 1);
this.addToHistory(`Decremented to ${this.count()}`);
}
reset() {
this.count.set(0);
this.addToHistory('Reset to 0');
}
private addToHistory(message: string) {
this.history.update(h => [...h, `${new Date().toLocaleTimeString()}: ${message}`]);
}
}Example 2: Component with Input/Output and Model
Parent-child component communication with signal-based inputs and two-way binding.
// Child Component
import { Component, input, output, model, computed } from '@angular/core';
export interface Task {
id: number;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
@Component({
selector: 'app-task-item',
standalone: true,
template: `
<div class="task-item" [class.completed]="task().completed">
<input
type="checkbox"
[checked]="task().completed"
(change)="toggleComplete()"
>
<span class="task-title">{{ task().title }}</span>
<span class="priority" [class]="'priority-' + task().priority">
{{ task().priority }}
</span>
<button (click)="delete.emit(task().id)" class="btn-delete">
Delete
</button>
</div>
`,
styles: [`
.task-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid #ddd;
margin: 5px 0;
}
.task-item.completed {
opacity: 0.6;
text-decoration: line-through;
}
.priority {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.priority-low { background: #28a745; color: white; }
.priority-medium { background: #ffc107; color: black; }
.priority-high { background: #dc3545; color: white; }
`]
})
export class TaskItemComponent {
task = input.required<Task>();
delete = output<number>();
completed = model(false);
toggleComplete() {
this.completed.update(c => !c);
}
}
// Parent Component
@Component({
selector: 'app-task-list',
standalone: true,
imports: [CommonModule, TaskItemComponent],
template: `
<div class="task-list">
<h2>Tasks ({{ activeCount() }} active, {{ completedCount() }} completed)</h2>
@for (task of tasks(); track task.id) {
<app-task-item
[task]="task"
[(completed)]="task.completed"
(delete)="deleteTask($event)"
/>
} @empty {
<p>No tasks yet!</p>
}
<button (click)="addSampleTask()" class="btn-add">Add Sample Task</button>
</div>
`
})
export class TaskListComponent {
tasks = signal<Task[]>([
{ id: 1, title: 'Learn Angular', completed: false, priority: 'high' },
{ id: 2, title: 'Build a project', completed: false, priority: 'medium' },
{ id: 3, title: 'Write tests', completed: true, priority: 'low' }
]);
activeCount = computed(() =>
this.tasks().filter(t => !t.completed).length
);
completedCount = computed(() =>
this.tasks().filter(t => t.completed).length
);
deleteTask(id: number) {
this.tasks.update(tasks => tasks.filter(t => t.id !== id));
}
addSampleTask() {
const newTask: Task = {
id: Date.now(),
title: `New Task ${this.tasks().length + 1}`,
completed: false,
priority: 'medium'
};
this.tasks.update(tasks => [...tasks, newTask]);
}
}Example 3: Component with Lifecycle Hooks
Comprehensive example showing all major lifecycle hooks.
import {
Component,
OnInit,
OnDestroy,
AfterViewInit,
AfterContentInit,
OnChanges,
SimpleChanges,
input,
signal,
viewChild,
ElementRef
} from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `
<div class="lifecycle-demo">
<h2>Lifecycle Demo: {{ name() }}</h2>
<p>Timer: {{ timer() }}</p>
<div #contentDiv>Content goes here</div>
<div class="logs">
<h3>Lifecycle Logs:</h3>
@for (log of logs(); track $index) {
<div class="log-entry">{{ log }}</div>
}
</div>
</div>
`,
styles: [`
.lifecycle-demo {
padding: 20px;
border: 2px solid #007bff;
border-radius: 8px;
}
.logs {
margin-top: 20px;
max-height: 200px;
overflow-y: auto;
background: #f5f5f5;
padding: 10px;
}
.log-entry {
padding: 4px;
border-bottom: 1px solid #ddd;
font-size: 12px;
}
`]
})
export class LifecycleDemoComponent implements OnInit, OnDestroy, AfterViewInit, AfterContentInit, OnChanges {
name = input.required<string>();
timer = signal(0);
logs = signal<string[]>([]);
contentDiv = viewChild<ElementRef>('contentDiv');
private timerSubscription?: Subscription;
constructor() {
this.addLog('Constructor called');
}
ngOnChanges(changes: SimpleChanges) {
this.addLog(`ngOnChanges: ${JSON.stringify(changes)}`);
}
ngOnInit() {
this.addLog('ngOnInit: Component initialized');
// Start timer
this.timerSubscription = interval(1000).subscribe(() => {
this.timer.update(t => t + 1);
});
}
ngAfterContentInit() {
this.addLog('ngAfterContentInit: Content initialized');
}
ngAfterViewInit() {
this.addLog('ngAfterViewInit: View initialized');
const element = this.contentDiv();
if (element) {
this.addLog(`Content div found: ${element.nativeElement.textContent}`);
}
}
ngOnDestroy() {
this.addLog('ngOnDestroy: Component being destroyed');
this.timerSubscription?.unsubscribe();
}
private addLog(message: string) {
const timestamp = new Date().toLocaleTimeString();
this.logs.update(logs => [...logs, `[${timestamp}] ${message}`]);
}
}---
Service Examples
Example 4: HTTP Service with CRUD Operations
Complete CRUD service with error handling and type safety.
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: string;
}
export interface CreateUserDto {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export interface UpdateUserDto {
name?: string;
email?: string;
role?: 'admin' | 'user' | 'guest';
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/users';
private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
// GET all users
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
retry(3),
catchError(this.handleError)
);
}
// GET user by ID
getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
// POST create user
createUser(user: CreateUserDto): Observable<User> {
return this.http.post<User>(this.apiUrl, user, this.httpOptions).pipe(
catchError(this.handleError)
);
}
// PUT update user (full update)
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${id}`, user, this.httpOptions).pipe(
catchError(this.handleError)
);
}
// PATCH update user (partial update)
patchUser(id: number, updates: UpdateUserDto): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, updates, this.httpOptions).pipe(
catchError(this.handleError)
);
}
// DELETE user
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
// Search users
searchUsers(query: string): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}?q=${encodeURIComponent(query)}`).pipe(
retry(2),
catchError(this.handleError)
);
}
// Get users by role
getUsersByRole(role: string): Observable<User[]> {
return this.getUsers().pipe(
map(users => users.filter(user => user.role === role))
);
}
// Private error handler
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Client Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Server Error Code: ${error.status}\nMessage: ${error.message}`;
switch (error.status) {
case 400:
errorMessage = 'Bad Request: Invalid data provided';
break;
case 401:
errorMessage = 'Unauthorized: Please log in';
break;
case 403:
errorMessage = 'Forbidden: You do not have permission';
break;
case 404:
errorMessage = 'Not Found: Resource does not exist';
break;
case 500:
errorMessage = 'Internal Server Error: Please try again later';
break;
}
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}Example 5: Authentication Service with Token Management
Complete authentication service with JWT token handling.
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, BehaviorSubject, tap, catchError, of } from 'rxjs';
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
name: string;
email: string;
password: string;
}
export interface AuthResponse {
user: User;
token: string;
refreshToken: string;
}
export interface User {
id: number;
name: string;
email: string;
role: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private http = inject(HttpClient);
private router = inject(Router);
private apiUrl = 'https://api.example.com/auth';
// Signals for reactive state
isAuthenticated = signal(false);
currentUser = signal<User | null>(null);
// Observable for legacy compatibility
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
constructor() {
this.checkAuthentication();
}
// Check if user is authenticated on app load
private checkAuthentication() {
const token = this.getToken();
if (token) {
this.validateToken(token).subscribe({
next: (valid) => {
if (valid) {
this.isAuthenticated.set(true);
this.isAuthenticatedSubject.next(true);
this.loadCurrentUser();
} else {
this.logout();
}
},
error: () => this.logout()
});
}
}
// Login
login(credentials: LoginCredentials): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/login`, credentials).pipe(
tap(response => {
this.handleAuthSuccess(response);
}),
catchError(error => {
console.error('Login failed:', error);
throw error;
})
);
}
// Register
register(data: RegisterData): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/register`, data).pipe(
tap(response => {
this.handleAuthSuccess(response);
}),
catchError(error => {
console.error('Registration failed:', error);
throw error;
})
);
}
// Logout
logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
this.isAuthenticated.set(false);
this.currentUser.set(null);
this.isAuthenticatedSubject.next(false);
this.router.navigate(['/login']);
}
// Refresh token
refreshToken(): Observable<AuthResponse> {
const refreshToken = this.getRefreshToken();
return this.http.post<AuthResponse>(`${this.apiUrl}/refresh`, { refreshToken }).pipe(
tap(response => {
this.setToken(response.token);
this.setRefreshToken(response.refreshToken);
})
);
}
// Get current user
getCurrentUser(): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/me`).pipe(
tap(user => {
this.currentUser.set(user);
this.saveUser(user);
})
);
}
// Load current user from storage
private loadCurrentUser() {
const userJson = localStorage.getItem('user');
if (userJson) {
const user = JSON.parse(userJson) as User;
this.currentUser.set(user);
} else {
this.getCurrentUser().subscribe();
}
}
// Validate token
private validateToken(token: string): Observable<boolean> {
return this.http.post<{ valid: boolean }>(`${this.apiUrl}/validate`, { token }).pipe(
tap(response => response.valid),
catchError(() => of({ valid: false }))
).pipe(
tap(response => response.valid)
);
}
// Handle successful authentication
private handleAuthSuccess(response: AuthResponse) {
this.setToken(response.token);
this.setRefreshToken(response.refreshToken);
this.saveUser(response.user);
this.currentUser.set(response.user);
this.isAuthenticated.set(true);
this.isAuthenticatedSubject.next(true);
}
// Token management
getToken(): string | null {
return localStorage.getItem('access_token');
}
private setToken(token: string) {
localStorage.setItem('access_token', token);
}
private getRefreshToken(): string | null {
return localStorage.getItem('refresh_token');
}
private setRefreshToken(token: string) {
localStorage.setItem('refresh_token', token);
}
private saveUser(user: User) {
localStorage.setItem('user', JSON.stringify(user));
}
// Check permissions
hasRole(role: string): boolean {
const user = this.currentUser();
return user?.role === role;
}
hasAnyRole(roles: string[]): boolean {
const user = this.currentUser();
return user ? roles.includes(user.role) : false;
}
}---
Routing Examples
Example 6: Complete Routing Configuration with Guards
Advanced routing setup with lazy loading and multiple guard types.
// app.routes.ts
import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './services/auth.service';
import { Router } from '@angular/router';
// Functional guard for authentication
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login']);
};
// Functional guard for admin role
export const adminGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.hasRole('admin')) {
return true;
}
return router.createUrlTree(['/unauthorized']);
};
// Functional guard to prevent authenticated users from accessing login
export const guestGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/dashboard']);
};
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
title: 'Home'
},
{
path: 'login',
loadComponent: () => import('./pages/auth/login.component').then(m => m.LoginComponent),
canActivate: [guestGuard],
title: 'Login'
},
{
path: 'register',
loadComponent: () => import('./pages/auth/register.component').then(m => m.RegisterComponent),
canActivate: [guestGuard],
title: 'Register'
},
{
path: 'dashboard',
loadComponent: () => import('./pages/dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [authGuard],
title: 'Dashboard'
},
{
path: 'profile',
loadComponent: () => import('./pages/profile/profile.component').then(m => m.ProfileComponent),
canActivate: [authGuard],
title: 'Profile'
},
{
path: 'admin',
canActivate: [authGuard, adminGuard],
children: [
{
path: '',
loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent),
title: 'Admin Dashboard'
},
{
path: 'users',
loadComponent: () => import('./pages/admin/users/users.component').then(m => m.UsersComponent),
title: 'Manage Users'
},
{
path: 'settings',
loadComponent: () => import('./pages/admin/settings/settings.component').then(m => m.SettingsComponent),
title: 'Admin Settings'
}
]
},
{
path: 'products',
children: [
{
path: '',
loadComponent: () => import('./pages/products/product-list.component').then(m => m.ProductListComponent),
title: 'Products'
},
{
path: ':id',
loadComponent: () => import('./pages/products/product-detail.component').then(m => m.ProductDetailComponent),
title: 'Product Details'
},
{
path: ':id/edit',
loadComponent: () => import('./pages/products/product-edit.component').then(m => m.ProductEditComponent),
canActivate: [authGuard],
title: 'Edit Product'
}
]
},
{
path: 'unauthorized',
loadComponent: () => import('./pages/error/unauthorized.component').then(m => m.UnauthorizedComponent),
title: 'Unauthorized'
},
{
path: '404',
loadComponent: () => import('./pages/error/not-found.component').then(m => m.NotFoundComponent),
title: 'Page Not Found'
},
{
path: '**',
redirectTo: '/404'
}
];Example 7: Route Parameters and Query Params
Component demonstrating route parameters and query parameter handling.
import { Component, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { CommonModule } from '@angular/common';
import { switchMap, map } from 'rxjs/operators';
import { ProductService, Product } from '../../services/product.service';
@Component({
selector: 'app-product-detail',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="product-detail">
@if (loading()) {
<p>Loading product...</p>
} @else if (error()) {
<div class="error">{{ error() }}</div>
} @else if (product(); as product) {
<div class="product-card">
<h1>{{ product.name }}</h1>
<p class="price">{{ product.price | currency }}</p>
<p class="description">{{ product.description }}</p>
<p class="category">Category: {{ product.category }}</p>
<div class="actions">
<button (click)="goToEdit()">Edit</button>
<button (click)="goBack()">Back to List</button>
<button (click)="viewRelated()">View Related Products</button>
</div>
@if (showRelated()) {
<div class="related-products">
<h3>Related Products</h3>
@for (related of relatedProducts(); track related.id) {
<a [routerLink]="['/products', related.id]">
{{ related.name }}
</a>
}
</div>
}
</div>
}
</div>
`,
styles: [`
.product-detail {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
.price {
font-size: 24px;
font-weight: bold;
color: #28a745;
}
.actions {
display: flex;
gap: 10px;
margin: 20px 0;
}
.related-products {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
`]
})
export class ProductDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private productService = inject(ProductService);
product = signal<Product | null>(null);
relatedProducts = signal<Product[]>([]);
loading = signal(false);
error = signal('');
showRelated = signal(false);
ngOnInit() {
// Get product ID from route params and load product
this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
this.loading.set(true);
return this.productService.getProduct(id);
})
).subscribe({
next: (product) => {
this.product.set(product);
this.loading.set(false);
// Check query params for showRelated
const queryParams = this.route.snapshot.queryParams;
if (queryParams['showRelated'] === 'true') {
this.viewRelated();
}
},
error: (err) => {
this.error.set('Failed to load product');
this.loading.set(false);
console.error(err);
}
});
}
goToEdit() {
const product = this.product();
if (product) {
this.router.navigate(['/products', product.id, 'edit']);
}
}
goBack() {
// Navigate back with query params preserved
const queryParams = this.route.snapshot.queryParams;
this.router.navigate(['/products'], { queryParams });
}
viewRelated() {
const product = this.product();
if (product) {
this.productService.getProductsByCategory(product.category).subscribe(products => {
this.relatedProducts.set(products.filter(p => p.id !== product.id));
this.showRelated.set(true);
// Update URL with query param
this.router.navigate([], {
relativeTo: this.route,
queryParams: { showRelated: 'true' },
queryParamsHandling: 'merge'
});
});
}
}
}---
Forms Examples
Example 8: Complex Reactive Form with Nested FormGroups
Multi-step form with validation and dynamic form arrays.
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators, FormArray, FormGroup } from '@angular/forms';
interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}
interface PhoneNumber {
type: 'home' | 'work' | 'mobile';
number: string;
}
interface UserProfile {
personalInfo: {
firstName: string;
lastName: string;
email: string;
dateOfBirth: string;
};
address: Address;
phoneNumbers: PhoneNumber[];
preferences: {
newsletter: boolean;
notifications: boolean;
theme: 'light' | 'dark';
};
}
@Component({
selector: 'app-user-profile-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()" class="profile-form">
<h2>User Profile</h2>
<!-- Personal Information -->
<div formGroupName="personalInfo" class="form-section">
<h3>Personal Information</h3>
<div class="form-group">
<label for="firstName">First Name:</label>
<input id="firstName" type="text" formControlName="firstName">
@if (personalInfo.get('firstName')?.invalid && personalInfo.get('firstName')?.touched) {
<div class="error">First name is required (min 2 characters)</div>
}
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<input id="lastName" type="text" formControlName="lastName">
@if (personalInfo.get('lastName')?.invalid && personalInfo.get('lastName')?.touched) {
<div class="error">Last name is required (min 2 characters)</div>
}
</div>
<div class="form-group">
<label for="email">Email:</label>
<input id="email" type="email" formControlName="email">
@if (personalInfo.get('email')?.invalid && personalInfo.get('email')?.touched) {
<div class="error">Valid email is required</div>
}
</div>
<div class="form-group">
<label for="dateOfBirth">Date of Birth:</label>
<input id="dateOfBirth" type="date" formControlName="dateOfBirth">
</div>
</div>
<!-- Address -->
<div formGroupName="address" class="form-section">
<h3>Address</h3>
<div class="form-group">
<label for="street">Street:</label>
<input id="street" type="text" formControlName="street">
</div>
<div class="form-row">
<div class="form-group">
<label for="city">City:</label>
<input id="city" type="text" formControlName="city">
</div>
<div class="form-group">
<label for="state">State:</label>
<input id="state" type="text" formControlName="state">
</div>
<div class="form-group">
<label for="zipCode">Zip Code:</label>
<input id="zipCode" type="text" formControlName="zipCode">
@if (address.get('zipCode')?.invalid && address.get('zipCode')?.touched) {
<div class="error">Valid zip code required (5 digits)</div>
}
</div>
</div>
</div>
<!-- Phone Numbers (FormArray) -->
<div class="form-section">
<h3>Phone Numbers</h3>
<div formArrayName="phoneNumbers">
@for (phone of phoneNumbers.controls; track $index; let i = $index) {
<div [formGroupName]="i" class="phone-entry">
<select formControlName="type">
<option value="home">Home</option>
<option value="work">Work</option>
<option value="mobile">Mobile</option>
</select>
<input type="tel" formControlName="number" placeholder="Phone number">
<button type="button" (click)="removePhone(i)" class="btn-remove">
Remove
</button>
</div>
}
</div>
<button type="button" (click)="addPhone()" class="btn-add">
Add Phone Number
</button>
</div>
<!-- Preferences -->
<div formGroupName="preferences" class="form-section">
<h3>Preferences</h3>
<div class="form-group">
<label>
<input type="checkbox" formControlName="newsletter">
Subscribe to newsletter
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" formControlName="notifications">
Enable notifications
</label>
</div>
<div class="form-group">
<label for="theme">Theme:</label>
<select id="theme" formControlName="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<button type="submit" [disabled]="profileForm.invalid" class="btn-submit">
Save Profile
</button>
<button type="button" (click)="resetForm()" class="btn-reset">
Reset
</button>
<button type="button" (click)="fillSampleData()" class="btn-sample">
Fill Sample Data
</button>
</div>
<!-- Form Status -->
<div class="form-status">
<p>Form Valid: {{ profileForm.valid }}</p>
<p>Form Dirty: {{ profileForm.dirty }}</p>
<p>Form Touched: {{ profileForm.touched }}</p>
</div>
<!-- Form Value Preview -->
@if (showPreview()) {
<div class="form-preview">
<h3>Form Value:</h3>
<pre>{{ profileForm.value | json }}</pre>
</div>
}
</form>
`,
styles: [`
.profile-form {
max-width: 800px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-section {
margin: 20px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 4px;
}
.form-group {
margin: 10px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-group input.ng-invalid.ng-touched {
border-color: #dc3545;
}
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.phone-entry {
display: flex;
gap: 10px;
margin: 10px 0;
}
.error {
color: #dc3545;
font-size: 12px;
margin-top: 4px;
}
.form-actions {
display: flex;
gap: 10px;
margin: 20px 0;
}
.btn-submit {
background: #28a745;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-submit:disabled {
background: #6c757d;
cursor: not-allowed;
}
.form-preview {
margin-top: 20px;
padding: 20px;
background: #e9ecef;
border-radius: 4px;
}
.form-preview pre {
background: white;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
`]
})
export class UserProfileFormComponent {
private fb = inject(FormBuilder);
showPreview = signal(true);
profileForm = this.fb.group({
personalInfo: this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
dateOfBirth: ['']
}),
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zipCode: ['', [Validators.pattern(/^\d{5}$/)]]
}),
phoneNumbers: this.fb.array([]),
preferences: this.fb.group({
newsletter: [false],
notifications: [true],
theme: ['light' as 'light' | 'dark']
})
});
get personalInfo() {
return this.profileForm.get('personalInfo') as FormGroup;
}
get address() {
return this.profileForm.get('address') as FormGroup;
}
get phoneNumbers() {
return this.profileForm.get('phoneNumbers') as FormArray;
}
get preferences() {
return this.profileForm.get('preferences') as FormGroup;
}
addPhone() {
const phoneForm = this.fb.group({
type: ['mobile' as 'home' | 'work' | 'mobile'],
number: ['', [Validators.required, Validators.pattern(/^\d{10}$/)]]
});
this.phoneNumbers.push(phoneForm);
}
removePhone(index: number) {
this.phoneNumbers.removeAt(index);
}
onSubmit() {
if (this.profileForm.valid) {
const formValue = this.profileForm.value as UserProfile;
console.log('Form submitted:', formValue);
// Handle form submission
} else {
console.log('Form is invalid');
this.markFormGroupTouched(this.profileForm);
}
}
resetForm() {
this.profileForm.reset({
preferences: {
newsletter: false,
notifications: true,
theme: 'light'
}
});
this.phoneNumbers.clear();
}
fillSampleData() {
this.profileForm.patchValue({
personalInfo: {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
dateOfBirth: '1990-01-15'
},
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001'
},
preferences: {
newsletter: true,
notifications: true,
theme: 'dark'
}
});
// Add sample phone numbers
this.addPhone();
this.phoneNumbers.at(0).patchValue({
type: 'mobile',
number: '5551234567'
});
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
control?.markAsTouched();
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
}
});
}
}---
Signals Examples
Example 9: Shopping Cart with Signals
Complete shopping cart implementation using signals for state management.
import { Injectable, signal, computed, effect } from '@angular/core';
export interface CartItem {
id: number;
productId: number;
name: string;
price: number;
quantity: number;
imageUrl?: string;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
// Private writable signal
private items = signal<CartItem[]>([]);
// Public readonly signal
readonly cartItems = this.items.asReadonly();
// Computed signals
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
readonly subtotal = computed(() =>
this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
readonly tax = computed(() => this.subtotal() * 0.08);
readonly total = computed(() => this.subtotal() + this.tax());
readonly isEmpty = computed(() => this.items().length === 0);
readonly uniqueItemCount = computed(() => this.items().length);
constructor() {
// Load cart from localStorage
this.loadCart();
// Save cart whenever it changes
effect(() => {
const items = this.items();
this.saveCart(items);
console.log('Cart updated:', items);
});
}
addItem(product: Omit<CartItem, 'quantity'>) {
this.items.update(currentItems => {
const existingItem = currentItems.find(item => item.productId === product.productId);
if (existingItem) {
// Increase quantity if item already exists
return currentItems.map(item =>
item.productId === product.productId
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// Add new item with quantity 1
return [...currentItems, { ...product, 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
)
);
}
incrementQuantity(id: number) {
this.items.update(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity: item.quantity + 1 } : item
)
);
}
decrementQuantity(id: number) {
this.items.update(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity: Math.max(1, item.quantity - 1) } : item
)
);
}
clear() {
this.items.set([]);
}
private saveCart(items: CartItem[]) {
localStorage.setItem('cart', JSON.stringify(items));
}
private loadCart() {
const cartJson = localStorage.getItem('cart');
if (cartJson) {
try {
const items = JSON.parse(cartJson) as CartItem[];
this.items.set(items);
} catch (error) {
console.error('Failed to load cart:', error);
}
}
}
}
// Cart Component
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-shopping-cart',
standalone: true,
imports: [CommonModule],
template: `
<div class="shopping-cart">
<h2>Shopping Cart</h2>
@if (cart.isEmpty()) {
<div class="empty-cart">
<p>Your cart is empty</p>
<button (click)="addSampleItems()">Add Sample Items</button>
</div>
} @else {
<div class="cart-items">
@for (item of cart.cartItems(); track item.id) {
<div class="cart-item">
<div class="item-info">
<h3>{{ item.name }}</h3>
<p class="price">{{ item.price | currency }}</p>
</div>
<div class="quantity-controls">
<button (click)="cart.decrementQuantity(item.id)">-</button>
<span class="quantity">{{ item.quantity }}</span>
<button (click)="cart.incrementQuantity(item.id)">+</button>
</div>
<div class="item-total">
{{ item.price * item.quantity | currency }}
</div>
<button (click)="cart.removeItem(item.id)" class="btn-remove">
Remove
</button>
</div>
}
</div>
<div class="cart-summary">
<div class="summary-row">
<span>Items ({{ cart.itemCount() }}):</span>
<span>{{ cart.subtotal() | currency }}</span>
</div>
<div class="summary-row">
<span>Tax (8%):</span>
<span>{{ cart.tax() | currency }}</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span>{{ cart.total() | currency }}</span>
</div>
<button (click)="checkout()" class="btn-checkout">
Proceed to Checkout
</button>
<button (click)="cart.clear()" class="btn-clear">
Clear Cart
</button>
</div>
}
</div>
`,
styles: [`
.shopping-cart {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.empty-cart {
text-align: center;
padding: 40px;
background: #f9f9f9;
border-radius: 8px;
}
.cart-items {
margin: 20px 0;
}
.cart-item {
display: flex;
align-items: center;
gap: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 10px 0;
}
.item-info {
flex: 1;
}
.quantity-controls {
display: flex;
align-items: center;
gap: 10px;
}
.quantity-controls button {
width: 30px;
height: 30px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
}
.cart-summary {
border-top: 2px solid #ddd;
padding-top: 20px;
margin-top: 20px;
}
.summary-row {
display: flex;
justify-content: space-between;
margin: 10px 0;
}
.summary-row.total {
font-size: 20px;
font-weight: bold;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ddd;
}
.btn-checkout {
width: 100%;
padding: 15px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-top: 15px;
}
.btn-clear {
width: 100%;
padding: 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
`]
})
export class ShoppingCartComponent {
cart = inject(CartService);
addSampleItems() {
this.cart.addItem({
id: 1,
productId: 101,
name: 'Angular Book',
price: 39.99
});
this.cart.addItem({
id: 2,
productId: 102,
name: 'TypeScript Guide',
price: 29.99
});
this.cart.addItem({
id: 3,
productId: 103,
name: 'RxJS Mastery',
price: 34.99
});
}
checkout() {
console.log('Proceeding to checkout with:', {
items: this.cart.cartItems(),
total: this.cart.total()
});
}
}---
RxJS Examples
Example 10: Advanced RxJS Patterns - Real-time Search
Comprehensive search implementation with debouncing, caching, and error handling.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
Observable,
Subject,
BehaviorSubject,
merge,
of,
throwError,
timer
} from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
switchMap,
map,
catchError,
retry,
shareReplay,
tap,
filter,
take
} from 'rxjs/operators';
export interface SearchResult {
id: number;
title: string;
description: string;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class SearchService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/search';
// Search query subject
private searchQuerySubject = new Subject<string>();
// Loading state
private loadingSubject = new BehaviorSubject<boolean>(false);
readonly loading$ = this.loadingSubject.asObservable();
// Error state
private errorSubject = new BehaviorSubject<string | null>(null);
readonly error$ = this.errorSubject.asObservable();
// Cache for search results
private cache = new Map<string, Observable<SearchResult[]>>();
private cacheTimeout = 5 * 60 * 1000; // 5 minutes
// Search results observable
readonly searchResults$: Observable<SearchResult[]> = this.searchQuerySubject.pipe(
// Debounce to avoid excessive API calls
debounceTime(300),
// Only search if query changed
distinctUntilChanged(),
// Filter out empty queries
filter(query => query.trim().length > 0),
// Set loading state
tap(() => {
this.loadingSubject.next(true);
this.errorSubject.next(null);
}),
// Switch to new search, canceling previous
switchMap(query => this.searchWithCache(query)),
// Clear loading state
tap(() => this.loadingSubject.next(false)),
// Share the same observable among multiple subscribers
shareReplay(1)
);
search(query: string) {
this.searchQuerySubject.next(query);
}
private searchWithCache(query: string): Observable<SearchResult[]> {
// Check cache first
const cached = this.cache.get(query);
if (cached) {
console.log('Returning cached results for:', query);
return cached;
}
// Perform actual search
const search$ = this.performSearch(query).pipe(
retry(2),
catchError(error => {
console.error('Search failed:', error);
this.errorSubject.next('Search failed. Please try again.');
return of([]);
}),
shareReplay(1)
);
// Store in cache
this.cache.set(query, search$);
// Clear from cache after timeout
timer(this.cacheTimeout).pipe(take(1)).subscribe(() => {
this.cache.delete(query);
});
return search$;
}
private performSearch(query: string): Observable<SearchResult[]> {
return this.http.get<SearchResult[]>(`${this.apiUrl}?q=${encodeURIComponent(query)}`);
}
clearCache() {
this.cache.clear();
}
}
// Search Component
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="search-container">
<div class="search-box">
<input
type="text"
[(ngModel)]="searchQuery"
(input)="onSearchChange()"
placeholder="Search..."
class="search-input"
>
@if (searchService.loading$ | async) {
<span class="loading-spinner">Loading...</span>
}
</div>
@if (searchService.error$ | async; as error) {
<div class="error-message">{{ error }}</div>
}
<div class="search-results">
@for (result of searchService.searchResults$ | async; track result.id) {
<div class="result-item">
<h3>{{ result.title }}</h3>
<p>{{ result.description }}</p>
<span class="category">{{ result.category }}</span>
</div>
} @empty {
@if (searchQuery && !(searchService.loading$ | async)) {
<p class="no-results">No results found</p>
}
}
</div>
</div>
`,
styles: [`
.search-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
}
.search-box {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 8px;
}
.loading-spinner {
position: absolute;
right: 12px;
top: 12px;
color: #007bff;
}
.result-item {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 10px 0;
}
.result-item h3 {
margin: 0 0 8px 0;
}
.category {
display: inline-block;
padding: 4px 8px;
background: #007bff;
color: white;
border-radius: 4px;
font-size: 12px;
}
`]
})
export class SearchComponent {
searchService = inject(SearchService);
searchQuery = '';
onSearchChange() {
this.searchService.search(this.searchQuery);
}
}---
Directives Examples
Example 11: Custom Tooltip Directive
Advanced attribute directive with dynamic positioning and styling.
import {
Directive,
ElementRef,
HostListener,
Input,
Renderer2,
inject,
OnDestroy
} from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective implements OnDestroy {
private el = inject(ElementRef);
private renderer = inject(Renderer2);
@Input() appTooltip = '';
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
@Input() tooltipDelay = 300;
private tooltipElement: HTMLElement | null = null;
private showTimeout?: number;
@HostListener('mouseenter') onMouseEnter() {
this.showTimeout = window.setTimeout(() => {
this.show();
}, this.tooltipDelay);
}
@HostListener('mouseleave') onMouseLeave() {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
}
this.hide();
}
private show() {
if (!this.appTooltip || this.tooltipElement) return;
// Create tooltip element
this.tooltipElement = this.renderer.createElement('div');
this.renderer.appendChild(
this.tooltipElement,
this.renderer.createText(this.appTooltip)
);
// Style tooltip
this.renderer.addClass(this.tooltipElement, 'custom-tooltip');
this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
this.renderer.setStyle(this.tooltipElement, 'background', '#333');
this.renderer.setStyle(this.tooltipElement, 'color', 'white');
this.renderer.setStyle(this.tooltipElement, 'padding', '8px 12px');
this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
this.renderer.setStyle(this.tooltipElement, 'font-size', '14px');
this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
this.renderer.setStyle(this.tooltipElement, 'white-space', 'nowrap');
// Append to body
this.renderer.appendChild(document.body, this.tooltipElement);
// Position tooltip
this.positionTooltip();
}
private positionTooltip() {
if (!this.tooltipElement) return;
const hostPos = this.el.nativeElement.getBoundingClientRect();
const tooltipPos = this.tooltipElement.getBoundingClientRect();
const scrollPos = window.pageYOffset || document.documentElement.scrollTop;
const scrollPosX = window.pageXOffset || document.documentElement.scrollLeft;
let top = 0;
let left = 0;
switch (this.tooltipPosition) {
case 'top':
top = hostPos.top + scrollPos - tooltipPos.height - 8;
left = hostPos.left + scrollPosX + (hostPos.width - tooltipPos.width) / 2;
break;
case 'bottom':
top = hostPos.bottom + scrollPos + 8;
left = hostPos.left + scrollPosX + (hostPos.width - tooltipPos.width) / 2;
break;
case 'left':
top = hostPos.top + scrollPos + (hostPos.height - tooltipPos.height) / 2;
left = hostPos.left + scrollPosX - tooltipPos.width - 8;
break;
case 'right':
top = hostPos.top + scrollPos + (hostPos.height - tooltipPos.height) / 2;
left = hostPos.right + scrollPosX + 8;
break;
}
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
}
private hide() {
if (this.tooltipElement) {
this.renderer.removeChild(document.body, this.tooltipElement);
this.tooltipElement = null;
}
}
ngOnDestroy() {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
}
this.hide();
}
}
// Usage example:
// <button
// appTooltip="Click to save"
// tooltipPosition="top"
// [tooltipDelay]="500"
// >
// Save
// </button>---
Pipes Examples
Example 12: Custom Pipes Collection
Multiple useful custom pipes for data transformation.
// 1. File Size Pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize',
standalone: true
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number, decimals = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}
// Usage: {{ 1048576 | fileSize }} // Output: 1 MB
// 2. Time Ago Pipe
@Pipe({
name: 'timeAgo',
standalone: true
})
export class TimeAgoPipe implements PipeTransform {
transform(value: string | Date): string {
const date = value instanceof Date ? value : new Date(value);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`;
if (seconds < 2592000) return `${Math.floor(seconds / 604800)} weeks ago`;
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)} months ago`;
return `${Math.floor(seconds / 31536000)} years ago`;
}
}
// Usage: {{ '2024-01-01' | timeAgo }} // Output: 3 months ago
// 3. Safe HTML Pipe
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'safeHtml',
standalone: true
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}
// Usage: <div [innerHTML]="htmlContent | safeHtml"></div>
// 4. Filter Pipe
@Pipe({
name: 'filter',
standalone: true,
pure: false
})
export class FilterPipe implements PipeTransform {
transform<T>(items: T[], searchText: string, property?: keyof T): T[] {
if (!items || !searchText) {
return items;
}
searchText = searchText.toLowerCase();
return items.filter(item => {
if (property) {
const value = item[property];
return String(value).toLowerCase().includes(searchText);
}
// Search in all properties
return Object.values(item as object).some(value =>
String(value).toLowerCase().includes(searchText)
);
});
}
}
// Usage: @for (item of items | filter:searchText:'name'; track item.id)
// 5. Sort Pipe
@Pipe({
name: 'sort',
standalone: true,
pure: false
})
export class SortPipe implements PipeTransform {
transform<T>(items: T[], property?: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
if (!items || items.length === 0) {
return items;
}
const sortedItems = [...items].sort((a, b) => {
const aVal = property ? a[property] : a;
const bVal = property ? b[property] : b;
if (aVal < bVal) return order === 'asc' ? -1 : 1;
if (aVal > bVal) return order === 'asc' ? 1 : -1;
return 0;
});
return sortedItems;
}
}
// Usage: @for (item of items | sort:'name':'asc'; track item.id)---
Testing Examples
Example 13: Comprehensive Component Testing
Complete test suite for a component with dependencies.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { UserListComponent } from './user-list.component';
import { UserService, User } from './user.service';
import { of, throwError } from 'rxjs';
import { signal } from '@angular/core';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
let httpMock: HttpTestingController;
const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin', createdAt: '2024-01-01' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user', createdAt: '2024-01-02' }
];
beforeEach(async () => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers', 'deleteUser']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{ provide: UserService, useValue: userServiceSpy }
]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
httpMock = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
afterEach(() => {
httpMock.verify();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load users on init', () => {
userService.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges(); // triggers ngOnInit
expect(userService.getUsers).toHaveBeenCalled();
expect(component.users().length).toBe(2);
expect(component.loading()).toBe(false);
});
it('should handle error when loading users fails', () => {
const errorMessage = 'Failed to load users';
userService.getUsers.and.returnValue(throwError(() => new Error(errorMessage)));
fixture.detectChanges();
expect(component.error()).toBeTruthy();
expect(component.loading()).toBe(false);
});
it('should delete user', () => {
component.users.set(mockUsers);
userService.deleteUser.and.returnValue(of(void 0));
component.deleteUser(1);
expect(userService.deleteUser).toHaveBeenCalledWith(1);
});
it('should filter users by search term', () => {
component.users.set(mockUsers);
component.searchTerm.set('Jane');
fixture.detectChanges();
const filtered = component.filteredUsers();
expect(filtered.length).toBe(1);
expect(filtered[0].name).toBe('Jane Smith');
});
it('should render user list', () => {
component.users.set(mockUsers);
fixture.detectChanges();
const compiled = fixture.nativeElement;
const userItems = compiled.querySelectorAll('.user-item');
expect(userItems.length).toBe(2);
});
it('should show loading state', () => {
component.loading.set(true);
fixture.detectChanges();
const compiled = fixture.nativeElement;
const loadingElement = compiled.querySelector('.loading');
expect(loadingElement).toBeTruthy();
expect(loadingElement.textContent).toContain('Loading');
});
it('should show error message', () => {
const errorMsg = 'Error loading users';
component.error.set(errorMsg);
fixture.detectChanges();
const compiled = fixture.nativeElement;
const errorElement = compiled.querySelector('.error');
expect(errorElement).toBeTruthy();
expect(errorElement.textContent).toContain(errorMsg);
});
});---
This comprehensive examples collection demonstrates modern Angular development patterns using Context7-researched best practices, including standalone components, signals, inject() function, reactive forms, RxJS operators, and thorough testing strategies.
Angular Development Skill
A comprehensive skill for building modern Angular applications with standalone components, signals, reactive forms, routing, and RxJS integration.
Overview
This skill provides deep expertise in Angular framework development, covering everything from basic component creation to advanced patterns like lazy loading, state management with signals, and reactive programming with RxJS. Based on official Angular documentation (Context7 Trust Score: 8.9), it incorporates the latest best practices and modern patterns.
What This Skill Covers
Core Angular Concepts
- Components: Standalone components, lifecycle hooks, component communication
- Services: Dependency injection, providedIn configuration, service patterns
- Directives: Structural and attribute directives, custom directive creation
- Pipes: Built-in pipes, custom pipe implementation, pure vs impure pipes
- Modules: Migration from NgModules to standalone components
Modern Angular Features
- Signals: Reactive state management with signals, computed values, effects
- Control Flow: New @if, @for, @switch syntax replacing ngIf, ngFor, *ngSwitch
- inject() Function: Modern dependency injection replacing constructor injection
- Standalone Components: Module-less architecture for better tree-shaking
- Signal-based Inputs/Outputs: Modern component communication patterns
Routing and Navigation
- Lazy Loading: Route-level code splitting for performance
- Guards: Functional and class-based route guards
- Resolvers: Pre-fetching data before route activation
- Route Parameters: Accessing and reacting to route params
- Child Routes: Nested routing patterns
Forms
- Reactive Forms: FormBuilder, FormGroup, FormControl, FormArray
- Validation: Built-in validators, custom validators, async validators
- Dynamic Forms: Programmatic form generation
- Form State: Tracking touched, dirty, valid states
- Custom Form Controls: ControlValueAccessor implementation
RxJS Integration
- Observables: Creating and subscribing to observables
- Operators: map, filter, switchMap, combineLatest, debounceTime, etc.
- Subjects: BehaviorSubject, Subject, ReplaySubject patterns
- Error Handling: retry, catchError operators
- Subscription Management: takeUntilDestroyed, async pipe
HTTP and Data
- HttpClient: GET, POST, PUT, PATCH, DELETE operations
- Interceptors: Request/response interception
- Error Handling: Global and local error handling
- Type Safety: Typed HTTP responses
- Caching: Response caching strategies
State Management
- Signals: Modern reactive state with fine-grained updates
- Services: Service-based state management
- BehaviorSubject: Observable state patterns (legacy)
- Computed Values: Derived state with signals
- Effects: Side effects triggered by signal changes
Performance Optimization
- Lazy Loading: Route and module lazy loading
- OnPush Change Detection: Optimized change detection strategy
- TrackBy: Efficient list rendering
- Virtual Scrolling: CDK virtual scroll for large lists
- Memoization: Computed signals for expensive calculations
Testing
- Component Tests: TestBed, ComponentFixture, async testing
- Service Tests: HttpClientTestingModule, mock services
- Dependency Injection: Testing with DI
- Spy Objects: Jasmine spies and mocks
- E2E Testing: End-to-end testing patterns
When to Use This Skill
Use this skill when you need to:
- Build a new Angular application from scratch
- Migrate an existing Angular app to modern patterns (standalone, signals, inject())
- Implement complex forms with validation
- Set up routing with lazy loading and guards
- Integrate RxJS for reactive programming
- Optimize Angular application performance
- Implement state management patterns
- Create reusable components and services
- Set up HTTP client with interceptors
- Write tests for Angular components and services
- Build enterprise-scale Angular applications
- Implement modern Angular best practices
Quick Start Examples
Creating a Standalone Component
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-welcome',
standalone: true,
imports: [CommonModule],
template: `
<div class="welcome">
<h1>{{ title() }}</h1>
<p>{{ message() }}</p>
<button (click)="updateMessage()">Update</button>
</div>
`
})
export class WelcomeComponent {
title = signal('Welcome');
message = signal('Hello, Angular!');
updateMessage() {
this.message.set('Message updated!');
}
}Creating a Service with inject()
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private http = inject(HttpClient);
getData(): Observable<any[]> {
return this.http.get<any[]>('/api/data');
}
}Setting Up Lazy Loading Routes
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'home',
loadComponent: () => import('./home/home.component')
.then(m => m.HomeComponent)
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
}
];Building a Reactive Form
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="Name">
<input formControlName="email" placeholder="Email">
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`
})
export class ContactFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}Key Patterns and Best Practices
1. Use Standalone Components
Standalone components simplify the architecture and improve tree-shaking:
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class MyComponent {}2. Leverage the inject() Function
The inject() function provides cleaner dependency injection:
export class MyService {
private http = inject(HttpClient);
private router = inject(Router);
}3. Use Signals for State Management
Signals provide fine-grained reactivity with automatic dependency tracking:
export class CounterService {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment() {
this.count.update(n => n + 1);
}
}4. Implement Lazy Loading
Lazy load routes for better performance:
{
path: 'admin',
loadComponent: () => import('./admin/admin.component')
.then(m => m.AdminComponent)
}5. Use Reactive Forms
Reactive forms provide better type safety and testability:
form = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
password: ['', [Validators.required, Validators.minLength(8)]]
});6. Handle Subscriptions Properly
Always clean up subscriptions to prevent memory leaks:
// Use async pipe (automatically unsubscribes)
data$ = this.service.getData();
// Or use takeUntilDestroyed
ngOnInit() {
this.service.getData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => this.data = data);
}7. Use OnPush Change Detection
Optimize performance with OnPush strategy:
@Component({
selector: 'app-optimized',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
export class OptimizedComponent {}8. Type Everything
Leverage TypeScript's type system:
interface User {
id: number;
name: string;
email: string;
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}9. Use TrackBy Functions
Improve rendering performance with trackBy:
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}10. Implement Proper Error Handling
Always handle errors in HTTP requests:
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Error:', error);
return of([]);
})
);
}Modern Angular Architecture
Application Bootstrap
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor])
)
]
});Feature-Based Organization
src/app/
├── core/
│ ├── services/
│ ├── guards/
│ ├── interceptors/
│ └── models/
├── features/
│ ├── users/
│ │ ├── components/
│ │ ├── services/
│ │ └── models/
│ └── products/
│ ├── components/
│ ├── services/
│ └── models/
└── shared/
├── components/
├── directives/
└── pipes/Smart vs Presentational Components
Presentational (Dumb) Component:
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ user().name }}</h3>
<button (click)="edit.emit(user())">Edit</button>
</div>
`
})
export class UserCardComponent {
user = input.required<User>();
edit = output<User>();
}Smart (Container) Component:
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent],
template: `
@for (user of users$ | async; track user.id) {
<app-user-card
[user]="user"
(edit)="handleEdit($event)"
/>
}
`
})
export class UserListComponent {
private userService = inject(UserService);
users$ = this.userService.getUsers();
handleEdit(user: User) {
this.userService.updateUser(user.id, user).subscribe();
}
}Migration Strategies
From NgModules to Standalone
Before:
@NgModule({
declarations: [MyComponent],
imports: [CommonModule],
exports: [MyComponent]
})
export class MyModule {}After:
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule],
template: `...`
})
export class MyComponent {}From Constructor Injection to inject()
Before:
constructor(
private http: HttpClient,
private router: Router
) {}After:
private http = inject(HttpClient);
private router = inject(Router);From *ngIf to @if
Before:
<div *ngIf="isVisible">Content</div>
<div *ngIf="isVisible; else elseBlock">Content</div>
<ng-template #elseBlock>Else content</ng-template>After:
@if (isVisible) {
<div>Content</div>
}
@if (isVisible) {
<div>Content</div>
} @else {
<div>Else content</div>
}From *ngFor to @for
Before:
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>After:
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}From BehaviorSubject to Signals
Before:
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}After:
count = signal(0);
increment() {
this.count.update(n => n + 1);
}Common Use Cases
Authentication Flow
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private router = inject(Router);
isAuthenticated = signal(false);
currentUser = signal<User | null>(null);
login(credentials: { email: string; password: string }) {
return this.http.post<{ user: User; token: string }>('/api/login', credentials)
.pipe(
tap(response => {
localStorage.setItem('token', response.token);
this.currentUser.set(response.user);
this.isAuthenticated.set(true);
})
);
}
logout() {
localStorage.removeItem('token');
this.currentUser.set(null);
this.isAuthenticated.set(false);
this.router.navigate(['/login']);
}
}CRUD Operations
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private apiUrl = '/api/products';
getAll(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
getById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
create(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product);
}
update(id: number, product: Partial<Product>): Observable<Product> {
return this.http.patch<Product>(`${this.apiUrl}/${id}`, product);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}Real-time Search
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<input [formControl]="searchControl" placeholder="Search...">
@for (result of results$ | async; track result.id) {
<div>{{ result.name }}</div>
}
`
})
export class SearchComponent {
private searchService = inject(SearchService);
searchControl = new FormControl('');
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query =>
query ? this.searchService.search(query) : of([])
)
);
}Testing Patterns
Component Test
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [{ provide: UserService, useValue: spy }]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should load users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
userService.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges();
expect(component.users.length).toBe(1);
});
});Service Test
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch data', () => {
const mockData = [{ id: 1 }];
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('/api/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});Resources
- Official Angular Documentation
- Angular Blog
- Angular GitHub Repository
- RxJS Documentation
- Angular Style Guide
Context7 Integration
This skill is based on comprehensive research from the official Angular repository (Context7 Trust Score: 8.9), incorporating:
- Latest standalone component patterns
- Modern dependency injection with inject()
- Signal-based reactive state management
- New control flow syntax (@if, @for, @switch)
- Lazy loading best practices
- Reactive forms patterns
- RxJS integration strategies
- Performance optimization techniques
- Testing methodologies
- Migration paths from legacy patterns
All examples and patterns follow the official Angular team's recommendations and best practices, ensuring your Angular applications are built with industry-standard approaches that are maintainable, performant, and future-proof.