Angular Guide - RumenDamyanov/js-chess GitHub Wiki
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.
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.
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
# 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
Visit http://localhost:3005
to see the Angular chess application in action.
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))
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()
}
}
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)
}
}
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))
}
}
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
}
}
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'
}
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'
}
}
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))
}
}
}
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
}
})
)
}
}
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)
})
)
}
}
// 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')
})
})
// 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 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.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
}
}
}
}
}
}
}
@Component({
selector: 'app-chess-board',
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
export class ChessBoardComponent {
// Component only updates when inputs change
}
// 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)
}
]
trackByMove(index: number, move: Move): string {
return move.timestamp
}
trackByPosition(index: number, square: ChessSquare): string {
return square.position
}
- Use TypeScript strictly with proper interfaces
- Implement OnPush change detection for performance
- Use standalone components for better tree-shaking
- Handle errors gracefully with interceptors
- Test components and services thoroughly
- Use reactive patterns with RxJS
- Implement proper accessibility with CDK
- Review the API Integration Guide for backend communication
- Check the Chess Features Guide for advanced chess logic
- See the UI/UX Guide for design improvements
- 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.