Agent skill
angular-development
Comprehensive Angular framework development covering components, directives, services, dependency injection, routing, and reactive programming based on official Angular documentation
Install this agent skill to your Project
npx add-skill https://github.com/manutej/luxor-claude-marketplace/tree/main/plugins/luxor-frontend-essentials/skills/angular-development
SKILL.md
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.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
svelte-development
Comprehensive Svelte development skill covering reactivity runes, components, stores, lifecycle, transitions, and modern Svelte 5 patterns
ui-design-patterns
Common interface patterns, navigation patterns, form patterns, data display patterns, feedback patterns, and accessibility considerations
react-patterns
Modern React development with hooks, component patterns, state management, and performance optimization for building scalable applications
mobile-design
Mobile UX patterns, touch interactions, gesture design, mobile-first principles, app navigation, and mobile performance
vuejs-development
Comprehensive Vue.js development skill covering Composition API, reactivity system, components, directives, and modern Vue 3 patterns based on official Vue.js documentation
tailwind-css
Utility-first CSS framework for rapid UI development with responsive design, component patterns, and production optimization. Master core utilities, dark mode, customization, and modern component composition for building beautiful, performant user interfaces.
Didn't find tool you were looking for?