Claude Code Plugins

Community-maintained marketplace

Feedback
17
0

Comprehensive Angular framework development covering components, directives, services, dependency injection, routing, and reactive programming based on official Angular documentation

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name angular-development
description Comprehensive Angular framework development covering components, directives, services, dependency injection, routing, and reactive programming based on official Angular documentation
category frontend
tags angular, typescript, components, services, dependency-injection, rxjs, reactive, standalone, signals
version 1.0.0
context7_library /angular/angular
context7_trust_score 8.9

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>&copy; 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.