No articles found
Try different keywords or browse our categories
Building a Camera App in Angular: Complete Photo and Video Recorder
Step-by-step guide to building a professional camera application in Angular with photo capture, video recording, filters, and download features.
Build a professional camera application in Angular with webcam access, photo capture, video recording, and file downloads. Full TypeScript implementation with services and components.
Setup Angular Project
Create new Angular project with routing and styles.
ng new camera-app
cd camera-app
ng generate component camera
ng generate service camera
Camera Service
Create reusable camera service for stream management.
camera.service.ts:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface CameraError {
type: string;
message: string;
}
@Injectable({
providedIn: 'root'
})
export class CameraService {
private streamSubject = new BehaviorSubject<MediaStream | null>(null);
private errorSubject = new BehaviorSubject<CameraError | null>(null);
public stream$ = this.streamSubject.asObservable();
public error$ = this.errorSubject.asObservable();
private currentStream: MediaStream | null = null;
async startCamera(constraints?: MediaStreamConstraints): Promise<void> {
try {
const defaultConstraints: MediaStreamConstraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: false
};
const stream = await navigator.mediaDevices.getUserMedia(
constraints || defaultConstraints
);
this.currentStream = stream;
this.streamSubject.next(stream);
this.errorSubject.next(null);
} catch (error: any) {
this.handleError(error);
}
}
stopCamera(): void {
if (this.currentStream) {
this.currentStream.getTracks().forEach(track => track.stop());
this.currentStream = null;
this.streamSubject.next(null);
}
}
async getAvailableDevices(): Promise<MediaDeviceInfo[]> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === 'videoinput');
}
private handleError(error: any): void {
let errorMessage = 'Failed to access camera';
let errorType = 'UnknownError';
if (error.name === 'NotAllowedError') {
errorMessage = 'Camera access denied by user';
errorType = 'PermissionDenied';
} else if (error.name === 'NotFoundError') {
errorMessage = 'No camera device found';
errorType = 'DeviceNotFound';
} else if (error.name === 'NotReadableError') {
errorMessage = 'Camera already in use';
errorType = 'DeviceBusy';
}
this.errorSubject.next({ type: errorType, message: errorMessage });
}
}
Photo Capture Component
Component for taking photos with download.
photo-capture.component.ts:
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';
import { Subject, takeUntil } from 'rxjs';
interface CapturedPhoto {
id: string;
dataUrl: string;
timestamp: Date;
}
@Component({
selector: 'app-photo-capture',
templateUrl: './photo-capture.component.html',
styleUrls: ['./photo-capture.component.css']
})
export class PhotoCaptureComponent implements OnInit, OnDestroy {
@ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;
@ViewChild('canvasElement') canvasElement!: ElementRef<HTMLCanvasElement>;
photos: CapturedPhoto[] = [];
errorMessage: string | null = null;
isLoading = true;
private destroy$ = new Subject<void>();
constructor(private cameraService: CameraService) {}
ngOnInit(): void {
this.initializeCamera();
this.subscribeToErrors();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.cameraService.stopCamera();
}
private initializeCamera(): void {
this.cameraService.startCamera();
this.cameraService.stream$
.pipe(takeUntil(this.destroy$))
.subscribe(stream => {
if (stream && this.videoElement) {
this.videoElement.nativeElement.srcObject = stream;
this.isLoading = false;
}
});
}
private subscribeToErrors(): void {
this.cameraService.error$
.pipe(takeUntil(this.destroy$))
.subscribe(error => {
if (error) {
this.errorMessage = error.message;
this.isLoading = false;
}
});
}
capturePhoto(): void {
const video = this.videoElement.nativeElement;
const canvas = this.canvasElement.nativeElement;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
if (context) {
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/jpeg', 0.95);
this.photos.push({
id: this.generateId(),
dataUrl,
timestamp: new Date()
});
}
}
downloadPhoto(photo: CapturedPhoto): void {
const link = document.createElement('a');
link.href = photo.dataUrl;
link.download = `photo-${photo.id}.jpg`;
link.click();
}
deletePhoto(photoId: string): void {
this.photos = this.photos.filter(p => p.id !== photoId);
}
clearAll(): void {
this.photos = [];
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}
photo-capture.component.html:
<div class="photo-capture-container">
<div class="camera-section">
<div *ngIf="isLoading" class="loading">
<p>Loading camera...</p>
</div>
<div *ngIf="errorMessage" class="error-message">
<h3>Camera Error</h3>
<p>{{ errorMessage }}</p>
</div>
<div class="video-container" *ngIf="!isLoading && !errorMessage">
<video #videoElement autoplay playsinline></video>
</div>
<canvas #canvasElement style="display: none;"></canvas>
<div class="controls">
<button
class="btn-primary"
(click)="capturePhoto()"
[disabled]="isLoading || errorMessage !== null">
📷 Capture Photo
</button>
<button
class="btn-secondary"
(click)="clearAll()"
[disabled]="photos.length === 0">
Clear All ({{ photos.length }})
</button>
</div>
</div>
<div class="gallery" *ngIf="photos.length > 0">
<h2>Captured Photos</h2>
<div class="photo-grid">
<div *ngFor="let photo of photos" class="photo-card">
<img [src]="photo.dataUrl" [alt]="'Photo ' + photo.id">
<div class="photo-info">
<span class="timestamp">{{ photo.timestamp | date:'short' }}</span>
<div class="photo-actions">
<button (click)="downloadPhoto(photo)" class="btn-download">
⬇ Download
</button>
<button (click)="deletePhoto(photo.id)" class="btn-delete">
🗑 Delete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
photo-capture.component.css:
.photo-capture-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.camera-section {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.video-container {
position: relative;
border-radius: 8px;
overflow: hidden;
background: #000;
margin-bottom: 20px;
}
.video-container video {
width: 100%;
height: auto;
display: block;
}
.controls {
display: flex;
gap: 10px;
}
.btn-primary {
flex: 1;
padding: 15px 30px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-secondary {
padding: 15px 30px;
background: #6c757d;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.btn-secondary:hover:not(:disabled) {
background: #545b62;
}
.loading, .error-message {
text-align: center;
padding: 40px;
}
.error-message {
background: #fee;
border: 2px solid #fcc;
border-radius: 8px;
color: #c33;
}
.gallery h2 {
margin-bottom: 20px;
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.photo-card {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.photo-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.photo-info {
padding: 15px;
}
.timestamp {
display: block;
color: #666;
font-size: 14px;
margin-bottom: 10px;
}
.photo-actions {
display: flex;
gap: 10px;
}
.photo-actions button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-download {
background: #28a745;
color: white;
}
.btn-delete {
background: #dc3545;
color: white;
}
Video Recording Component
Record videos with timer and playback.
video-recorder.component.ts:
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';
import { Subject, takeUntil, interval } from 'rxjs';
interface RecordedVideo {
id: string;
url: string;
duration: number;
timestamp: Date;
}
@Component({
selector: 'app-video-recorder',
templateUrl: './video-recorder.component.html',
styleUrls: ['./video-recorder.component.css']
})
export class VideoRecorderComponent implements OnInit, OnDestroy {
@ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;
isRecording = false;
recordingDuration = 0;
videos: RecordedVideo[] = [];
errorMessage: string | null = null;
private mediaRecorder: MediaRecorder | null = null;
private recordedChunks: Blob[] = [];
private destroy$ = new Subject<void>();
private recordingStartTime = 0;
constructor(private cameraService: CameraService) {}
ngOnInit(): void {
this.initializeCamera();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.stopRecording();
this.cameraService.stopCamera();
}
private initializeCamera(): void {
this.cameraService.startCamera({
video: { width: 1280, height: 720 },
audio: true
});
this.cameraService.stream$
.pipe(takeUntil(this.destroy$))
.subscribe(stream => {
if (stream && this.videoElement) {
this.videoElement.nativeElement.srcObject = stream;
}
});
this.cameraService.error$
.pipe(takeUntil(this.destroy$))
.subscribe(error => {
if (error) {
this.errorMessage = error.message;
}
});
}
startRecording(): void {
const stream = this.videoElement.nativeElement.srcObject as MediaStream;
if (!stream) return;
this.recordedChunks = [];
this.recordingDuration = 0;
this.recordingStartTime = Date.now();
const options: MediaRecorderOptions = {
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 2500000
};
try {
this.mediaRecorder = new MediaRecorder(stream, options);
} catch (e) {
this.mediaRecorder = new MediaRecorder(stream);
}
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
this.mediaRecorder.onstop = () => {
this.saveRecording();
};
this.mediaRecorder.start();
this.isRecording = true;
interval(1000)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
if (this.isRecording) {
this.recordingDuration = Math.floor((Date.now() - this.recordingStartTime) / 1000);
}
});
}
stopRecording(): void {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
}
}
private saveRecording(): void {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
this.videos.push({
id: this.generateId(),
url,
duration: this.recordingDuration,
timestamp: new Date()
});
this.recordedChunks = [];
}
downloadVideo(video: RecordedVideo): void {
const link = document.createElement('a');
link.href = video.url;
link.download = `video-${video.id}.webm`;
link.click();
}
deleteVideo(videoId: string): void {
const video = this.videos.find(v => v.id === videoId);
if (video) {
URL.revokeObjectURL(video.url);
}
this.videos = this.videos.filter(v => v.id !== videoId);
}
formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}
video-recorder.component.html:
<div class="video-recorder-container">
<div class="camera-section">
<div *ngIf="errorMessage" class="error-message">
<h3>Error</h3>
<p>{{ errorMessage }}</p>
</div>
<div class="video-preview">
<video #videoElement autoplay playsinline muted></video>
<div *ngIf="isRecording" class="recording-indicator">
<span class="rec-badge">🔴 REC</span>
<span class="timer">{{ formatTime(recordingDuration) }}</span>
</div>
</div>
<div class="controls">
<button
*ngIf="!isRecording"
class="btn-record"
(click)="startRecording()"
[disabled]="errorMessage !== null">
⏺ Start Recording
</button>
<button
*ngIf="isRecording"
class="btn-stop"
(click)="stopRecording()">
⏹ Stop Recording ({{ formatTime(recordingDuration) }})
</button>
</div>
</div>
<div class="videos-list" *ngIf="videos.length > 0">
<h2>Recorded Videos ({{ videos.length }})</h2>
<div class="video-grid">
<div *ngFor="let video of videos" class="video-card">
<video [src]="video.url" controls></video>
<div class="video-info">
<div class="video-meta">
<span>Duration: {{ formatTime(video.duration) }}</span>
<span>{{ video.timestamp | date:'short' }}</span>
</div>
<div class="video-actions">
<button (click)="downloadVideo(video)" class="btn-download">
Download
</button>
<button (click)="deleteVideo(video.id)" class="btn-delete">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
</div>
Camera Switcher Component
Switch between multiple cameras.
camera-switcher.component.ts:
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';
@Component({
selector: 'app-camera-switcher',
template: `
<div class="camera-switcher">
<div class="device-selector">
<label>Select Camera:</label>
<select [(ngModel)]="selectedDeviceId" (change)="switchCamera()">
<option *ngFor="let device of devices" [value]="device.deviceId">
{{ device.label || 'Camera ' + device.deviceId.substring(0, 8) }}
</option>
</select>
</div>
<div class="video-container">
<video #videoElement autoplay playsinline></video>
</div>
<button (click)="capturePhoto()" class="btn-capture">
Take Photo
</button>
<div *ngIf="capturedImage" class="preview">
<h3>Last Photo</h3>
<img [src]="capturedImage">
<button (click)="downloadImage()">Download</button>
</div>
</div>
`,
styles: [`
.camera-switcher {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.device-selector {
margin-bottom: 20px;
}
.device-selector label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
.device-selector select {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.video-container {
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
background: #000;
}
.video-container video {
width: 100%;
display: block;
}
.btn-capture {
width: 100%;
padding: 15px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
}
.preview {
margin-top: 30px;
}
.preview img {
width: 100%;
border-radius: 8px;
}
`]
})
export class CameraSwitcherComponent implements OnInit {
@ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;
devices: MediaDeviceInfo[] = [];
selectedDeviceId: string = '';
capturedImage: string | null = null;
constructor(private cameraService: CameraService) {}
async ngOnInit() {
await this.loadDevices();
if (this.devices.length > 0) {
this.selectedDeviceId = this.devices[0].deviceId;
this.switchCamera();
}
}
async loadDevices() {
this.devices = await this.cameraService.getAvailableDevices();
}
switchCamera() {
this.cameraService.stopCamera();
this.cameraService.startCamera({
video: { deviceId: { exact: this.selectedDeviceId } },
audio: false
});
this.cameraService.stream$.subscribe(stream => {
if (stream && this.videoElement) {
this.videoElement.nativeElement.srcObject = stream;
}
});
}
capturePhoto() {
const video = this.videoElement.nativeElement;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx?.drawImage(video, 0, 0);
this.capturedImage = canvas.toDataURL('image/jpeg');
}
downloadImage() {
if (!this.capturedImage) return;
const link = document.createElement('a');
link.href = this.capturedImage;
link.download = `photo-${Date.now()}.jpg`;
link.click();
}
}
Complete Camera App Component
All-in-one camera application.
app.component.ts:
import { Component } from '@angular/core';
type CameraMode = 'photo' | 'video';
@Component({
selector: 'app-root',
template: `
<div class="camera-app">
<header>
<h1>📷 Angular Camera App</h1>
<div class="mode-tabs">
<button
[class.active]="mode === 'photo'"
(click)="mode = 'photo'">
Photo
</button>
<button
[class.active]="mode === 'video'"
(click)="mode = 'video'">
Video
</button>
</div>
</header>
<main>
<app-photo-capture *ngIf="mode === 'photo'"></app-photo-capture>
<app-video-recorder *ngIf="mode === 'video'"></app-video-recorder>
</main>
</div>
`,
styles: [`
.camera-app {
min-height: 100vh;
background: #f5f5f5;
}
header {
background: white;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
header h1 {
margin: 0 0 20px 0;
text-align: center;
}
.mode-tabs {
display: flex;
justify-content: center;
gap: 10px;
}
.mode-tabs button {
padding: 10px 30px;
border: 2px solid #333;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.2s;
}
.mode-tabs button.active {
background: #333;
color: white;
}
main {
padding: 0 20px;
}
`]
})
export class AppComponent {
mode: CameraMode = 'photo';
}
Add Photo Filters
Apply filters using canvas context.
import { Component } from '@angular/core';
@Component({
selector: 'app-photo-filters',
template: `
<div class="filters-app">
<div class="filter-selector">
<label>Select Filter:</label>
<select [(ngModel)]="selectedFilter">
<option value="none">None</option>
<option value="grayscale(100%)">Grayscale</option>
<option value="sepia(100%)">Sepia</option>
<option value="blur(3px)">Blur</option>
<option value="brightness(150%)">Bright</option>
<option value="contrast(200%)">Contrast</option>
<option value="invert(100%)">Invert</option>
</select>
</div>
<div class="video-container" [style.filter]="selectedFilter">
<video #videoElement autoplay playsinline></video>
</div>
<canvas #canvasElement style="display: none;"></canvas>
<button (click)="captureWithFilter()">
Capture with {{ getFilterName() }}
</button>
<div *ngIf="capturedImage" class="preview">
<img [src]="capturedImage">
<button (click)="download()">Download</button>
</div>
</div>
`
})
export class PhotoFiltersComponent {
selectedFilter = 'none';
capturedImage: string | null = null;
getFilterName(): string {
return this.selectedFilter.split('(')[0] || 'None';
}
captureWithFilter() {
// Implementation similar to previous examples
}
download() {
// Download implementation
}
}
Module Configuration
Import required modules.
app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PhotoCaptureComponent } from './photo-capture/photo-capture.component';
import { VideoRecorderComponent } from './video-recorder/video-recorder.component';
import { CameraSwitcherComponent } from './camera-switcher/camera-switcher.component';
import { CameraService } from './camera.service';
@NgModule({
declarations: [
AppComponent,
PhotoCaptureComponent,
VideoRecorderComponent,
CameraSwitcherComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [CameraService],
bootstrap: [AppComponent]
})
export class AppModule { }
Permission Guard
Guard for checking camera permissions.
camera-permission.guard.ts:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class CameraPermissionGuard implements CanActivate {
constructor(private router: Router) {}
async canActivate(): Promise<boolean> {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true
});
stream.getTracks().forEach(track => track.stop());
return true;
} catch (error) {
alert('Camera permission required');
return false;
}
}
}
Snapshot Pipe
Format file sizes for downloads.
filesize.pipe.ts:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filesize'
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
}
Testing Components
Unit test for camera service.
camera.service.spec.ts:
import { TestBed } from '@angular/core/testing';
import { CameraService } from './camera.service';
describe('CameraService', () => {
let service: CameraService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CameraService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should start camera stream', async () => {
const mockStream = new MediaStream();
spyOn(navigator.mediaDevices, 'getUserMedia')
.and.returnValue(Promise.resolve(mockStream));
await service.startCamera();
service.stream$.subscribe(stream => {
expect(stream).toBe(mockStream);
});
});
it('should stop camera stream', () => {
const mockTrack = jasmine.createSpyObj('MediaStreamTrack', ['stop']);
const mockStream = new MediaStream();
spyOn(mockStream, 'getTracks').and.returnValue([mockTrack]);
service['currentStream'] = mockStream;
service.stopCamera();
expect(mockTrack.stop).toHaveBeenCalled();
});
});
Performance Optimization
Optimize video recording for large files.
export class OptimizedRecorderComponent {
private recordingConfig = {
mimeType: 'video/webm;codecs=vp8',
videoBitsPerSecond: 1000000, // 1 Mbps
timeSlice: 1000 // Record in 1-second chunks
};
startOptimizedRecording(stream: MediaStream) {
const mediaRecorder = new MediaRecorder(stream, this.recordingConfig);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
// Process chunks immediately
this.processChunk(event.data);
}
};
mediaRecorder.start(this.recordingConfig.timeSlice);
}
private processChunk(chunk: Blob) {
// Handle chunk (upload, store, etc.)
console.log('Chunk size:', chunk.size);
}
}
Quick Reference
Service Methods:
startCamera(constraints?: MediaStreamConstraints)
stopCamera()
getAvailableDevices()
Capture Photo:
const canvas = this.canvasElement.nativeElement;
canvas.getContext('2d')?.drawImage(video, 0, 0);
const dataUrl = canvas.toDataURL('image/jpeg');
Record Video:
const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.start();
mediaRecorder.stop();
Download File:
const link = document.createElement('a');
link.href = dataUrl;
link.download = 'file.jpg';
link.click();
RxJS Cleanup:
private destroy$ = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
Browser Support
Modern browsers with MediaDevices API:
- Chrome 53+
- Firefox 36+
- Safari 11+
- Edge 79+
HTTPS required for production.
Best Practices
- Always clean up streams in ngOnDestroy
- Use RxJS operators for stream management
- Handle errors with proper user feedback
- Implement loading states
- Use TypeScript interfaces for type safety
- Create reusable services for camera logic
- Test camera permissions before access
- Optimize recording settings for file size
- Provide clear UI for recording state
- Clean up object URLs after use
Conclusion
Angular provides powerful tools for building camera applications. Use services for stream management, components for UI logic, and RxJS for reactive state handling. TypeScript ensures type safety throughout the application.
Related Articles
Angular 21 jsPDF Example to Edit & Modify PDF Files in Browser
Learn how to integrate jsPDF with Angular 21 to create, edit, and modify PDF documents directly in the browser using standalone components and signals.
How to Fix Property does not exist on type 'never' Error in Angular
Learn how to fix the 'Property does not exist on type never' error in Angular TypeScript. This comprehensive guide covers type guards, unions, and best practices.
Fix: ExpressionChangedAfterItHasBeenCheckedError in Angular - Complete Tutorial
Complete guide to fix ExpressionChangedAfterItHasBeenCheckedError in Angular applications. Learn how to resolve change detection issues with practical solutions, debugging techniques, and best practices for Angular development.