27

I'm learning Angular 2 Beta. I wonder how to download the PDF file from the API and display it in my view? I've tried to make a request using the following:

    var headers = new Headers();
    headers.append('Accept', 'application/pdf');
    var options = new ResponseOptions({
        headers: headers
    });
    var response = new Response(options);
    this.http.get(this.setUrl(endpoint), response).map(res => res.arrayBuffer()).subscribe(r=>{
       console.log(r);
    })
  • Please note that I only use the console.log to see the value of r

But I always get the following exception message:

"arrayBuffer()" method not implemented on Response superclass

Is it because that method isn't ready yet in Angular 2 Beta? Or is there any mistake that I made?

Any help would be appreciated. Thank you very much.

Mark Pieszak - Trilon.io
  • 61,391
  • 14
  • 82
  • 96
asubanovsky
  • 1,608
  • 3
  • 19
  • 36

10 Answers10

18

In fact, this feature isn't implemented yet in the HTTP support.

As a workaround, you need to extend the BrowserXhr class of Angular2 as described below to set the responseType to blob on the underlying xhr object:

import {Injectable} from 'angular2/core';
import {BrowserXhr} from 'angular2/http';

@Injectable()
export class CustomBrowserXhr extends BrowserXhr {
  constructor() {}
  build(): any {
    let xhr = super.build();
    xhr.responseType = "blob";
    return <any>(xhr);
  }
}

Then you need to wrap the response payload into a Blob object and use the FileSaver library to open the download dialog:

downloadFile() {
  this.http.get(
    'https://mapapi.apispark.net/v1/images/Granizo.pdf').subscribe(
      (response) => {
        var mediaType = 'application/pdf';
        var blob = new Blob([response._body], {type: mediaType});
        var filename = 'test.pdf';
        saveAs(blob, filename);
      });
}

The FileSaver library must be included into your HTML file:

<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2014-11-29/FileSaver.min.js"></script>

See this plunkr: http://plnkr.co/edit/tfpS9k2YOO1bMgXBky5Y?p=preview

Unfortunately this will set the responseType for all AJAX requests. To be able to set the value of this property, there are more updates to do in the XHRConnection and Http classes.

As references see these links:

Edit

After thinking a bit more, I think that you could leverage hierarchical injectors and configure this provider only at the level of the component that executes the download:

@Component({
  selector: 'download',
  template: '<div (click)="downloadFile() ">Download</div>'
  , providers: [
    provide(CustomBrowserXhr, 
      { useClass: CustomBrowserXhr }
  ]
})
export class DownloadComponent {
  @Input()
  filename:string;

  constructor(private http:Http) {
  }

  downloadFile() {
    this.http.get(
      'https://mapapi.apispark.net/v1/images/'+this.filename).subscribe(
        (response) => {
          var mediaType = 'application/pdf';
          var blob = new Blob([response._body], {type: mediaType});
          var filename = 'test.pdf';
          saveAs(blob, filename);
        });
    }
}

This override would only applies for this component (don't forget to remove the corresponding provide when bootstrapping your application). The download component could be used like that:

@Component({
  selector: 'somecomponent',
  template: `
    <download filename="'Granizo.pdf'"></download>
  `
  , directives: [ DownloadComponent ]
})
SteamFire
  • 367
  • 2
  • 14
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Hi, Thierry. Thanks for the answer. Your workaround works. But unfortunately it impacts all AJAX requests in my application. Is there any trick to make this working only for PDF request (not JSON request)? – asubanovsky Feb 13 '16 at 02:35
  • You're welcome! I updated my answer with a potential solution for this. This should work like this ;-) – Thierry Templier Feb 13 '16 at 08:11
  • Thanks again. One more question: is there any way to display PDF instead of force download? – asubanovsky Feb 13 '16 at 16:48
  • @asubanovsky how about: window.open(window.URL.createObjectURL(result)); – Spock Mar 28 '16 at 21:26
  • 1
    Hi @ThierryTemplier, I'm getting "Constructors for derived classes must contain a 'super' call." error when trying to use the custom Xhr class... any idea? And thank you for a great solution – Spock Mar 29 '16 at 08:54
  • Hey @Spock. What is the content of your constructor method? – Thierry Templier Mar 29 '16 at 08:56
  • 1
    @ThierryTemplier hi, thanks for replying.. actually it's empty but the error thrown but the compiler - the code gets executed anyway.. I*m using Typescript 1.8 btw.. I'm having more trouble getting blobs to be read, because response._body is still private when I run the code.. I'm using the same code as you wrote here.. .super weird – Spock Mar 29 '16 at 13:21
  • 2
    Hi again - sorry for spamming you all - just an update. I couldn't get past the "_body is private property" problem, so I resolved to a raw XMLHttpRequest instantiation instead. Not the most elegant solution, but it works. ... argggh another day in a programmer's miserable life :). .thanks for the help @ThierryTemplier – Spock Mar 29 '16 at 21:03
  • @Spock Hi! Would you mind elaborating on how to were able to work around this? I've the provided answer but am running into the same compiler errors "_body is private property" and "Constructors for derived classes must contain a 'super' call.". Any help would be greatly appreciated. – E. Allison May 04 '16 at 13:25
  • Running into the same error `error TS2341: Property '_body' is private and only accessible within class 'Response'. error TS2304: Cannot find name 'saveAs'.` – user2180794 Jun 16 '16 at 01:18
  • I can't get this to work on `@Component` level. If I add the `provide()` to the bootstrap, it uses the appropriate `CustomBrowserXhr` component, but to all requests, as the original post stated. If I add it to the `@Component`, a plain XHR will be used. Can anyone show me a working example where not all requests are made through this custom XHR class? – Sleeper9 Jun 16 '16 at 08:32
  • This didn't work for me, but this did : http://stackoverflow.com/questions/39049260/how-do-you-download-a-pdf-from-an-api-in-angular2-rc5 – Hari Oct 20 '16 at 02:25
  • I have posted solution here which works without using any other NPM packages. Here is the link - https://stackoverflow.com/a/48467727/3926504 – Dilip Nannaware Jan 26 '18 at 18:57
17

So here is how I managed to get it to work. My situation: I needed to download a PDF from my API endpoint, and save the result as a PDF in the browser.

To support file-saving in all browsers, I used the FileSaver.js module.

I created a component that takes the ID of the file to download as parameter. The component, , is called like this:

<pdf-downloader no="24234232"></pdf-downloader>

The component itself uses XHR to fetch/save the file with the number given in the no parameter. This way we can circumvent the fact that the Angular2 http module doesn't yet support binary result types.

And now, without further ado, the component code:

    import {Component,Input } from 'angular2/core';
    import {BrowserXhr} from 'angular2/http';

    // Use Filesaver.js to save binary to file
    // https://github.com/eligrey/FileSaver.js/
    let fileSaver = require('filesaver.js');


    @Component({
      selector: 'pdf-downloader',
      template: `
        <button
           class="btn btn-secondary-outline btn-sm "
          (click)="download()">
            <span class="fa fa-download" *ngIf="!pending"></span>
            <span class="fa fa-refresh fa-spin" *ngIf="pending"></span>
        </button>
        `
   })

   export class PdfDownloader  {

       @Input() no: any;

       public pending:boolean = false;

       constructor() {}

       public download() {

        // Xhr creates new context so we need to create reference to this
        let self = this;

        // Status flag used in the template.
        this.pending = true;

        // Create the Xhr request object
        let xhr = new XMLHttpRequest();
        let url =  `/api/pdf/iticket/${this.no}?lang=en`;
        xhr.open('GET', url, true);
        xhr.responseType = 'blob';

        // Xhr callback when we get a result back
        // We are not using arrow function because we need the 'this' context
        xhr.onreadystatechange = function() {

            // We use setTimeout to trigger change detection in Zones
            setTimeout( () => { self.pending = false; }, 0);

            // If we get an HTTP status OK (200), save the file using fileSaver
            if(xhr.readyState === 4 && xhr.status === 200) {
                var blob = new Blob([this.response], {type: 'application/pdf'});
                fileSaver.saveAs(blob, 'Report.pdf');
            }
        };

        // Start the Ajax request
        xhr.send();
    }
}

I've used Font Awesome for the fonts used in the template. I wanted the component to display a download button and a spinner while the pdf is fetched.

Also, notice I could use require to fetch the fileSaver.js module. This is because I'm using WebPack so I can require/import like I want. Your syntax might be different depending of your build tool.

Spock
  • 2,482
  • 29
  • 27
  • Any workaround with angular-cli? Because it's not easy to use require/import (since you're using FileSaver.js module). I even had to request the specific feature for the 3rd party plugin (Angular2 Datepicker) to make it works with angular-cli. – asubanovsky May 07 '16 at 03:43
  • I'm not sure how angular-cli works regarding 3rd party imports. As far as I know it uses systemJs, and I know from manually playing with that, that it should be possible to configure it to import external dependencies. But I'm not sure how angular-cli does it... I found this conversation (although pretty old) about how to manually add libraries using ng2-cli, but haven't tried myself: https://github.com/angular/angular-cli/issues/274 – Spock May 08 '16 at 12:52
  • How did you import Filesaver.js? I am having issues http://stackoverflow.com/questions/37852166/angular2-importing-3rd-party-javascript-in-systemjs – user2180794 Jun 18 '16 at 06:48
  • Hey sorry man, I didn't see your comment until now.. let fileSaver = require('filesaver.js'); (outside my component)... then to use it in the component: fileSaver.saveAs(blob, fileTitle); – Spock Sep 09 '16 at 12:07
7

I don't think all of these hacks are necessary. I just did a quick test with the standard http service in angular 2.0, and it worked as expected.

/* generic download mechanism */
public download(url: string, data: Object = null): Observable<Response> {

    //if custom headers are required, add them here
    let headers = new Headers();        

    //add search parameters, if any
    let params = new URLSearchParams();
    if (data) {
        for (let key in data) {
            params.set(key, data[key]);
        }
    }

    //create an instance of requestOptions 
    let requestOptions = new RequestOptions({
        headers: headers,
        search: params
    });

    //any other requestOptions
    requestOptions.method = RequestMethod.Get;
    requestOptions.url = url;
    requestOptions.responseType = ResponseContentType.Blob;

    //create a generic request object with the above requestOptions
    let request = new Request(requestOptions);

    //get the file
    return this.http.request(request)
        .catch(err => {
            /* handle errors */
        });      
}


/* downloads a csv report file generated on the server based on search criteria specified. Save using fileSaver.js. */
downloadSomethingSpecifc(searchCriteria: SearchCriteria): void {

    download(this.url, searchCriteria) 
        .subscribe(
            response => {                                
                let file = response.blob();
                console.log(file.size + " bytes file downloaded. File type: ", file.type);                
                saveAs(file, 'myCSV_Report.csv');
            },
            error => { /* handle errors */ }
        );
}
Indev
  • 81
  • 1
  • 5
  • This should be the answer -- it allowed me to use authorization on the get without having to extend Xhr and worked just as well as the top answer. – AndrewBenjamin Jan 27 '17 at 03:26
  • 2
    What is this 'saveAs' function here? If it is https://github.com/eligrey/FileSaver.js/ then I think it would be better to include an import to be clear about it – Tarmo Aug 21 '17 at 09:08
  • 1
    Can you include the imports that you've used? – chris31389 Sep 21 '17 at 09:35
6

Here is the simplest way to download a file from an API that I was able to come up with.

import { Injectable } from '@angular/core';
import { Http, ResponseContentType } from "@angular/http";

import * as FileSaver from 'file-saver';

@Injectable()
export class FileDownloadService {


    constructor(private http: Http) { }

    downloadFile(api: string, fileName: string) {
        this.http.get(api, { responseType: 'blob' })
            .subscribe((file: Blob) => {
               FileSaver.saveAs(file, fileName);
        });    
    }

}

Call the downloadFile(api,fileName) method from your component class.

To get FileSaver run the following commands in your terminal

npm install file-saver --save
npm install @types/file-saver --save
Olivier Tonglet
  • 3,312
  • 24
  • 40
Shashank Shekhar
  • 3,958
  • 2
  • 40
  • 52
  • 1
    Hello, I get a syntax error: Here is the error severity: 'Error' message: 'Argument of type '{ responseType: ResponseContentType; }' is not assignable to parameter of type '{ headers?: HttpHeaders | { [header: string]: string | string[]; }; observe?: "body"; params?: Ht...'. Types of property 'responseType' are incompatible. Type 'ResponseContentType' is not assignable to type '"json"'.' at: '41,28' source: 'ts' code: '2345' – Ben Donnelly Feb 22 '18 at 12:37
  • @BenDonnelly It should be this.http.get(api, { responseType: 'blob' }) .subscribe((file : Blob) => { FileSaver.saveAs(file, fileName); }); – Olivier Tonglet Jun 01 '18 at 14:29
4

Hello, here is a working example. It is also suitable for PDF! application/octet-stream - general type. Controller:

public FileResult exportExcelTest()
{ 
    var contentType = "application/octet-stream";
    HttpContext.Response.ContentType = contentType;

    RealisationsReportExcell reportExcell = new RealisationsReportExcell();     
    byte[] filedata = reportExcell.RunSample1();

    FileContentResult result = new FileContentResult(filedata, contentType)
    {
        FileDownloadName = "report.xlsx"
    };
    return result;
}

Angular2:

Service xhr:

import { Injectable } from '@angular/core';
import { BrowserXhr } from '@angular/http';

@Injectable()
export class CustomBrowserXhr extends BrowserXhr {
  constructor() {
      super();
  }

  public build(): any {
      let xhr = super.build();
      xhr.responseType = "blob";
      return <any>(xhr);
  }   
}

Install file-saver npm packages "file-saver": "^1.3.3", "@types/file-saver": "0.0.0" and include in vendor.ts import 'file-saver';

Component btn download.

import { Component, OnInit, Input } from "@angular/core";
import { Http, ResponseContentType } from '@angular/http';
import { CustomBrowserXhr } from '../services/customBrowserXhr.service';
import * as FileSaver from 'file-saver';

@Component({
    selector: 'download-btn',
    template: '<button type="button" (click)="downloadFile()">Download</button>',
    providers: [
        { provide: CustomBrowserXhr, useClass: CustomBrowserXhr }
    ]
})

export class DownloadComponent {        
    @Input() api: string; 

    constructor(private http: Http) {
    }

    public downloadFile() {
        return this.http.get(this.api, { responseType: ResponseContentType.Blob })
        .subscribe(
            (res: any) =>
            {
                let blob = res.blob();
                let filename = 'report.xlsx';
                FileSaver.saveAs(blob, filename);
            }
        );
    }
}

Using

<download-btn api="api/realisations/realisationsExcel"></download-btn>
dev-siberia
  • 2,746
  • 2
  • 21
  • 17
2

Here is the code that works for downloadign the API respone in IE and chrome/safari. Here response variable is API response.

Note: http call from client needs to support blob response.

    let blob = new Blob([response], {type: 'application/pdf'});
    let fileUrl = window.URL.createObjectURL(blob);
    if (window.navigator.msSaveOrOpenBlob) {
        window.navigator.msSaveOrOpenBlob(blob, fileUrl.split(':')[1] + '.pdf');
    } else {
        window.open(fileUrl);
    }
Dilip Nannaware
  • 1,410
  • 1
  • 16
  • 24
2

To get Filesaver working in Angular 5: Install

npm install file-saver --save
npm install @types/file-saver --save

In your component use import * as FileSaver from "file-saver";

and use FileSaver.default and not FileSaver.SaveAs

.subscribe(data => {
            const blob = data.data;
            const filename = "filename.txt";
            FileSaver.default(blob, filename);
Thom Kiesewetter
  • 6,703
  • 3
  • 28
  • 41
0

Working solution with C# Web API loading PDF as a byte array:

C# loads PDF as a byte array and converts to Base64 encoded string

public HttpResponseMessage GetPdf(Guid id)
{
    byte[] file = GetFile(id);
    HttpResponseMessage result = Request.CreateResponse(HttpStatusCode.OK);
    result.Content = new StringContent("data:application/pdf;base64," + Convert.ToBase64String(file));
    return result;
}

Angular service gets PDF

getPdf(): Observable<string> {
    return this.http.get(webApiRequest).pipe(
        map(response => {
            var anonymous = <any>response;
            return anonymous._body;
        })
    );
}

Component view embeds the PDF via binding to service response

The pdfSource variable below is the returned value from the service.

<embed [src]="sanitizer.bypassSecurityTrustResourceUrl(pdfSource)" type="application/pdf" width="100%" height="300px" />

See the Angular DomSanitizer docs for more info.

ElliotSchmelliot
  • 7,322
  • 4
  • 41
  • 64
0
http
  .post(url, data, {
    responseType: "blob",
    observe: "response"
  })
  .pipe(
    map(response => {
      saveAs(response.body, "fileName.pdf");
    })
  );
Eylon Sultan
  • 936
  • 9
  • 16
  • 1
    Please add some description so that the original poster can learn from you. – Bob Dalgleish Apr 17 '19 at 14:12
  • 1
    While this code may solve the question, [including an explanation](https://meta.stackexchange.com/questions/114762/explaining-entirely-code-based-answers) of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please edit your answer to add explanations and give an indication of what limitations and assumptions apply. – Busti Apr 17 '19 at 15:03
0

Extending what @ThierryTemplier did (the accepted answer) for Angular 8.

HTML:

<button mat-raised-button color="accent" (click)="downloadFile()">Download</button>

TypeScript:

downloadFile() {
  this.http.get(
    'http://localhost:4200/assets/other/' + this.fileName, {responseType: 'blob'})
    .pipe(tap( // Log the result or error
      data => console.log(this.fileName, data),
      error => console.log(this.fileName, error)
    )).subscribe(results => {
    saveAs(results, this.fileName);
  });
}

Sources:

Benehiko
  • 462
  • 3
  • 6