ben tedder : code things

Upload images in Angular 4 without a plugin

2017-10-03 Edit to save you time

Let me save you some time, you're probably here because the only thing you really need to do is delete the Content-Type header that you're sending when you're trying to send image data. In order to send FormData, just remove that header and things should send normally. If you're not here for that reason, then carry on reading!

Original post

It was really hard to title this post because so much ground needs to be covered. Here was my set of problems:

  • Using Rails 5
  • Using Devise, Angular2-Token, and Devise Token Auth
  • Using Angular 4
  • Needing to keep a user logged in while they were uploading a file to a page
  • Being prohibited from using many plugins because of their interopability with Angular2-Token

I battled with this for probably...5 hours? I can only hope I cut your time in half!

Step 1: Build basic HTML

Do things in a simple way. This escaped me for a while because I thought I needed/wanted plugins to do stuff for me. To keep things simple, cut out the middle man and get a plain 'ol File input to work. Tart it up later.

<input type="file" #fileInput placeholder="Upload file..." />
<button type="button" (click)="upload()">Upload</button>

I've named the input with the reference #fileInput for later use. And the upload button merely uploads when you click it. This is fancy.

Step 2: Setup your basic component

To get started with this component I'm going to assume you've imported angular2-token and have a working setup with other endpoints in your app. If not, maybe I'll write up a post on getting and staying authenticated with Angular2-Token and Devise with Rails.

So for your component, register the ViewChild #fileInput, create an upload method, and inject your token service into your constructor.

The upload method will target the nativeElement, grab its file, append it to the empty FormData object, and then send it off to my projectService upload method (in the next step).

...
import { Angular2TokenService } from 'angular2-token';
...

export class ProjectComponent {
  @ViewChild('fileInput') fileInput;

  constructor(private tokenService: Angular2TokenService) {}
  
  upload() {
    let fileBrowser = this.fileInput.nativeElement;
    if (fileBrowser.files && fileBrowser.files[0]) {
      const formData = new FormData();
      formData.append("image", fileBrowser.files[0]);
      this.projectService.upload(formData, this.project.id).subscribe(res => {
        // do stuff w/my uploaded file
      });
    }
  }
}

Step 3: Setup Angular2-Token's headers and options for its .request method wrapper

I'm also going to assume you already have a service, some way to set your base url, and can create a new method on said service to hit from your component (in step 4). Note a few things here:

  • using the .request() method instead of .post
  • sending formData straight into the body
  • customizing header items and constructing a new RequestOptions object
  • In order to send formData with image content you must remove the Content-Type header.

That last point is very, very important. This is really what kept me screwed for a while. I kept trying to just set it to an empty string, but this just added a comma before the multipart/form-data..... stuff. The Content-Type must be deleted from the headers so it can be added in automatically once you post this formData.

The other critical thing that blocked me was not actually sending in the auth headers that came from Angular2-Token. This meant that almost every time I submitted a file upload it wouldn't work because I'd been logged out. Talk about a frustrating couple of hours! This is due to the request/response handshake that goes on between the token service in Angular and the Devise Token Auth gem.

@Injectable()
export class ProjectService {
  upload(formData, options, id) {
    let headers = this.tokenService.currentAuthHeaders;
    headers.delete('Content-Type');
    let options = new RequestOptions({ headers: headers });

    return this.tokenService.request({
      method: 'post',
      url: `http://localhost:3000/api/projects/${id}/upload`,
      body: formData,
      headers: options.headers
    }).map(res => res.json());
  }
}

What I learned

This blog post was way easier to write than I wish it was. I hate that I wasted hours on this problem. I tried 3 different plugins, was considering forking angular2-token, was going to attempt a wrapper around angular2-token, thought about giving up entirely and going into farming...it was a rough project.

But I prevailed! And it was merely a matter of these few points:

  • Understanding that the headers with angular2-token need to be sent and received to stay authenticated
  • Realizing I should delete the Content-Type and construct a new RequestOptions object to attach it to
  • Sticking with the FormData route and a simple html solution for now
  • Pushing through and not giving up. Well, or giving up 3 or 4 times but still realizing the problem isn't going to go away, and people have been uploading files to the internet since the dawn of AOL, and it shouldn't be that hard of a problem, and what am I doing with my life.