Reputation: 3899
I currently have an ASP.NET Core 2.1 API endpoint that users can hit to download a CSV file:
[HttpPost("Gimme")]
public IActionResult Gimme([FromBody] MyOptions options)
{
MemoryStream reportStream = _reportGenerator.GenerateReportStream(options.StartDate, options.EndDate);
return new FileStreamResult(reportStream, "text/csv") { FileDownloadName = "report.csv" };
}
I can test it via POSTMan and it returns a CSV with the correct data, no problem.
Enter Angular. The web site we're interacting with our API from uses an Angular-Driven Single Page Application. I've searched for various ways on how to handle files coming from API endpoints, and a lot seem to revolve around getting a Blob and creating an inline URL that is then navigated to via window.open()
in the JavaScript. Or creating in-memory a
tags to then call click()
on.
My question: Does the latest Angular really have no way to handle this out of the box? I'm no Angular expert but I would've thought there'd be an example up on their site or some built-in mechanism for serving a downloaded file to the browser. There seems, however, to just be a lot of hackery involved.
My current, WIP solution does return the file to the browser for download, but regardless of the file name specified in the API, it is downloaded as a temporary file name (some GUID-looking thing) instead:
Below is what I have both in my Component and Service classes. I updated the downloadFile()
method to use the answer supplied in this SO answer to get friendly file names working, but I would still rather find a less-hacky solution.
// Component
DownloadReport(options: ReportOptions) {
this._service.getCSV(options).subscribe(data => this.downloadFile(data));
}
downloadFile(blob: Blob) {
const fileName = 'report.csv';
if (navigator.msSaveBlob) {
// IE 10+
navigator.msSaveBlob(blob, fileName);
} else {
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
// Service
getCSV(reportOptions: ReportOptions): Observable<any> {
let headers = new HttpHeaders();
headers = headers.set('Accept', 'text/csv');
return this._httpClient
.post(`${this.apiRoot}/Gimme`, reportOptions, { headers: headers, responseType: 'blob' })
.catch(this.handleError);
}
As you can see, I'm currently implementing the createObjectURL
hack to get this working (a mash of Found On Internet solutions). Is there a better way? A "Best Practices" sort of way?
Upvotes: 1
Views: 2102
Reputation: 7239
This is what I use. It's basically the same thing you are using, but looks a little cleaner to me with the Anchor typing. I researched the right way to do this for a long time and couldn't find a non-hacky way to do it.
download(): void {
this.service.getFile({ id: id, name: name })
.subscribe(data => {
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
//save file for IE
window.navigator.msSaveOrOpenBlob(data, name);
} else {
const objectUrl: string = URL.createObjectURL(data);
const a: HTMLAnchorElement = document.createElement('a') as HTMLAnchorElement;
a.href = objectUrl;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(objectUrl);
}
});
}
Upvotes: 1