Any type of file upload in Angular is not different than a regular file upload using native HTML file input control and a little bit of javascript. POSTing the file to the server can be achieved with the HttpClient available in Angular.

Running the sample application will give you the option to select an image with a file control. You can see the preview of the selected image before you start uploading it to the server. A progress control is also added so that you can see the upload progress in real-time.

The markup for the UI is pretty simple as,

<div class="columns">
  <div class="column is-one-third">
    <div class="field">
      <div class="file has-name is-primary is-fullwidth">
        <label class="file-label">
          <input
            class="file-input"
            type="file"
            name="file"
            #fileInput
            (change)="onChange(fileInput.files[0])"
          />
          <span class="file-cta">
            <span class="file-icon">
              <fa-icon [icon]="['fas', 'upload']"></fa-icon>
            </span>
            <span class="file-label">
              Choose a file…
            </span>
          </span>
          <span class="file-name">
            {{ fileName }}
          </span>
        </label>
      </div>
    </div>

    <article class="message" *ngIf="infoMessage">
      <div class="message-body">
        {{ infoMessage }}
      </div>
    </article>

    <div class="field" *ngIf="!infoMessage">
      <div class="control">
        <progress
          class="progress is-primary"
          [attr.value]="progress"
          max="100"
        ></progress>
      </div>
    </div>

    <div class="field">
      <div class="control">
        <button
          class="button is-primary"
          (click)="onUpload()"
          [attr.disabled]="isUploading ? '' : null"
        >
          Upload
        </button>
      </div>
    </div>
  </div>
  <div class="column">
    <figure class="image is-128x128">
      <img [src]="imageUrl" />
    </figure>
  </div>
</div>
user.component.html

I'm only concerned with a single upload that's why I passed the first file in the files array down to the onChange event.

Notice that there is no form element wrapped around the file input

The component class does nothing more interesting than reading the image using FileReader and setting the preview.

import { Component, OnInit } from "@angular/core";
import { UploaderService } from "../services/uploader.service";

@Component({
  selector: "app-user",
  templateUrl: "./user.component.html",
  styleUrls: ["./user.component.scss"]
})
export class UserComponent implements OnInit {
  progress: number;
  infoMessage: any;
  isUploading: boolean = false;
  file: File;

  imageUrl: string | ArrayBuffer =
    "https://bulma.io/images/placeholders/480x480.png";
  fileName: string = "No file selected";

  constructor(private uploader: UploaderService) {}

  ngOnInit() {
    this.uploader.progressSource.subscribe(progress => {
      this.progress = progress;
    });
  }

  onChange(file: File) {
    if (file) {
      this.fileName = file.name;
      this.file = file;

      const reader = new FileReader();
      reader.readAsDataURL(file);

      reader.onload = event => {
        this.imageUrl = reader.result;
      };
    }
  }

  onUpload() {
    this.infoMessage = null;
    this.progress = 0;
    this.isUploading = true;

    this.uploader.upload(this.file).subscribe(message => {
      this.isUploading = false;
      this.infoMessage = message;
    });
  }
}
user.component.ts
In this scenario, using Reactive Forms control only to check the validity of the attached file control is an overkill. That's why it is ignored.

What's interesting is the uploader service. Here's what it looks like:

import { Injectable } from "@angular/core";
import {
  HttpClient,
  HttpRequest,
  HttpEventType,
  HttpEvent
} from "@angular/common/http";
import { map, tap, last } from "rxjs/operators";
import { BehaviorSubject } from "rxjs";

@Injectable({
  providedIn: "root"
})
export class UploaderService {
  public progressSource = new BehaviorSubject<number>(0);

  constructor(private http: HttpClient) {}

  upload(file: File) {
    let formData = new FormData();
    formData.append("avatar", file);

    const req = new HttpRequest(
      "POST",
      "http://localhost:5000/upload",
      formData,
      {
        reportProgress: true
      }
    );

    return this.http.request(req).pipe(
      map(event => this.getEventMessage(event, file)),
      tap((envelope: any) => this.processProgress(envelope)),
      last()
    );
  }

  processProgress(envelope: any): void {
    if (typeof envelope === "number") {
      this.progressSource.next(envelope);
    }
  }

  private getEventMessage(event: HttpEvent<any>, file: File) {
    switch (event.type) {
      case HttpEventType.Sent:
        return `Uploading file "${file.name}" of size ${file.size}.`;
      case HttpEventType.UploadProgress:
        return Math.round((100 * event.loaded) / event.total);
      case HttpEventType.Response:
        return `File "${file.name}" was completely uploaded!`;
      default:
        return `File "${file.name}" surprising upload event: ${event.type}.`;
    }
  }
}
uploader.service.ts

You can send the raw file or wrap it into a FormData before sending it to the server side. It depends on your own implementation or the server-side framework you are working with. However, in most cases encrypting the file with FormData is considered the best practice.

To get the progress report of an HTTP request, you have to enable the flag for reportProgress. An ongoing HTTP request can emit a bunch of events and we can tap into those using the Rxjs tap operator. When the upload is finished the last value (HttpEventType.Response) is emitted to the subscriber (onUpload() function of user.component.ts). The subscriber then shows the value in a presentable way.

To update the progress bar in real time, I took the liberty to declare a BehaviorSubjcet named progressSource. This subject will get a new value every time an HttpEventType.UploadProgress event is emitted. This event contains the loaded and total property which is used in this scenario to find out the percentage of the uploaded file. I subscribed to this source in the ngOnInit hook of the component class.

And that's all about it. Fire up your own server and change the URL in the uploader service to your upload API endpoint.

Repository:

https://github.com/fiyazbinhasan/ng-playground/tree/ng-playground-upload-file

fiyazbinhasan/ng-playground
Contribute to fiyazbinhasan/ng-playground development by creating an account on GitHub.

Links:

https://angular.io/api/common/http/HttpRequest

https://rxjs-dev.firebaseapp.com/api/operators/tap

Using FormData Objects - Web APIs | MDN
The FormData object lets you compile a set of key/value pairs to send using XMLHttpRequest. It is primarily intended for use in sending form data, but can be used independently from forms in order to transmit keyed data. The transmitted data is in the same format that the form’s submit() method woul…