Angular Guide - RumenDamyanov/js-chess GitHub Wiki

Angular Guide

Learn how to build chess applications using Angular with the JS Chess project, leveraging the enterprise-grade framework's powerful features including TypeScript, dependency injection, and reactive programming.

Overview

The Angular implementation demonstrates how to use the enterprise-grade framework to build scalable chess applications. It showcases Angular's component architecture, services, dependency injection, reactive forms, and TypeScript integration.

Project Structure

angular/
├── package.json              # Dependencies and scripts
├── angular.json              # Angular workspace configuration
├── tsconfig.json             # TypeScript configuration
├── src/
│   ├── main.ts               # Application bootstrap
│   ├── index.html            # HTML template
│   ├── styles.css            # Global styles
│   ├── app/
│   │   ├── app.component.ts  # Root component
│   │   ├── app.component.html # Root template
│   │   ├── app.module.ts     # Root module
│   │   ├── app-routing.module.ts # Routing configuration
│   │   ├── components/       # Feature components
│   │   │   ├── chess-board/  # Chess board component
│   │   │   ├── game-controls/ # Game controls
│   │   │   ├── game-status/  # Status display
│   │   │   ├── move-history/ # Move history
│   │   │   └── settings-modal/ # Settings dialog
│   │   ├── services/         # Angular services
│   │   │   ├── chess-api.service.ts # API service
│   │   │   ├── game.service.ts # Game logic service
│   │   │   ├── websocket.service.ts # WebSocket service
│   │   │   └── settings.service.ts # Settings service
│   │   ├── models/           # TypeScript interfaces
│   │   │   ├── game.interface.ts # Game models
│   │   │   ├── move.interface.ts # Move models
│   │   │   └── api.interface.ts # API models
│   │   ├── guards/           # Route guards
│   │   │   └── game.guard.ts # Game route guard
│   │   ├── pipes/            # Custom pipes
│   │   │   └── piece-symbol.pipe.ts # Piece display pipe
│   │   └── shared/           # Shared modules
│   │       ├── shared.module.ts # Shared module
│   │       └── components/   # Shared components
│   ├── assets/               # Static assets
│   └── environments/         # Environment configuration
│       ├── environment.ts    # Development environment
│       └── environment.prod.ts # Production environment
└── public/                   # Public assets
    └── pieces/               # Chess piece images

Getting Started

1. Setup Development Environment

# Clone the repository
git clone --recursive https://github.com/RumenDamyanov/js-chess.git
cd js-chess

# Start the Angular application
make run-angular

# Or manually
cd angular
npm install
ng serve

2. Open in Browser

Visit http://localhost:3005 to see the Angular chess application in action.

Core Implementation

Application Bootstrap

The main.ts file bootstraps the Angular application:

// main.ts - Angular application bootstrap
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'
import { provideRouter } from '@angular/router'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { importProvidersFrom } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'

import { routes } from './app/app-routing'
import { apiInterceptor } from './app/interceptors/api.interceptor'
import { errorInterceptor } from './app/interceptors/error.interceptor'

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([apiInterceptor, errorInterceptor])
    ),
    importProvidersFrom(BrowserAnimationsModule)
  ]
}).catch(err => console.error(err))

Root Component

The app.component.ts serves as the application shell:

// app.component.ts - Root application component
import { Component, OnInit, OnDestroy } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Subject, takeUntil } from 'rxjs'

import { GameService } from './services/game.service'
import { SettingsService } from './services/settings.service'
import { WebSocketService } from './services/websocket.service'

import { ChessBoardComponent } from './components/chess-board/chess-board.component'
import { GameControlsComponent } from './components/game-controls/game-controls.component'
import { GameStatusComponent } from './components/game-status/game-status.component'
import { MoveHistoryComponent } from './components/move-history/move-history.component'
import { SettingsModalComponent } from './components/settings-modal/settings-modal.component'
import { LoadingSpinnerComponent } from './shared/components/loading-spinner/loading-spinner.component'

import { GameState, Move, ChessSettings } from './models'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CommonModule,
    ChessBoardComponent,
    GameControlsComponent,
    GameStatusComponent,
    MoveHistoryComponent,
    SettingsModalComponent,
    LoadingSpinnerComponent
  ],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>()

  gameState$ = this.gameService.gameState$
  selectedSquare$ = this.gameService.selectedSquare$
  validMoves$ = this.gameService.validMoves$
  moveHistory$ = this.gameService.moveHistory$
  isPlayerTurn$ = this.gameService.isPlayerTurn$
  isLoading$ = this.gameService.isLoading$
  error$ = this.gameService.error$

  settings$ = this.settingsService.settings$
  showSettings = false

  constructor(
    private gameService: GameService,
    private settingsService: SettingsService,
    private webSocketService: WebSocketService
  ) {}

  ngOnInit(): void {
    this.initializeGame()
    this.setupWebSocket()
    this.loadSettings()
  }

  ngOnDestroy(): void {
    this.destroy$.next()
    this.destroy$.complete()
    this.webSocketService.disconnect()
  }

  private async initializeGame(): Promise<void> {
    try {
      await this.gameService.createNewGame()
    } catch (error) {
      console.error('Failed to initialize game:', error)
    }
  }

  private setupWebSocket(): void {
    this.gameState$
      .pipe(takeUntil(this.destroy$))
      .subscribe(gameState => {
        if (gameState?.id) {
          this.webSocketService.connect(gameState.id)
        }
      })

    this.webSocketService.gameUpdates$
      .pipe(takeUntil(this.destroy$))
      .subscribe(update => {
        this.gameService.handleWebSocketUpdate(update)
      })
  }

  private loadSettings(): void {
    this.settingsService.loadSettings()
  }

  onSquareClick(position: string): void {
    this.gameService.handleSquareClick(position)
  }

  onPieceDrag(event: { from: string; to: string }): void {
    this.gameService.handlePieceDrag(event.from, event.to)
  }

  onNewGame(): void {
    this.gameService.createNewGame()
  }

  onUndoMove(): void {
    this.gameService.undoLastMove()
  }

  onGetHint(): void {
    this.gameService.getHint()
  }

  onShowSettings(): void {
    this.showSettings = true
  }

  onSettingsSave(settings: ChessSettings): void {
    this.settingsService.updateSettings(settings)
    this.showSettings = false
  }

  onSettingsClose(): void {
    this.showSettings = false
  }

  onErrorClose(): void {
    this.gameService.clearError()
  }
}

Game Service

The game.service.ts manages game state and logic:

// services/game.service.ts
import { Injectable } from '@angular/core'
import { BehaviorSubject, Observable, throwError } from 'rxjs'
import { catchError, finalize } from 'rxjs/operators'

import { ChessApiService } from './chess-api.service'
import { GameState, Move, CreateGameRequest, MakeMoveRequest } from '../models'

@Injectable({
  providedIn: 'root'
})
export class GameService {
  private gameStateSubject = new BehaviorSubject<GameState | null>(null)
  private selectedSquareSubject = new BehaviorSubject<string | null>(null)
  private validMovesSubject = new BehaviorSubject<string[]>([])
  private moveHistorySubject = new BehaviorSubject<Move[]>([])
  private isPlayerTurnSubject = new BehaviorSubject<boolean>(true)
  private isLoadingSubject = new BehaviorSubject<boolean>(false)
  private errorSubject = new BehaviorSubject<string | null>(null)

  gameState$ = this.gameStateSubject.asObservable()
  selectedSquare$ = this.selectedSquareSubject.asObservable()
  validMoves$ = this.validMovesSubject.asObservable()
  moveHistory$ = this.moveHistorySubject.asObservable()
  isPlayerTurn$ = this.isPlayerTurnSubject.asObservable()
  isLoading$ = this.isLoadingSubject.asObservable()
  error$ = this.errorSubject.asObservable()

  constructor(private chessApi: ChessApiService) {}

  async createNewGame(): Promise<void> {
    this.setLoading(true)
    this.clearError()

    try {
      const request: CreateGameRequest = {
        ai_enabled: true,
        difficulty: 'medium'
      }

      const gameState = await this.chessApi.createGame(request).toPromise()

      this.gameStateSubject.next(gameState)
      this.selectedSquareSubject.next(null)
      this.validMovesSubject.next([])
      this.moveHistorySubject.next([])
      this.isPlayerTurnSubject.next(true)
    } catch (error) {
      this.handleError('Failed to create new game', error)
      throw error
    } finally {
      this.setLoading(false)
    }
  }

  async makeMove(from: string, to: string, promotion?: string): Promise<void> {
    const gameState = this.gameStateSubject.value
    if (!gameState?.id || !this.isPlayerTurnSubject.value) {
      return
    }

    this.isPlayerTurnSubject.next(false)
    this.clearSelection()

    try {
      const request: MakeMoveRequest = { from, to, promotion }
      const response = await this.chessApi.makeMove(gameState.id, request).toPromise()

      if (response.success) {
        this.gameStateSubject.next(response.gameState)
        this.addMoveToHistory(response.move)

        if (!response.gameState.gameOver) {
          // Get AI move
          await this.makeAIMove()
        }
      } else {
        throw new Error(response.error || 'Invalid move')
      }
    } catch (error) {
      this.handleError('Failed to make move', error)
      this.isPlayerTurnSubject.next(true)
    }
  }

  private async makeAIMove(): Promise<void> {
    const gameState = this.gameStateSubject.value
    if (!gameState?.id) return

    try {
      const response = await this.chessApi.getAIMove(gameState.id).toPromise()

      if (response.success) {
        this.gameStateSubject.next(response.gameState)
        this.addMoveToHistory(response.move)
        this.isPlayerTurnSubject.next(true)
      }
    } catch (error) {
      this.handleError('AI move failed', error)
      this.isPlayerTurnSubject.next(true)
    }
  }

  async undoLastMove(): Promise<void> {
    const gameState = this.gameStateSubject.value
    const moveHistory = this.moveHistorySubject.value

    if (!gameState?.id || moveHistory.length === 0) {
      return
    }

    this.setLoading(true)

    try {
      const response = await this.chessApi.undoMove(gameState.id).toPromise()

      if (response.success) {
        this.gameStateSubject.next(response.gameState)
        // Remove last two moves (player + AI)
        const newHistory = moveHistory.slice(0, -2)
        this.moveHistorySubject.next(newHistory)
        this.isPlayerTurnSubject.next(true)
        this.clearSelection()
      }
    } catch (error) {
      this.handleError('Failed to undo move', error)
    } finally {
      this.setLoading(false)
    }
  }

  async getHint(): Promise<void> {
    const gameState = this.gameStateSubject.value
    if (!gameState?.id) return

    try {
      const hint = await this.chessApi.getHint(gameState.id).toPromise()
      // Handle hint display
      console.log('Hint:', hint)
    } catch (error) {
      this.handleError('Failed to get hint', error)
    }
  }

  handleSquareClick(position: string): void {
    if (!this.isPlayerTurnSubject.value) return

    const selectedSquare = this.selectedSquareSubject.value
    const validMoves = this.validMovesSubject.value

    if (selectedSquare === position) {
      this.clearSelection()
      return
    }

    if (selectedSquare && validMoves.includes(position)) {
      this.makeMove(selectedSquare, position)
      return
    }

    this.selectSquare(position)
  }

  handlePieceDrag(from: string, to: string): void {
    if (!this.isPlayerTurnSubject.value) return

    const validMoves = this.validMovesSubject.value
    if (validMoves.includes(to)) {
      this.makeMove(from, to)
    }
  }

  private selectSquare(position: string): void {
    this.selectedSquareSubject.next(position)

    // Get valid moves for this position
    const validMoves = this.getValidMovesForPosition(position)
    this.validMovesSubject.next(validMoves)
  }

  private clearSelection(): void {
    this.selectedSquareSubject.next(null)
    this.validMovesSubject.next([])
  }

  private getValidMovesForPosition(position: string): string[] {
    // This should come from the API in a real implementation
    // Placeholder implementation
    return []
  }

  private addMoveToHistory(move: Move): void {
    const currentHistory = this.moveHistorySubject.value
    this.moveHistorySubject.next([...currentHistory, move])
  }

  handleWebSocketUpdate(update: any): void {
    // Handle WebSocket game updates
    switch (update.type) {
      case 'game_update':
        this.gameStateSubject.next(update.gameState)
        break
      case 'move_made':
        this.addMoveToHistory(update.move)
        break
      case 'game_over':
        const currentState = this.gameStateSubject.value
        if (currentState) {
          this.gameStateSubject.next({
            ...currentState,
            gameOver: true,
            winner: update.winner
          })
        }
        break
    }
  }

  clearError(): void {
    this.errorSubject.next(null)
  }

  private setLoading(loading: boolean): void {
    this.isLoadingSubject.next(loading)
  }

  private handleError(message: string, error: any): void {
    console.error(message, error)
    this.errorSubject.next(message)
  }
}

Chess API Service

The chess-api.service.ts handles HTTP communication:

// services/chess-api.service.ts
import { Injectable } from '@angular/core'
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
import { Observable, throwError } from 'rxjs'
import { catchError, retry } from 'rxjs/operators'
import { environment } from '../../environments/environment'

import {
  GameState,
  CreateGameRequest,
  CreateGameResponse,
  MakeMoveRequest,
  MakeMoveResponse,
  AIMove Response,
  UndoMoveResponse,
  HintResponse,
  AnalysisResponse
} from '../models'

@Injectable({
  providedIn: 'root'
})
export class ChessApiService {
  private readonly baseUrl = environment.apiUrl

  constructor(private http: HttpClient) {}

  createGame(request: CreateGameRequest): Observable<GameState> {
    return this.http.post<GameState>(`${this.baseUrl}/api/games`, request)
      .pipe(
        retry(1),
        catchError(this.handleError)
      )
  }

  getGame(gameId: number): Observable<GameState> {
    return this.http.get<GameState>(`${this.baseUrl}/api/games/${gameId}`)
      .pipe(
        catchError(this.handleError)
      )
  }

  makeMove(gameId: number, request: MakeMoveRequest): Observable<MakeMoveResponse> {
    return this.http.post<MakeMoveResponse>(`${this.baseUrl}/api/games/${gameId}/moves`, request)
      .pipe(
        catchError(this.handleError)
      )
  }

  getAIMove(gameId: number): Observable<AIMoveResponse> {
    return this.http.post<AIMoveResponse>(`${this.baseUrl}/api/games/${gameId}/ai-move`, {})
      .pipe(
        catchError(this.handleError)
      )
  }

  undoMove(gameId: number): Observable<UndoMoveResponse> {
    return this.http.post<UndoMoveResponse>(`${this.baseUrl}/api/games/${gameId}/undo`, {})
      .pipe(
        catchError(this.handleError)
      )
  }

  getHint(gameId: number): Observable<HintResponse> {
    return this.http.get<HintResponse>(`${this.baseUrl}/api/games/${gameId}/hint`)
      .pipe(
        catchError(this.handleError)
      )
  }

  getAnalysis(gameId: number): Observable<AnalysisResponse> {
    return this.http.get<AnalysisResponse>(`${this.baseUrl}/api/games/${gameId}/analysis`)
      .pipe(
        catchError(this.handleError)
      )
  }

  private handleError(error: HttpErrorResponse): Observable<never> {
    let errorMessage = 'An unknown error occurred'

    if (error.error instanceof ErrorEvent) {
      // Client-side error
      errorMessage = `Error: ${error.error.message}`
    } else {
      // Server-side error
      errorMessage = error.error?.message || `Error Code: ${error.status}\nMessage: ${error.message}`
    }

    console.error('Chess API Error:', errorMessage)
    return throwError(() => new Error(errorMessage))
  }
}

Chess Board Component

The chess-board.component.ts handles board rendering:

// components/chess-board/chess-board.component.ts
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'
import { CommonModule } from '@angular/common'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'

import { GameState } from '../../models'
import { PieceSymbolPipe } from '../../pipes/piece-symbol.pipe'

interface ChessSquare {
  position: string
  file: string
  rank: number
  isLight: boolean
  piece: string | null
  isSelected: boolean
  isValidMove: boolean
  isLastMove: boolean
  showFileLabel: boolean
  showRankLabel: boolean
}

@Component({
  selector: 'app-chess-board',
  standalone: true,
  imports: [CommonModule, DragDropModule, PieceSymbolPipe],
  templateUrl: './chess-board.component.html',
  styleUrls: ['./chess-board.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChessBoardComponent {
  @Input() gameState: GameState | null = null
  @Input() selectedSquare: string | null = null
  @Input() validMoves: string[] = []
  @Input() theme = 'classic'
  @Input() showCoordinates = true

  @Output() squareClick = new EventEmitter<string>()
  @Output() pieceDrag = new EventEmitter<{ from: string; to: string }>()

  get squares(): ChessSquare[] {
    const squares: ChessSquare[] = []

    for (let rank = 8; rank >= 1; rank--) {
      for (let file = 0; file < 8; file++) {
        const fileChar = String.fromCharCode(97 + file) // a-h
        const position = fileChar + rank

        squares.push({
          position,
          file: fileChar,
          rank,
          isLight: (rank + file) % 2 === 1,
          piece: this.getPieceAt(position),
          isSelected: this.selectedSquare === position,
          isValidMove: this.validMoves.includes(position),
          isLastMove: this.isLastMoveSquare(position),
          showFileLabel: rank === 1 && this.showCoordinates,
          showRankLabel: file === 0 && this.showCoordinates
        })
      }
    }

    return squares
  }

  onSquareClick(position: string): void {
    this.squareClick.emit(position)
  }

  onPieceDrop(event: CdkDragDrop<string[]>): void {
    const fromPosition = event.previousContainer.id
    const toPosition = event.container.id

    if (fromPosition !== toPosition) {
      this.pieceDrag.emit({ from: fromPosition, to: toPosition })
    }
  }

  getSquareClasses(square: ChessSquare): string[] {
    return [
      'square',
      square.isLight ? 'light' : 'dark',
      square.isSelected ? 'selected' : '',
      square.isValidMove ? 'valid-move' : '',
      square.isLastMove ? 'last-move' : ''
    ].filter(Boolean)
  }

  canDragPiece(piece: string | null): boolean {
    if (!piece) return false
    // Only allow dragging white pieces (player pieces)
    return piece === piece.toUpperCase()
  }

  private getPieceAt(position: string): string | null {
    if (!this.gameState?.board) return null

    // Parse board string and find piece at position
    return this.parseBoardString(this.gameState.board, position)
  }

  private parseBoardString(board: string, position: string): string | null {
    // Implementation depends on board format from API
    // This is a placeholder
    return null
  }

  private isLastMoveSquare(position: string): boolean {
    if (!this.gameState?.lastMove) return false

    return this.gameState.lastMove.from === position ||
           this.gameState.lastMove.to === position
  }

  trackByPosition(index: number, square: ChessSquare): string {
    return square.position
  }
}

TypeScript Models

Define strong typing with interfaces:

// models/game.interface.ts
export interface GameState {
  id: number
  board: string
  status: 'ready' | 'in_progress' | 'finished'
  activeColor: 'white' | 'black'
  inCheck: boolean
  gameOver: boolean
  winner: 'white' | 'black' | null
  lastMove: Move | null
  createdAt: string
  updatedAt: string
}

export interface Move {
  from: string
  to: string
  piece: string
  captured?: string
  promotion?: string
  san: string
  timestamp: string
}

export interface CreateGameRequest {
  ai_enabled: boolean
  difficulty: 'easy' | 'medium' | 'hard'
}

export interface MakeMoveRequest {
  from: string
  to: string
  promotion?: string
}

export interface MakeMoveResponse {
  success: boolean
  move: Move
  gameState: GameState
  error?: string
}

export interface AIMoveResponse {
  success: boolean
  move: Move
  gameState: GameState
  analysis?: {
    evaluation: number
    depth: number
    bestLine: string[]
  }
}

export interface HintResponse {
  success: boolean
  from: string
  to: string
  explanation: string
}

export interface ChessSettings {
  boardTheme: 'classic' | 'modern' | 'wooden'
  showCoordinates: boolean
  enableSounds: boolean
  aiDifficulty: 'easy' | 'medium' | 'hard'
  autoPromoteQueen: boolean
  animationSpeed: 'slow' | 'normal' | 'fast'
}

Custom Pipe

Create a pipe for piece symbols:

// pipes/piece-symbol.pipe.ts
import { Pipe, PipeTransform } from '@angular/core'

@Pipe({
  name: 'pieceSymbol',
  standalone: true
})
export class PieceSymbolPipe implements PipeTransform {
  private readonly pieceSymbols = {
    white: {
      king: '♔', queen: '♕', rook: '♖',
      bishop: '♗', knight: '♘', pawn: '♙'
    },
    black: {
      king: '♚', queen: '♛', rook: '♜',
      bishop: '♝', knight: '♞', pawn: '♟'
    }
  }

  transform(piece: string | null): string {
    if (!piece) return ''

    const isWhite = piece === piece.toUpperCase()
    const color = isWhite ? 'white' : 'black'
    const pieceType = this.getPieceType(piece.toLowerCase())

    return this.pieceSymbols[color][pieceType] || ''
  }

  private getPieceType(piece: string): keyof typeof this.pieceSymbols.white {
    const types = {
      'k': 'king', 'q': 'queen', 'r': 'rook',
      'b': 'bishop', 'n': 'knight', 'p': 'pawn'
    } as const

    return types[piece as keyof typeof types] || 'pawn'
  }
}

WebSocket Service

Handle real-time communication:

// services/websocket.service.ts
import { Injectable } from '@angular/core'
import { Subject, Observable } from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class WebSocketService {
  private socket: WebSocket | null = null
  private gameUpdatesSubject = new Subject<any>()
  private reconnectAttempts = 0
  private maxReconnectAttempts = 5

  gameUpdates$ = this.gameUpdatesSubject.asObservable()

  connect(gameId: number): void {
    if (this.socket?.readyState === WebSocket.OPEN) {
      return
    }

    try {
      const wsUrl = `ws://localhost:8080/ws/games/${gameId}`
      this.socket = new WebSocket(wsUrl)

      this.socket.onopen = () => {
        console.log('WebSocket connected')
        this.reconnectAttempts = 0
      }

      this.socket.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data)
          this.gameUpdatesSubject.next(data)
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error)
        }
      }

      this.socket.onclose = () => {
        console.log('WebSocket disconnected')
        this.attemptReconnect(gameId)
      }

      this.socket.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
    } catch (error) {
      console.error('Failed to create WebSocket connection:', error)
    }
  }

  disconnect(): void {
    if (this.socket) {
      this.socket.close()
      this.socket = null
    }
  }

  private attemptReconnect(gameId: number): void {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      setTimeout(() => {
        this.reconnectAttempts++
        this.connect(gameId)
      }, 1000 * Math.pow(2, this.reconnectAttempts))
    }
  }
}

Advanced Features

Route Guards

Protect routes with guards:

// guards/game.guard.ts
import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router'
import { Observable, map } from 'rxjs'
import { GameService } from '../services/game.service'

@Injectable({
  providedIn: 'root'
})
export class GameGuard implements CanActivate {
  constructor(
    private gameService: GameService,
    private router: Router
  ) {}

  canActivate(): Observable<boolean> {
    return this.gameService.gameState$.pipe(
      map(gameState => {
        if (gameState?.id) {
          return true
        } else {
          this.router.navigate(['/'])
          return false
        }
      })
    )
  }
}

HTTP Interceptors

Add global HTTP error handling:

// interceptors/error.interceptor.ts
import { Injectable } from '@angular/core'
import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http'
import { catchError, throwError } from 'rxjs'
import { MatSnackBar } from '@angular/material/snack-bar'

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private snackBar: MatSnackBar) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        let errorMessage = 'An unknown error occurred'

        if (error.error instanceof ErrorEvent) {
          errorMessage = `Error: ${error.error.message}`
        } else {
          errorMessage = error.error?.message || `Error Code: ${error.status}`
        }

        this.snackBar.open(errorMessage, 'Close', {
          duration: 5000,
          horizontalPosition: 'right',
          verticalPosition: 'top'
        })

        return throwError(() => error)
      })
    )
  }
}

Testing

Component Testing

// chess-board.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { DragDropModule } from '@angular/cdk/drag-drop'

import { ChessBoardComponent } from './chess-board.component'
import { PieceSymbolPipe } from '../../pipes/piece-symbol.pipe'

describe('ChessBoardComponent', () => {
  let component: ChessBoardComponent
  let fixture: ComponentFixture<ChessBoardComponent>

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ChessBoardComponent, DragDropModule, PieceSymbolPipe]
    }).compileComponents()

    fixture = TestBed.createComponent(ChessBoardComponent)
    component = fixture.componentInstance
  })

  it('should create', () => {
    expect(component).toBeTruthy()
  })

  it('should render 64 squares', () => {
    fixture.detectChanges()
    const squares = fixture.debugElement.queryAll(By.css('.square'))
    expect(squares).toHaveLength(64)
  })

  it('should emit squareClick when square is clicked', () => {
    spyOn(component.squareClick, 'emit')

    const square = fixture.debugElement.query(By.css('[data-position="e4"]'))
    square.nativeElement.click()

    expect(component.squareClick.emit).toHaveBeenCalledWith('e4')
  })

  it('should highlight selected square', () => {
    component.selectedSquare = 'e4'
    fixture.detectChanges()

    const square = fixture.debugElement.query(By.css('[data-position="e4"]'))
    expect(square.nativeElement).toHaveClass('selected')
  })
})

Service Testing

// game.service.spec.ts
import { TestBed } from '@angular/core/testing'
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'

import { GameService } from './game.service'
import { ChessApiService } from './chess-api.service'

describe('GameService', () => {
  let service: GameService
  let httpMock: HttpTestingController
  let apiService: ChessApiService

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [GameService, ChessApiService]
    })

    service = TestBed.inject(GameService)
    httpMock = TestBed.inject(HttpTestingController)
    apiService = TestBed.inject(ChessApiService)
  })

  afterEach(() => {
    httpMock.verify()
  })

  it('should be created', () => {
    expect(service).toBeTruthy()
  })

  it('should create new game', async () => {
    const mockGame = {
      id: 1,
      board: 'starting-position',
      status: 'in_progress'
    }

    spyOn(apiService, 'createGame').and.returnValue(of(mockGame))

    await service.createNewGame()

    service.gameState$.subscribe(state => {
      expect(state).toEqual(mockGame)
    })
  })
})

Build and Deployment

Production Build

# Build for production
ng build --configuration production

# Test production build
ng serve --configuration production

# Analyze bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/stats.json

Angular Configuration

// angular.json (excerpt)
{
  "projects": {
    "chess-app": {
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                }
              ],
              "outputHashing": "all",
              "sourceMap": false,
              "optimization": true,
              "buildOptimizer": true
            }
          }
        }
      }
    }
  }
}

Performance Optimization

OnPush Change Detection

@Component({
  selector: 'app-chess-board',
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})
export class ChessBoardComponent {
  // Component only updates when inputs change
}

Lazy Loading

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'game',
    loadChildren: () => import('./game/game.module').then(m => m.GameModule),
    canActivate: [GameGuard]
  },
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component').then(c => c.SettingsComponent)
  }
]

TrackBy Functions

trackByMove(index: number, move: Move): string {
  return move.timestamp
}

trackByPosition(index: number, square: ChessSquare): string {
  return square.position
}

Best Practices

  1. Use TypeScript strictly with proper interfaces
  2. Implement OnPush change detection for performance
  3. Use standalone components for better tree-shaking
  4. Handle errors gracefully with interceptors
  5. Test components and services thoroughly
  6. Use reactive patterns with RxJS
  7. Implement proper accessibility with CDK

Next Steps

  1. Review the API Integration Guide for backend communication
  2. Check the Chess Features Guide for advanced chess logic
  3. See the UI/UX Guide for design improvements
  4. Compare with other framework implementations

The Angular implementation demonstrates how enterprise-grade frameworks can create robust, scalable chess applications with excellent type safety, testing capabilities, and maintainability.

⚠️ **GitHub.com Fallback** ⚠️