How to implement a form using Multipart data in Angular?

Varvara Sandakova
5 min readNov 1, 2021

--

As a Software Engineer, I face up a lot of challenges in my work on а regular basis.

Recently, I had a task with a new definition — multipart data. So, as I have a growth mindset( I hope =)) I try to interpret this question as an interesting challenge and dive deeply into it.

Clearly, people with the growth mindset thrive when they’re stretching themselves. When do people with the fixed mindset thrive? When things are safely within their grasp. If things get too challenging — when they’re not feeling smart or talented — they lose interest

Mindset: Changing The Way You Think To Fulfil Your Potential. Carol S. Dweck

Multipart data

Let’s consider in detail what is it multipart data.

As we know from the resource the form has the enctype attribute — entry list encoding type to use for form submission.

There are three basic options for enctype:

  • application/x-www-form-urlencoded (default attribute)
  • multipart/form-data. In this article we’ll take a look at the second option — multipart/form-data that is used for uploading files such as pdf, images, for MIME-type content and you’ll see examples of how all those cases work.
  • text-plain ( data without any encoding (HTML5) e.x. text)

As a result, our form will have the next template:

<form enctype=”application/x-www-form-urlencoded | multipart/form-data | text/plain”>

// content of the form

</form>

Multipart/form-data media type can be used by a wide variety of applications and transported by a wide variety of protocols as a way of returning a set of values as the result of a user filling out a form.

Multipart/form-data follows the model of multipart MIME data streams. A multipart/form-data body contains a series of parts separated by a boundary.

So, we should use a “multipart/form-data” when form includes <input type=”file”> elements or multiple files should be returned as the result of a single form entry.

Let’s imagine when we want to upload information about our trip:

  • name
  • image
  • pdf file

trip-upload.component.html

<form [formGroup]="form" enctype="multipart/form-data" (ngSubmit)="submitForm()">
<div class="form-group">
<label for="trip">Trip:</label>
<input formControlName="name" type="text" id="trip" name="trip" placeholder="Name of the trip">
</div>

<div class="form-group">
<label for="guide">Guide for the trip:</label>
<input formControlName="guide" type="file" id="guide" name="guide" (change)="uploadFile($event,'guide')">
</div>

<div class="form-group">
<label for="photo">Guide for the trip:</label>
<input formControlName="photo" type="image" id="photo" name="photo" (change)="uploadFile($event, 'photo')">
</div>

<div class="form-group">
<button>Upload files</button>
</div>
</form>

trip-upload.component.ts

import {Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from "@angular/forms";
import { HttpClient } from '@angular/common/http'
import {Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from "@angular/forms";
import { HttpClient } from '@angular/common/http'
@Component({
selector: 'trip-upload',
templateUrl: './trip-upload.component.html',
styleUrls: ['./trip-upload.component.css']
})
export class TripUploadComponent implements OnInit {
public form: FormGroup;
constructor(public fb: FormBuilder, private http: HttpClient) {}
ngOnInit() {
this.form = this.fb.group({
name: [''],
photo: [null],
guide: [null]
})
}
uploadFile(event, fileType: string) {
this.updateFileFormControl(event, fileType);
}
submitForm() {
let formData: any = newFormData(); Object.keys(this.form.controls).forEach(formControlName => { formData.append(formControlName, this.form.get(formControlName).value);
});
this.http.post('http://localhost:4200/api/trip',formData)
.subscribe(
(response) =>console.log(response),
(error) =>console.log(error)
)}

private updateFile(event: Event, formControlName: string){
const file = (event.target as HTMLInputElement).files[0]; this.form.controls[formControlName].patchValue([file]); this.form.get(formControlName).updateValueAndValidity()
}}

Multipart response

When Browser understands which enctype you use in your form for HTTP POST requests, user-agent configure a list of name/value pairs to the server. Depending on the type and amount of data being transmitted, one of the methods will be more efficient than the other:

Example of multipart/form-data response:

There are four important fields in the response which we are interested in:

— <<boundary_value>>

Content-Disposition: form-data; name=”<<field_name>>”

Content-Type: type of the data

<<field_value>>

The “Boundary” Parameter is one of the clues in the multipart response:

“Boundary” Parameter of multipart/form-data

As with other multipart types, the parts are delimited with a boundary delimiter, constructed using CRLF, “ — “, and the value of the “boundary” parameter. The boundary is supplied as a “boundary” parameter to the multipart/form-data type. The boundary delimiter MUST NOT appear inside any of the encapsulated parts, and it is often necessary to enclose the “boundary” parameter values in quotes in the Content-Type header field.

Content-Disposition Header Field for Each Part

Each part MUST contain a Content-Disposition header field [RFC2183] where the disposition type is “form-data”. The Content-Disposition header field MUST also contain an additional parameter of “name”; the value of the “name” parameter is the original field name from the form. Content-Disposition: form-data; name=”user” For form data that represents the content of a file, a name for the file SHOULD be supplied as well, by using a “filename” parameter of the Content-Disposition header field. The file name isn’t mandatory for cases where the file name isn’t available or is meaningless or private.

There are several ways to parse multipart data:

  1. Parsing on Server side (preferably)
  2. Parsing using libraries:

3. Parsing on UI side (not preferably)

It is a good practice to parse multipart data on the Server side.

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from ‘@angular/common/http’;
import {Injectable} from ‘@angular/core’;
import { Observable } from ‘rxjs’;
import { map } from ‘rxjs/operators’;
@Injectable()
export class MultipartInterceptService implements HttpInterceptor {
private parseData(multipart: string, boundary: string): any {
const dataArray: string[] = multipart.split(` — ${boundary}`);
dataArray.shift();
dataArray.map((dataBlock: string) => {
const rowsArray = dataBlock.split(/\r?\n/).filter(Boolean);
const image = rowsArray.splice(3, rowsArray.length);
if (image.length < 1) {
return;
}
const headers = dataBlock.split(/\r?\n/).splice(1, 4).filter(Boolean);
if (headers.length > 1) {
const pattern = /Content-Type: ([a-z\/+]+)/g;
const match = pattern.exec(headers[1]);
if (match === null) {
throwError(‘Unable to find Content-Type header value’);
}
const contentType = match[1];
if (contentType === ‘application/pdf’) {
// save pdf data
} else if (contentType === ‘image/svg+xml’) {
// save image data
}
}
});
return result;
}
private parseResponse(response: HttpResponse<any>): HttpResponse<any> {
const contentTypeHeaderValue = response.headers.get(‘Content-Type’);
const body = response.body;
const contentTypeArray = contentTypeHeaderValue ? contentTypeHeaderValue.split(‘;’) : [];
const contentType = contentTypeArray[0];
switch (contentType) {
case ‘multipart/form-data’:
if (!body) {
return response.clone({ body: {} });
}
const boundary = body?.split(‘ — ‘)[1].split(‘\r’)[0];
const parsed = this.parseData(body, boundary);
if (parsed === false) {
throwError(‘Unable to parse multipart response’);
}
return response.clone({ body: parsed });
default:
return response;
}
}
// intercept request and add parse custom response
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(customRequest).pipe(
map((response: HttpResponse<any>) => {
if (response instanceof HttpResponse) {
return this.parseResponse(response);
}
})
);
}
}

More information about interceptors you can read here.

Conclusion:

In this article we considered the next question:

  • what is it multipart data;
  • how to upload several files as multipart data;
  • multipart/form-data response;
  • how to parse multipart data using Interceptor.

References:

  1. https://html.spec.whatwg.org/multipage/forms.html#element-attrdef-form-enctype

2. https://datatracker.ietf.org/doc/html/rfc7578

3. https://www.w3schools.com/tags/att_form_enctype.asp

4. https://html.spec.whatwg.org/multipage/forms.html#element-attrdef-form-enctype

5. https://www.amazon.com/Mindset-Psychology-Carol-S-Dweck/dp/0345472322

--

--