Fixed multiple critical bugs and improved code quality based on comprehensive code review. 🔴 Critical Fixes: 1. Fix memory leak in event listeners (touches.ts) - Problem: Event listeners were never actually removed on destroy - Cause: method.bind(this) creates new function reference each time - Solution: Store bound handlers in Map for proper removal - Impact: Prevents memory leaks when components are destroyed - Files: touches.ts:52, 93-140, 142-165 2. Fix template signal syntax (pinch-zoom.component.html) - Problem: Signals not called in template bindings - Fixed: isDragging -> isDragging() - Fixed: isZoomedIn -> isZoomedIn() - Impact: Critical - features were broken, dragging class never applied - Files: pinch-zoom.component.html:1, 9 ⚠️ Medium Fixes: 3. Fix constructor early return (ivypinch.ts) - Problem: Constructor returned early if element missing - Solution: Throw error instead of early return - Impact: Better error reporting, prevents silent failures - Files: ivypinch.ts:40-46 4. Improve TypeScript type safety (ivypinch.ts) - Problem: Using definite assignment assertion (maxScale!) - Solution: Initialize maxScale with default value - Impact: Better null safety, clearer code - Files: ivypinch.ts:14, 25-26 💡 Improvements: 5. Add accessibility features (pinch-zoom.component.html) - Added keyboard support (Enter and Space keys) - Added ARIA labels for screen readers - Added role="button" and tabindex="0" - Impact: Better accessibility for all users - Files: pinch-zoom.component.html:11-15 Technical Details: Memory Leak Fix: - Added boundHandlers Map to store event listener references - toggleEventListeners now stores/retrieves handlers correctly - addEventListeners/removeEventListeners use same pattern - Dynamic listeners prefixed with 'dynamic-' key Before: const boundMethod = method.bind(this); element.addEventListener('event', boundMethod); // Later: element.removeEventListener('event', method.bind(this)); // ❌ Different ref! After: const boundMethod = method.bind(this); this.boundHandlers.set('event', boundMethod); element.addEventListener('event', boundMethod); // Later: const boundMethod = this.boundHandlers.get('event'); element.removeEventListener('event', boundMethod); // ✅ Same ref! Testing: ✅ Library builds successfully ✅ All TypeScript strict mode checks pass ✅ No compilation errors Breaking Changes: None Fixes: #memory-leak #signal-syntax #type-safety |
||
|---|---|---|
| docs | ||
| projects/ngx-pinch-zoom | ||
| src | ||
| .editorconfig | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| angular.json | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
ngx-pinch-zoom
An Angular library for pinch-to-zoom functionality on touch-enabled devices and mouse interactions. Built with Angular 20+ and modern signals API.
Features
- 🎯 Angular 20+ with Signals - Modern reactive programming
- 📱 Touch & Mouse Support - Works on all devices
- 🔄 Pinch to Zoom - Natural gesture support
- 🖱️ Mouse Wheel Zoom - Desktop-friendly
- 👆 Double Tap - Quick zoom in/out
- 🎨 Highly Configurable - Extensive options
- 📦 Standalone Component - No module imports needed
- ⚡ Performance Optimized - Uses signals for reactivity
Installation
npm install @meddv/ngx-pinch-zoom
Quick Start
1. Import the Component
import { Component } from '@angular/core';
import { PinchZoomComponent } from '@meddv/ngx-pinch-zoom';
@Component({
selector: 'app-root',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom>
<img src="path/to/image.jpg" alt="Zoomable image" />
</pinch-zoom>
`
})
export class AppComponent {}
2. Add Viewport Meta Tag
For proper touch support, add this to your index.html:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
Usage Examples
Basic Usage
<pinch-zoom>
<img src="image.jpg" />
</pinch-zoom>
With Configuration (Using Signals)
import { Component, signal } from '@angular/core';
import { PinchZoomComponent } from '@meddv/ngx-pinch-zoom';
@Component({
selector: 'app-example',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom
[transitionDuration]="200"
[doubleTap]="true"
[limitZoom]="3"
[autoZoomOut]="false"
[disabled]="isDisabled()"
(zoomChanged)="onZoomChange($event)">
<img src="image.jpg" />
</pinch-zoom>
`
})
export class ExampleComponent {
isDisabled = signal(false);
onZoomChange(scale: number) {
console.log('Current zoom level:', scale);
}
}
Programmatic Control
import { Component, viewChild } from '@angular/core';
import { PinchZoomComponent } from '@meddv/ngx-pinch-zoom';
@Component({
selector: 'app-controls',
standalone: true,
imports: [PinchZoomComponent],
template: `
<pinch-zoom #pinchZoom>
<img src="image.jpg" />
</pinch-zoom>
<button (click)="zoomIn()">Zoom In</button>
<button (click)="zoomOut()">Zoom Out</button>
<button (click)="reset()">Reset</button>
`
})
export class ControlsComponent {
pinchZoom = viewChild<PinchZoomComponent>('pinchZoom');
zoomIn() {
this.pinchZoom()?.zoomIn(0.5);
}
zoomOut() {
this.pinchZoom()?.zoomOut(0.5);
}
reset() {
this.pinchZoom()?.toggleZoom();
}
}
Configuration Options
| Input | Type | Default | Description |
|---|---|---|---|
transitionDuration |
number |
200 |
Animation duration in milliseconds |
doubleTap |
boolean |
true |
Enable double-tap to zoom |
doubleTapScale |
number |
2 |
Scale factor for double-tap zoom |
autoZoomOut |
boolean |
false |
Automatically reset zoom after pinch |
limitZoom |
number | 'original image size' |
'original image size' |
Maximum zoom level |
minScale |
number |
0 |
Minimum allowed scale |
disabled |
boolean |
false |
Disable all zoom functionality |
disablePan |
boolean |
false |
Disable panning with one finger |
disableZoomControl |
'disable' | 'never' | 'auto' |
'auto' |
Control zoom button visibility |
overflow |
'hidden' | 'visible' |
'hidden' |
CSS overflow behavior |
zoomControlScale |
number |
1 |
Scale factor for zoom controls |
backgroundColor |
string |
'rgba(0,0,0,0.85)' |
Container background color |
limitPan |
boolean |
false |
Prevent panning past image edges |
minPanScale |
number |
1.0001 |
Minimum scale at which panning is enabled |
listeners |
'auto' | 'mouse and touch' |
'mouse and touch' |
Event listener mode |
wheel |
boolean |
true |
Enable mouse wheel zoom |
wheelZoomFactor |
number |
0.2 |
Zoom factor for mouse wheel |
autoHeight |
boolean |
false |
Calculate height from image dimensions |
draggableImage |
boolean |
false |
Make image draggable |
draggableOnPinch |
boolean |
false |
Allow dragging while pinching |
Outputs
| Output | Type | Description |
|---|---|---|
zoomChanged |
OutputEmitterRef<number> |
Emits current scale when zoom changes |
Methods
Access these methods via template reference or viewChild:
| Method | Parameters | Returns | Description |
|---|---|---|---|
toggleZoom() |
- | void |
Toggle between zoomed in/out |
zoomIn(value) |
value: number |
number |
Zoom in by value, returns new scale |
zoomOut(value) |
value: number |
number |
Zoom out by value, returns new scale |
destroy() |
- | void |
Clean up event listeners |
Computed Properties
The component exposes several computed signals:
| Property | Type | Description |
|---|---|---|
scale() |
number |
Current zoom scale |
isZoomedIn() |
boolean |
Whether image is zoomed in |
isDisabled() |
boolean |
Whether zoom is disabled |
isDragging() |
boolean |
Whether user is currently dragging |
isZoomLimitReached() |
boolean |
Whether max zoom is reached |
maxScale() |
number |
Maximum allowed scale |
isControl() |
boolean |
Whether zoom controls should be shown |
Angular 20 Signals
This library fully embraces Angular 20's signals API:
Input Signals
All component inputs are now signal-based for better performance and reactivity.
// Before (Angular <16)
@Input() disabled: boolean = false;
// Now (Angular 20+)
disabled = input<boolean>(false);
Output Signals
Outputs use the new output() API:
// Before
@Output() zoomChanged = new EventEmitter<number>();
// Now
zoomChanged = output<number>();
Computed Signals
Derived state uses computed signals:
isZoomedIn = computed<boolean>(() => {
return this.scale() > 1;
});
Browser Support
- Chrome/Edge (latest 2 versions)
- Firefox (latest 2 versions)
- Safari (latest 2 versions)
- iOS Safari (latest 2 versions)
- Chrome for Android (latest 2 versions)
Requirements
- Angular 20.0.0 or higher
- TypeScript 5.8.0 or higher
- Node.js 18.19.1, 20.11.1, or 22.0.0+
Migration from Older Versions
If you're upgrading from a pre-signals version:
- Inputs: No changes needed in templates, binding syntax remains the same
- Outputs: Event binding syntax remains the same
- ViewChild: Update to
viewChildsignal (optional but recommended) - Component properties: Access computed properties by calling them:
component.scale()
Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
MIT
Credits
This project was forked and modernized for Angular 19/20 compatibility.
Original library: ngx-pinch-zoom
Issues and Support
Please report issues on GitHub Issues