Handling File Uploads in Angular: Reactive Approach

Utkarsh Chakravarty
Webtips
Published in
7 min readMay 31, 2020

Explaining how to handle client-side code for file uploads in Angular

In this article, I will tell you about how to create a reactive form to create a post that has a title, description, and image. It will also have the feature to preview the image in the form itself. Let’s get started.

Create a new Project

Run the CLI command ng new and provide the name file-upload, as shown here:

ng new file-upload

Installing Angular Material

Use the Angular CLI’s install schematic to set up your Angular Material project by running the following command:

ng add @angular/material

We will also use Angular Material in this project as it provides awesome resources to make our form look more attractive. I always recommend using this.

Create a new Component

As we all know, Angular follows a component-based Architecture so we should create different components for different purposes as it is considered a better programming approach. It also helps to make our code simple and clean. Now create a new component for creating a post using Angular CLI command.

ng generate component post-create

Creating a model

Create an interface of the post model so that we don’t have to define the post again and again, rather import this model and use it.

//post.model.tsexport interface Post {
id: string;
title: string;
description: string;
imagePath: string;
}

Creating the Form

Let’s create a post form that has a title, description and image. We will be using the Reactive technique so make sure to import ReactiveFormsModule in your app.module.ts file.

...
import {ReactiveFormsModule} from '@angular/forms';
@NgModule({
declarations: [
...
],
imports: [
...
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now we can use the Reactive Forms in our project so let’s make a form in which there are three fields as mentioned earlier in the article.

Note: Make sure to add imports of the respective material modules used in the project to the app.module.ts file.

Now move to the app.component.html file and write the following code.

<form [formGroup]="form" (ngSubmit)="onSavePost()"><mat-form-field appearance="fill"><mat-label>Title</mat-label><input type="text" placeholder="Title" formControlName="title"   matInput><mat-error *ngIf="!form.get('title').valid">Please enter a valid  title</mat-error></mat-form-field><br><button type="button" mat-stroked-button
(click)="fileUplaoder.click()">Upload</button>
<br><input type="file" #fileUplaoder (change)="onSelect($event)"><div class="preview-image" *ngIf="imagePreview !== '' && imagePreview && form.get('image').valid"><img [src]="imagePreview" [alt]="form.value.title"></div><br><mat-form-field appearance="fill"><mat-label>Description</mat-label><textarea placeholder="Description" formControlName="description"
rows="6" matInput>
</textarea>
<mat-error *ngIf="!form.get('description').valid">Please enter a valid description</mat-error></mat-form-field><br><button type="submit" mat-raised-button color="primary"> Create </button></form>

This is a reactive form in which we’ve created a form group that has title, description, and image. In this, we are using a file type input field in which we have provided reference to #fileUploader which is accessed by a button so to make it look better. In CSS we are hiding the visibility of the basic input file field so that only the styled button is visible to us.

onSelect($event) function will emit an event on selecting a file.

‘imagePreview’ is the URL of the image we are handling in the ts file of this component. We are using data binding for the src URL as the data is dynamic as the input by the user.

Now let’s move to post-create.component.ts to write the logic for the form.

import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {PostService} from '../post.service';
@Component({
selector: 'app-post-create',
templateUrl: './post-create.component.html',
styleUrls: ['./post-create.component.css']
})
export class PostCreateComponent implements OnInit {
form: FormGroup;
imagePreview: string;

constructor(private postService: PostService) { }

ngOnInit(): void {
this.form = new FormGroup({title: new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}),description: new FormControl(null, {validators: [Validators.required]}),image: new FormControl(null, {validators: [Validators.required]})
});
}

onSavePost() {
if (!this.form.valid) {return;}const post = {id: null,
title: this.form.value.title,
description: this.form.value.description,
image: this.form.value.image
};this.postService.addPost(post);this.form.reset();}

onSelect(event: Event) {
const file = (event.target as HTMLInputElement).files[0];this.form.patchValue({image: file});this.form.get('image').updateValueAndValidity();const reader = new FileReader();reader.onload = () => {this.imagePreview = reader.result.toString();};reader.readAsDataURL(file);}
}

We initialize and validate the form elements in the ngOnInit() function which is invoked immediately when the component is loaded.

The onSelect(event: Event) function firstly stores the selected file in the file variable then patch the file in the form on the image field.

As the user might change his/her mind and upload another file so updateValueAndValidity() function provided in reactive forms updates the form image field accordingly.

The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user’s computer, using File or Blob objects to specify the file or data to read.

Important note: FileReader is used to read file content from the user’s (remote) system in secure ways only.

The readAsDataURL method is used to read the contents of the specified Blob or File. onload function executes after this and the result is stored on the imagePreview variable which has the path of the image so it can be displayed on the form.

Creating MIME-TYPE Validator

It is used to ensure that the user uploads the file with the correct file extension or not. Create a mime-type.validator.ts file inside the post-create component and write the following code.

import {AbstractControl} from '@angular/forms';
import {Observable, Observer, of} from 'rxjs';

export const mimeType = (control: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> => {
if (typeof (control.value) === 'string') {return of(null);}const file = control.value as File;const fileReader = new FileReader();const frObs = Observable.create((observer: Observer<{ [key: string]: any }>) => {fileReader.addEventListener('loadend', () => {const arr = new Uint8Array(fileReader.result as
ArrayBuffer).subarray(0, 4);
let header = '';let isValid = false;for (let i = 0; i < arr.length; i++) {header += arr[i].toString(16);}switch (header) {case '89504e47':
isValid = true;
break;
case 'ffd8ffe0':case 'ffd8ffe1':case 'ffd8ffe2':case 'ffd8ffe3':case 'ffd8ffe8':isValid = true;
break;
default:isValid = false; // Or you can use the blob.type as fallbackbreak;
}
if (isValid) {observer.next(null);} else {observer.next({invalidMimeType: true});}observer.complete();});fileReader.readAsArrayBuffer(file);}
);
return frObs;};

The reactive form provides AsyncValidatorFn which is a function that receives control and returns a Promise or observable that emits validation errors if present, otherwise null. It has to included in the validators array for the image in the post-create.component.ts file to use it.

import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {mimeType} from './mime-type.validator';
@Component({
selector: 'app-post-create',
templateUrl: './post-create.component.html',
styleUrls: ['./post-create.component.css']
})
export class PostCreateComponent implements OnInit {
form: FormGroup;
imagePreview: string;

constructor() { }

ngOnInit(): void {
this.form = new FormGroup({title: new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}),description: new FormControl(null, {validators: [Validators.required]}),image: new FormControl(null, {validators: [Validators.required], asyncValidators: [mimeType]})});
}

Creating a Service

We should always write backend interaction logics in the service file so to maintain a clean structure and make it easier for debugging. Firstly import the HttpClientModule in app.module.ts file.

...
import {HttpClientModule} from '@angular/common/http';
@NgModule({
declarations: [
...
],
imports: [
...
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Then create a post.service.ts file and write the following code.

import {Injectable} from '@angular/core';
import {Post} from './post.model';
import {HttpClient} from '@angular/common/http';

@Injectable({
providedIn: 'root'
})
export class PostService {

readonly baseURL = 'http://localhost:3000';
posts: Post[] =[];

constructor(private http: HttpClient) {
}

addPost(data) {
const postData = new FormData();postData.append('title', data.title);postData.append('description', data.description);postData.append('image', data.image);

this.http.post<{ message: string, post: Post }>(this.baseURL + '/api/posts', postData).subscribe((res) => {
const post: Post = {id: res.post.id, title: data.title, description: data.description, imagePath: res.post.imagePath};this.posts.push(post);}, error => {console.log(error);});
}
}

We are using FormData to store the value as it allows us to store our text files as well as the blob file that are to be sent to the backend. So all the data received from the form is appended to the postData which is of type FormData. It is then sent as a payload of the post request. In response, we are getting an object which contains a message and the created post which has the Object Id created in the backend that we have to store on id field of the Post and then store rest of the data and save it in an array so that it can be used to display if needed.

The Server Endpoint

We’ll conclude by showing the server-side logic and patterns. In this example, we’ll use Node and Express. For file uploading, we’ll be using a popular npm library multer to store the image files on the backend.

const express = require('express');
const path = require('path');
const multer = require('multer');
const app = express();const port = process.env.PORT || 3000;

app.use(express.json());
//To allow access to the image directory
app.use("/images", express.static(path.join("backend/images")));
const MIME_TYPE_MAP = {
"image/png": "png",
"image/jpeg": "jpg",
"image/jpg": "jpg"
};
//To prevent CORS error
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
next();
});
//To define the location and filename
const storage = multer.diskStorage({
destination: (req, file, cb) => {const isValid = MIME_TYPE_MAP[file.mimetype];let error = new Error("Invalid mime type");if (isValid) {error = null;}cb(error, "backend/images");},filename: (req, file, cb) => {const name = file.originalname.toLowerCase().split(" ").join("-");const ext = MIME_TYPE_MAP[file.mimetype];cb(null, name + "-" + Date.now() + "." + ext);
}
});
//post req to store the Post
app.post('/api/posts', multer({storage: storage}).single('image'), (req, res, next) => {
const url = req.protocol + "://" + req.get("host");const post = new Post({title: req.body.title,
description: req.body.description,
imagePath: url + "/images/" + req.file.filename
});post.save().then((createdPost) => {res.status(201).send({message: 'Post Added Successfully',
post: {
...createdPost,
id: createdPost._id
}
});
});
});
app.listen(port, () => {console.log('Server is up on', port);});

Run the Application

Go to workspace folder and launch the server by using the CLI command ng serve and test your work.

ng serve

Take a look at an example of this code for any query.

Thank you for reading!

Leave a clap👏 if you liked the article.😊

--

--