I’m having issues trying to use cropperjs package – https://www.npmjs.com/package/cropperjs. I’m using it in a angular18 project. The image is being uploaded and sent to the cropper but the cropper does not let me crop the image, it only shows the image. I have 2 components, one for the user to upload the image and one for the crop dialog. The user clicks or drags the file to the input and the input is going to open the crop dialog.
Upload Component ts file
import {
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
EventEmitter,
Inject,
inject,
Injector,
input,
viewChild,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { DragDropFileDirective } from '@shared/directives/drag-drop-file.directive';
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { GenericControlValueAccessorDirective } from '@shared/directives/generic-control-value-accessor.directive';
import { MatInput } from '@angular/material/input';
import { BreakpointObserver } from '@angular/cdk/layout';
import { toSignal } from '@angular/core/rxjs-interop';
import { CropDialogComponent } from './crop-dialog/crop-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'upload-image',
standalone: true,
imports: [
CommonModule,
DragDropFileDirective,
ReactiveFormsModule,
MatInput,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: upload-image,
multi: true,
},
],
templateUrl: 'upload-image.component.html',
styleUrls: ['.upload-image.component.css'],
})
export class UploadComponent<T> extends GenericControlValueAccessorDirective<T> {
constructor(
public matDialog: MatDialog,
@Inject(Injector) private Injector: Injector
) {
super(Injector);
}
public fileSelected = new EventEmitter<File>();
public files: any[] = [];
public acceptedFormats = ['image/jpeg', 'image/png', 'image/jpg'];
readonly fileInputUploadRef =
viewChild.required<ElementRef<HTMLInputElement>>('fileInputUploadRef');
override isRequired = true;
public drawBorder = false;
public selectFile(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
this.fileInputUploadRef().nativeElement.click();
}
public onFileSelected(event: any) {
const file = event.target.files[0];
if (file) {
this.fileSelected.emit(file);
this.openCropDialog(file);
}
}
public onFilesDropped(files: File[]) {
if (files.length > 0) {
this.fileSelected.emit(files[0]);
this.openCropDialog(files[0]);
this.drawBorder = false;
}
}
public setDropBorder(value: boolean) {
this.drawBorder = value;
}
public fileBrowseHandler(event: any) {
this.prepareFilesList(event.target.files);
}
private prepareFilesList(files: Array<any>) {
for (const item of files) {
this.files.push(item);
this.openCropDialog(item);
}
}
public openCropDialog(file: File) {
const dialogRef = this.matDialog.open(CropDialogComponent, {
width: '40rem',
maxHeight: '95vh',
data: {
files: this.files,
img: file,
isLandscapeFormatAllowed: false,
},
disableClose: false,
});
}
}
upload-component.html
<div class="drag_drop_container" dragDropFile (fileDropped)="onFilesDropped($event)">
<figure class="office_image">
<label class="office_image_upload border-dashed border-2 border-sky-500" aria-label="Upload area">
<input
type="file"
id="officeImageUpload"
#fileInputUploadRef
(change)="onFileSelected($event)"
[accept]="acceptedFormats"
[formControl]="control"
[required]="isRequired"
class="hidden"
/>
<img
[src]="kanzleiBildUrl || ''"
alt="A portrait of the law firm's office"
aria-hidden="true"
(click)="selectFile($event)"
/>
<span *ngIf="!kanzleiBildUrl" class="kanzlei_bild_upload_icon" aria-hidden="true">
<mat-icon class="cursor-pointer" svgIcon="fi-hochladen-20"></mat-icon>
</span>
<div class="kanzlei_bild__upload__text cursor-pointer">
<button mat-button (click)="selectFile($event)">Datei auswählen</button>
</div>
</label>
</figure>
</div>
</div>
cropper.ts file
import { Component, Inject, ViewChild, inject } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog';
import { Subscription } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { MatInputModule } from '@angular/material/input';
import { MatCardModule } from '@angular/material/card';
import Cropper from 'cropperjs';
@Component({
selector: 'app-crop-dialog',
templateUrl: './crop-dialog.component.html',
standalone: true,
imports: [
MatDialogModule,
MatIconModule,
MatFormFieldModule,
FormsModule,
CommonModule,
MatInputModule,
MatDialogActions,
MatDialogClose,
],
providers: [],
})
export class CropDialogComponent {
cropper: any;
imageInput: string | null = null;
fileReader = new FileReader();
newImgLoaded = false;
imageTooHeight = false;
@ViewChild('image') image: any;
@ViewChild('imagePreLoad') imagePreload: any;
constructor(
public dialogRef: MatDialogRef<CropDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.loadFile(data.img);
}
ngAfterViewInit() {
this.fileReader.addEventListener('load', this.setImage.bind(this));
this.initCropper();
}
private loadFile(file: File) {
if (file) {
this.fileReader.readAsDataURL(file);
}
}
private setImage() {
this.imageInput = this.fileReader.result as string;
this.initCropper();
}
onImageLoad(evt: any) {
if (evt && evt.target.id === 'image') {
this.newImgLoaded = true;
}
}
private initCropper() {
if (this.cropper) {
this.cropper.destroy()
}
const options = {
aspectRatio: this.data.imageProperties?.aspectRatio || 1,
autoCrop: true,
autoCropArea: 1,
};
if (this.image?.nativeElement) {
this.cropper = new Cropper(this.image.nativeElement, options);
}
}
onSubmit() {
try {
if (this.imageInput) {
this.data.img = this.cropper
.getCroppedCanvas({
fillColor: 'white',
})
.toDataURL();
this.dialogRef.close(this.data.img);
}
} catch (error) {
console.error('Error cropping image:', error);
}
}
`}
cropper html file`
<div class="close-button absolute right-3 z-[1]">
<button mat-mini-fab aria-label="Dialog schließen" mat-dialog-close>
<mat-icon svgIcon="sli-schliessen-gross-20"></mat-icon>
</button>
</div>
<h2 class="mb-4 heading-4 font-sans">Bild bearbeiten</h2>
<img #imagePreLoad class="hidden" alt="" [src]="imageInput" (load)="onImageLoad($event)" />
<ng-template #cropImage>
<img
[hidden]="!imageInput"
id="image"
#image
class="image-cropper"
alt="image"
[src]="imageInput"
(load)="onImageLoad($event)"
/>
</ng-template>
<div mat-dialog-content>
<div class="imageNotTooHeight">
<ng-container *ngTemplateOutlet="cropImage"></ng-container>
</div>
</div>
<div class="flex flex-col gap-4 mt-6">
<mat-form-field class="alt-form-field">
<input matInput type="text" name="image-alttext" id="image-alttext" maxlength="100" />
</mat-form-field>
I tried multiple methods to implement the cropper js for this use case but it does not seem to work in this case.