Thank you for reading my blog posts, I am no longer publishing new content on this platform. You can find my latest content on either mainawycliffe.dev or All Things Typescript Newsletter (✉️)
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.
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.
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.
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);
}
}
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 {}
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>
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.
Angular has built-in input validators for common input validation such as checking min and max length, email etc. These built-in validators …
Read MoreIn 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