October 22, 2024
Chicago 12, Melborne City, USA
javascript

Angular 18 – CropperJS not working properly on image upload


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.



You need to sign in to view this answers

Leave feedback about this

  • Quality
  • Price
  • Service

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video