How to build an image cropper form control in angular

| By Maina Wycliffe | | | 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 i croppie ngx-croppie

yarn add croppie ngx-croppie

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


npm i @types/croppie -D

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.

Angular Reactive Forms – Building Custom Validators

Angular has built-in input validators for common input validation such as checking min and max length, email etc. These built-in validators …

Read More
A Guide for Building Angular 6 Libraries

In an earlier post, I wrote about Angular CLI Workspaces, a new feature in Angular 6 that allows you to have more than one project in a …

Read More

Comments