How to build an image cropper form control in angular

In an earlier post, I demonstrated how to build an extremely simple custom form control. Today, I have decided to try to up the stakes. We are going to build an image cropper and package it as a form control which is ready to be used in any of our forms – be it template driven or reactive forms.

My goal for this post, is to create something that works like the file input html element, that you can select, crop and validate its content. A simple drop in solution in your forms for cropping images easily alongside other data input.

Getting Started

In this demo, we are going to be using Croppie JavaScript Library to crop our images. To make things easier, we will use an angular wrapper called ngx-croppie. I will try to make it easier to follow for those using a different Image Cropper JavaScript Library.

You can install the wrapper together with croppie using either NPM or Yarn:

NPM
npm i croppie ngx-croppie
YARN
yarn add croppie ngx-croppie

And then install @types/croppie (croppie typings for TypeScript) as dev dependencies in your project:

NPM
npm i @types/croppie -D

YARN
yarn add –dev "@types/croppie

Then import NgxCroppieModule in your module.

@NgModule({
  declarations: [ ...],
  imports: [
    ...
    NgxCroppieModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})

And then add croppie.css in your projects list of styles inside the angular.json:

"styles": [
   ...
   "node_modules/croppie/croppie.css"
   ...
],

First, we need to create a new component which will wrap around ngx-croppie component and implement the ControlValueAccessor interface.

Using ngx-croppie to crop images

NB: You can skip this whole section if you are using a different image cropper library.

Inside the component controller, we need a croppieImage property which will hold our current image. Then, we need to pass the height and width of our cropper using our cropper, so we need imgCropToHeight and imgCropToWidth properties – both have the Input() decorator.

I have also added responseType property, allowing us to request the image in either base64 or blob type. And an outputOptions property, which we pass to ngx-croppie and allows us to set the image size and output format of the image. On top of that we need ngxCroppie property – marked by viewChild decorator, which just passes the element where our image cropper will be rendered into.

Methods for ngx-croppie

The first method we need is to construct CroppieOptions. This will stitch together the variables we passed to our component, to create CroppieOptions. You can learn more about the available CroppieOptions here.

public get croppieOptions(): CroppieOptions {
    const opts: CroppieOptions = {};
    opts.viewport = {
      width: parseInt(this.imgCropToWidth, 10),
      height: parseInt(this.imgCropToHeight, 10)
    };

    opts.boundary = {
      width: parseInt(this.imgCropToWidth, 10) + 50,
      height: parseInt(this.imgCropToWidth, 10) + 50
    };

    opts.enforceBoundary = true;
    return opts;
}

Next, we need a method that gets triggered when an image is selected.

imageUploadEvent(evt: any) {
    if (!evt.target) {
      return;
    }
    if (!evt.target.files) {
      return;
    }

    if (evt.target.files.length !== 1) {
      return;
    }

    const file = evt.target.files[0];
    if (
      file.type !== 'image/jpeg' &&
      file.type !== 'image/png' &&
      file.type !== 'image/gif' &&
      file.type !== 'image/jpg'
    ) {
      return;
    }

    const fr = new FileReader();
    fr.onloadend = loadEvent => {
      this.croppieImage = fr.result.toString();
    };

    fr.readAsDataURL(file);
  }

After that, we need another method that is triggered when you crop your image. When it is triggered, the image is passed as a parameter that you can then it to the croppieImage property.

newImageResultFromCroppie(img: string) {
    this.croppieImage = img;
    //set this croppieImage value as the value of the component
    this.propagateChange(this.croppieImage);
}

Then, on component initialization, we need to set our return image type – whether base64 or blob image type.

ngOnInit() {
    /* Size the outputoptions of our cropped imaged - whether is base64 or blob */
    this.outputoption = { type: this.responseType, size: 'original' };
}

Next, we need to add a ngOnChanges Method.

ngOnChanges(changes: any) {
    if (this.croppieImage) {
      return;
    }

    if (!changes.imageUrl) {
      return;
    }

    if (!changes.imageUrl.previousValue && changes.imageUrl.currentValue) {
      this.croppieImage = changes.imageUrl.currentValue;
      this.propagateChange(this.croppieImage);
    }
}

ControlValueAccessor Interface Methods

After that, we need to implement the methods necessary for ControlValueAccessor interface. This is what gives our component the behavior of a form control. I explained everything my previous post here about creating custom form controls.

writeValue(value: any) {
    if (value !== undefined) {
      this.croppieImage = value;
      this.propagateChange(this.croppieImage);
    }
}

propagateChange = (_: any) => {};

registerOnChange(fn) {
    this.propagateChange = fn;
}

registerOnTouched() {}

And finally, provide the NG_VALUE_ACCESSOR for the component:

providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomImageFormControlComponent),
      multi: true
    }
]

And don’t forget to add the interfaces we are implementing to our component:

export class CustomImageFormControlComponent implements OnInit, OnChanges, ControlValueAccessor { }

Our Template

And then in our template, we need to add ngx-croppie component, a hidden file input html element and a button to trigger the select file dialog of our file input html element.

<ngx-croppie *ngIf="croppieImage" #ngxCroppie [outputFormatOptions]="outputoption" [croppieOptions]="croppieOptions" [imageUrl]="croppieImage"(result)="newImageResultFromCroppie($event)"></ngx-croppie>
<input #imageUpload hidden type="file" id="fileupload" #imageUpload (change)="imageUploadEvent($event)" accept="image/gif, image/jpeg, image/png"/>
<button fxFlex="100" class="text-white font-weight-bold mat-elevation-z0" type="button" mat-raised-button color="primary" (click)="imageUpload.click()">
  <mat-icon>add_a_photo</mat-icon>
  Select Image
</button>

Using our image form control

To use our image form control, we just use a normal form control:

myform: FormGroup = null;

constructor(private fb: FormBuilder) {}

createForm(): FormGroup {
    return this.fb.group({
      BlobImage: [null, Validators.compose([Validators.required])],
      base64Image: [null, Validators.compose([Validators.required])]
    });
}

ngOnInit() {
    this.myform = this.createForm();
}

submit() {
    console.log(this.myform.value);
}

And then in our template:

<form [formGroup]="myform" (submit)="submit()" fxFlex="100">
  <div fxFlex="500px" fxLayoutAlign="center center" fxLayout="column" fxFlexOffset="calc(50% - 250px)" fxFlex.xs="100" fxFlexOffset.xs="0"
    style="padding: 10px;">
    <app-custom-image-form-control formControlName="BlobImage" [responseType]="'blob'"></app-custom-image-form-control>
    <mat-error *ngIf="myform.controls['BlobImage'].hasError('required')"> This field is required </mat-error>
    <app-custom-image-form-control formControlName="base64Image"></app-custom-image-form-control>
    <mat-error *ngIf="myform.controls['base64Image'].hasError('required')"> This field is required </mat-error>
    <button fxFlex mat-button color="primary">
      <mat-icon>save</mat-icon>
      Submit</button>
  </div>
</form>

NB: We can also validate the content of our image form control. In the above post, I just made a required input, but you can do more by using custom form validators.

Demo and Source Code:

You can try this demo here and view this demo here.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.