50

Actually, I'm working on a Spring REST API with an interface coded in Angular 2.

My problem is I can't upload a file with Angular 2.

My Webresources in java is that :

@RequestMapping(method = RequestMethod.POST, value = "/upload")
public String handleFileUpload(@RequestParam MultipartFile file) {
    //Dosomething 
}

And it is perfectly working when I call it through URL request with Auth header etc ... ( with Advanced Rest Client extension for Chrome )

Proof: (everything works fine in that case )

enter image description here I added the

<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver" />

Spring config file and the Pom dependency

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.2</version>
</dependency>

BUT when I try to do the same thing with a webform :

<input type="file" #files (change)="change(files)"/>
<pre>{{fileContents$|async}}</pre>

With the (change) method :

change(file) {
    let formData = new FormData();
    formData.append("file", file);
    console.log(formData);
    let headers = new Headers({
        'Authorization': 'Bearer ' + this.token,
        'Content-Type': 'multipart/form-data'
    });
    this.http.post(this.url, formData, {headers}).map(res => res.json()).subscribe((data) => console.log(data));
    /*
    Observable.fromPromise(fetch(this.url,
        {method: 'post', body: formData},
        {headers: this.headers}
    )).subscribe(()=>console.log('done'));
    */
}

My web service returns me an error 500, with that in tomcat logs: http://pastebin.com/PGdcFUQb

I tried the 'Content-Type': undefined method too but without success ( the web service return me a 415 error in that case.

Can someone help me to figure out what's the problem is?

Problem solved, I'll update that question later with my code :) but, have a look on the plunker it's working perfectly well. Thanks.

naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
Slater
  • 817
  • 2
  • 10
  • 16

9 Answers9

157

This is actually really easy to do in the final release. Took me a while to wrap my head around it because most information about it that I've come across is outdated. Posting my solution here in case anyone else is struggling with this.

import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Http } from '@angular/http';

@Component({
    selector: 'file-upload',
    template: '<input type="file" [multiple]="multiple" #fileInput>'
})
export class FileUploadComponent {
    @Input() multiple: boolean = false;
    @ViewChild('fileInput') inputEl: ElementRef;

    constructor(private http: Http) {}

    upload() {
        let inputEl: HTMLInputElement = this.inputEl.nativeElement;
        let fileCount: number = inputEl.files.length;
        let formData = new FormData();
        if (fileCount > 0) { // a file was selected
            for (let i = 0; i < fileCount; i++) {
                formData.append('file[]', inputEl.files.item(i));
            }
            this.http
                .post('http://your.upload.url', formData)
                // do whatever you do...
                // subscribe to observable to listen for response
        }
    }
}

Then just use it like so:

<file-upload #fu (change)="fu.upload()" [multiple]="true"></file-upload>

That is really all there is to it.

Alternatively, capture the event object and get the files from the srcElement. Not sure if any way is better than the other, to be honest!

Keep in mind FormData is IE10+, so if you have to support IE9 you'll need a polyfill.

Update 2017-01-07

Updated code to be able to handle uploading of multiple files. Also my original answer was missing a rather crucial bit concerning FormData (since I moved the actual upload logic to a separate service in my own app I was handling it there).

Brother Woodrow
  • 6,092
  • 3
  • 18
  • 20
  • haha nice ! :D but the question is old and was on a beta release of angular 2 – Slater Oct 06 '16 at 14:10
  • 36
    Sure! But this question is the first one that comes up on Google when you search for "angular 2 file upload". Figured I'd add some up to date information. – Brother Woodrow Oct 15 '16 at 22:03
  • Any idea if i can just use a file_uri? – keepwalking Oct 21 '16 at 05:59
  • 2
    What version of RC are you on? Has anyone else had success using this? Doesn't seem to be working for me. – williamsandonz Dec 20 '16 at 21:19
  • You're right this doesn't work for multiple files, but that would be a trivial addition. I'll update my answer shortly. – Brother Woodrow Jan 07 '17 at 13:04
  • Thank you very much indeed, the `FormData` approach does work! btw please know that if you had wanted me to read your comment, you should have tagged me – phil294 Jan 22 '17 at 01:33
  • 1
    Okay I am a little bit confused, the fileupload seems to be working but the weird thing is, that since im running with localhost:3000 the upload cant find the uploadfolder (maybe because of the routes?) and when I just link localhost/folder I get a cors error any ideas on this? – stackg91 Feb 04 '17 at 06:09
  • what a life saver this is, I being banging my head against setting content type and the solution was to not pass it lol – Huangism Mar 08 '17 at 19:19
  • 4
    If anyone else was wondering where FormData came from, its the native FormData object: https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData You dont need to import anything. – Brad Mar 16 '17 at 21:26
  • 2
    This works great! One caveat - make sure not to manually set the Content-Type header. Leaving it unspecified, Angular 2 seems to choose the right value, at least between application/json and multipart/form-data – Rob Gwynn-Jones Apr 05 '17 at 05:13
  • 1
    @Slater this should be the selected answer – Chris Lang Apr 30 '17 at 04:30
  • @Brother Woodrow: Thanks for the answer can you please tell that can I directly send this Formdata in Api request (PHP json), and this be processed. I mean the image will be uploaded from PHP Api whose url we are giving not from Angular side please correct if I am wrong. – Narendra Vyas May 10 '17 at 13:49
  • How can we pass the data to c# controller? What should be the parameter type of the HttpPost method? – Gk_999 Mar 01 '18 at 20:10
  • I was worried about the security risk for using `ElementRef`(see [docs](https://angular.io/api/core/ElementRef)), but according to [this answer](https://stackoverflow.com/a/44509202/1792932), if you're using it just to get information (as in Woodrow's answer), there is no security risk. There is some risk if you use it to modify the DOM. Thought it would be useful to know as I had some reservations before using this code snippet. – Dragos Stanciu Oct 24 '18 at 06:49
27

In fact, at the moment, you can only provide string input for post, put and patch methods of the Angular2 HTTP support.

To support that, you need to leverage the XHR object directly, as described below:

import {Injectable} from 'angular2/core';
import {Observable} from 'rxjs/Rx';

@Injectable()
export class UploadService {
  constructor () {
    this.progress$ = Observable.create(observer => {
      this.progressObserver = observer
    }).share();
  }

  private makeFileRequest (url: string, params: string[], files: File[]): Observable {
    return Observable.create(observer => {
      let formData: FormData = new FormData(),
        xhr: XMLHttpRequest = new XMLHttpRequest();

      for (let i = 0; i < files.length; i++) {
        formData.append("uploads[]", files[i], files[i].name);
      }

      xhr.onreadystatechange = () => {
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            observer.next(JSON.parse(xhr.response));
            observer.complete();
          } else {
            observer.error(xhr.response);
          }
        }
      };

      xhr.upload.onprogress = (event) => {
        this.progress = Math.round(event.loaded / event.total * 100);

        this.progressObserver.next(this.progress);
      };

      xhr.open('POST', url, true);
      xhr.send(formData);
    });
  }
}

See this plunkr for more details: https://plnkr.co/edit/ozZqbxIorjQW15BrDFrg?p=info.

There is a an issue and a pending PR regarding this in the Angular repo:

Jan Nielsen
  • 10,892
  • 14
  • 65
  • 119
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Huuum what is a xhr object ? ( i'm looking on the solution ) And my webstrom don't recognize the "progressObeserver" is it a special class or something ? – Slater Apr 01 '16 at 09:24
  • The XMLHttpRequest object. The object provided by the browser to execute AJAX request. The Angular2 HTTP module relies on it. See https://github.com/angular/angular/blob/master/modules/angular2/src/http/backends/xhr_backend.ts#L33 and https://github.com/angular/angular/blob/master/modules/angular2/src/http/backends/browser_xhr.ts#L11. – Thierry Templier Apr 01 '16 at 09:26
  • xhr.open('POST', url, true); <= is it a way to had my headers ? for Authorization token ? – Slater Apr 01 '16 at 09:42
  • You can use the `setRequestHeader` after having called the `open` method: `_xhr.setRequestHeader(name, value);` – Thierry Templier Apr 01 '16 at 09:45
  • OMFG ! It's working perfectly ! I need to refactor and clean up that mess but it's working ! Thanks ! really ! Can I ask you later some questions on that kind of stuff ? Like : " how can i Handle the response of the xhr.send ? – Slater Apr 01 '16 at 09:54
  • Thank you very much. It works for me. But could you explain how your expected xhr.response looks like. – Johannes Jul 01 '16 at 08:21
  • This works for me too. Can we do it in angular 2 way instead of the above javascript way?? – clint May 11 '17 at 07:14
  • The http client has now been updated to reflect the progress as well. https://angular.io/guide/http#listening-to-progress-events – trungk18 Feb 27 '19 at 04:04
12

This has worked for me:

<input type="file" (change)="onChange($event)" required class="form-control " name="attach_file" id="attach_file">
onChange(event: any) {
    let fileList: FileList = event.target.files;
if(fileList.length > 0) {
    let file: File = fileList[0];
    let formData:FormData = new FormData();
    formData.append('degree_attachment', file, file.name);
    let headers = new Headers();
    headers.append('Accept', 'application/json');
    let options = new RequestOptions({ headers: headers });
    this.http.post('http://url', formData,options)
        .map(res => res.json())
        .catch(error => Observable.throw(error))
        .subscribe(
            data => console.log('success'),
            error => console.log(error)
        )
}}
Anil Samal
  • 1,343
  • 1
  • 13
  • 30
5

This has worked for me: Angular 2 provides good support to upload file:

<input type="file" (change)="fileChange($event)" placeholder="Upload file" accept=".pdf,.doc,.docx">

fileChange(event) {
    let fileList: FileList = event.target.files;
    if(fileList.length > 0) {
        let file: File = fileList[0];
        let formData:FormData = new FormData();
        formData.append('uploadFile', file, file.name);
        let headers = new Headers();
        headers.append('Content-Type', 'multipart/form-data');
        headers.append('Accept', 'application/json');
        let options = new RequestOptions({ headers: headers });
        this.http.post(URL, formData, options)
            .map(res => res.json())
            .catch(error => Observable.throw(error))
            .subscribe(
                data => console.log('success'),
                error => console.log(error)
            )
    }
}

I was getting error : java.io.IOException: RESTEASY007550: Unable to get boundary for multipart

In order to solve this you should remove the "Content-Type" "multipart/form-data"

heman123
  • 3,009
  • 4
  • 24
  • 37
  • the solution is old and the answer is from angular 2 Beta 15 ^^ – Slater Oct 25 '16 at 12:02
  • Oh. I used it and it worked for me ! can you please provide with a link or code for updated file upload. And where should i look for the updated code! @Slater – heman123 Oct 25 '16 at 14:16
  • Huuum idk that was the solution i used and i did not worked on it since that ... srry :/ – Slater Oct 26 '16 at 07:04
  • Try to look on the comments and pieces of code in this question ;) – Slater Oct 26 '16 at 07:05
  • 1
    Just comment headers.append('Content-Type', 'multipart/form-data'); in the code – Anil Samal Dec 07 '16 at 07:27
  • how would you append an array of files? – phil294 Dec 22 '16 at 15:39
  • I also encountered the **Multipart: Boundary not found**, but when I removed the `headers.append('Content-Type', 'multipart/form-data')`, it can **NOT** be processed at all, and another error messages I got some like . **code: LIMIT_UNEXPECTED_FILE,Error: Unexpected field**, BTW, I am using NodeJS to handle the uploading, https://www.thepolyglotdeveloper.com/2016/02/upload-files-to-node-js-using-angular-2/ – Hantsy Mar 27 '17 at 07:04
3

This thread has been so helpful that I felt compelled to share my solution. Brother Woodrow's answer was my starting point. I also wanted to draw attention to Rob Gwynn-Jones' comment "make sure not to manually set the Content-Type header" which is super-important and saved me a ton of time.


This version allows multiple add/remove operations (from different folders), before uploading all files at once.

Multiple files with the same name (from different folders) can be uploaded together, but the same file won't be added to the upload list twice (this is not as trivial as it seems!).

import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Http } from '@angular/http';

@Component({
    selector: 'file-upload',
    template: '<input type="file" [multiple]="multiple" #fileInput>'
})
export class FileUploadComponent {
    @Input() multiple: boolean = false;
    @ViewChild('fileInput') inputEl: ElementRef;

    files: Array<any> = [];
    fileObjects: Array<any> = [];
    fileKeys: Array<string> = [];
    fileCount: number = 0;

    constructor(private http: Http) {}

    addFiles(callback: any) {

        const inputEl: HTMLInputElement = this.inputEl.nativeElement;
        const newCount: number = inputEl.files.length;

        for (let i = 0; i < newCount; i ++) {

            const obj = {
                name: inputEl.files[ i ].name,
                type: inputEl.files[ i ].type,
                size: inputEl.files[ i ].size,
                ts: inputEl.files[ i ].lastModifiedDate
            };

            const key = JSON.stringify(obj);

            if ( ! this.fileKeys.includes(key)) {

                this.files.push(inputEl.files.item(i));
                this.fileObjects.push(obj);
                this.fileKeys.push(key);
                this.fileCount ++;
            }
        }

        callback(this.files);
    }

    removeFile(obj: any) {

        const key: string = JSON.stringify(obj);

        for (let i = 0; i < this.fileCount; i ++) {

            if (this.fileKeys[ i ] === key) {

                this.files.splice(i, 1);
                this.fileObjects.splice(i, 1);
                this.fileKeys.splice(i, 1);
                this.fileCount --;

                return;
            }
        }
    }
}

The callback in 'addFiles' allows the upload to happen outside the component. Component is used like this:

<file-upload #fu (change)="fu.addFiles(setFiles.bind(this))" [multiple]="true"></file-upload>

'setFiles' is the callback. 'this' in this context is the parent component:

    setFiles(files: Array<any>) { this.files = files; }

All that remains is to attach the multipart payload before calling the upload API (also in the parent component):

const formData = new FormData();
            
for (let i = 0; i < this.files.length; i ++) {

    formData.append('file[]', this.files[ i ]);
}

Hope this is helpful, and happy to fix/update if necessary. Cheers!

vanwinter
  • 153
  • 1
  • 8
2
this.uploader.onBeforeUploadItem = function(item) {
  item.url = URL.replace('?', "?param1=value1");
}
msanford
  • 11,803
  • 11
  • 66
  • 93
1

If your looking for a simple solution and don't want to do the coding yourself, I would recommend using this library:

https://www.npmjs.com/package/angular2-http-file-upload

gnast
  • 21
  • 1
0
fileUpload() {
  const formData = new FormData();

  const files = this.filesToUpload;
  for (let i = 0; i < files.length; i++) {
    formData.append('file', files.item(i));
    formData.append('Content-Type', 'application/json');
    formData.append('Accept', `application/json`);
  }


  this.http.post('http://localhost:8080/UploadFile', formData).subscribe(response => console.log(response));
}

Then:

<form (ngSubmit)="upload()">
    <input type="file" id="file" multiple (change)="fileUpload($event.target.files)">
    <button type="submit">Upload</button>
</form>
Venantius
  • 2,471
  • 2
  • 28
  • 36
0

I was just removed content-type from the header. for example this is our header:

 let headers = new Headers({
        'Authorization': 'Bearer ' + this.token,
        'Content-Type': 'multipart/form-data'
});

What you have to do is to just remove Content-Type from this. Like:

 let headers = new Headers({
        'Authorization': 'Bearer ' + this.token,
    });
Hammad Ahmad
  • 184
  • 1
  • 6