How To Use FormData() For Uploading File In Vue - vue.js

So I had asked a question previously, and got a little bit of help as far as logging the results however my results are not making sense.
So I have a input
<input type="file" name="import_file" v-on:change="selectedFile($event)">
The v-on:change binds the selected file to my data object this.file
selectedFile(event) {
this.file = event.target.files[0]
},
and then I submit the file with this method
uploadTodos() {
let formData = new FormData();
formData.append('file', this.file);
for(var pair of formData.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
this.$store.dispatch('uploadTodos', formData);
}
However when I submit it seems there is no data attached to formData because my logged result is this
file, [object File]
shouldn't I have my actual data appended to the formData object??
I have referenced other articles on how to post but I am not getting the desired results.
article 1
article2
uploadTodos(context, file) {
console.log(file)
axios.post('/import', file,{ headers: {
'Content-Type': 'multipart/form-data'
}})
.then(response => {
console.log(response.data)
context.commit('importTodos', response.data)
})
.catch(error => {
console.log(error.response.data)
})
}
when I console.log(file) the formData object is empty
Backend Question
So my issue with Laravel on the backend is with the maatwebsite package. From what I have seen is the 3.0 version does not yet support imports. And the only work around suggested is to install version 2.0? Is this still the only workaround? Here is the controller method
public function importExcel(Request $request)
{
if (empty($request->file('file')->getRealPath())) {
return back()->with('success','No file selected');
}
else {
$path = $request->file('file')->getRealPath();
$inserts = [];
Excel::load($path,function($reader) use (&$inserts)
{
foreach ($reader->toArray() as $rows){
foreach($rows as $row){
$inserts[] = ['user_id' => $row['user_id'], 'todo' => $row['todo']];
};
}
});
if (!empty($inserts)) {
DB::table('todos')->insert($inserts);
return back()->with('success','Inserted Record successfully');
}
return back();
}
}
The line not suppported by version 3.0 is this
Excel::load($path,function($reader) use (&$inserts)

I have reproduced your code and it seems to be working fine
when I console.log(file) the formData object is empty
Yeah the output should be an empty object when you console, that's the way javascript works.
after casting the output to an array i get the output in the image below:
const store = new Vuex.Store({
actions: {
uploadTodos(context, file) {
console.log([...file])
axios.post('/import', file,{ headers: {
'Content-Type': 'multipart/form-data'
}})
.then(response => {
console.log(response.data)
context.commit('importTodos', response.data)
})
.catch(error => {
console.log(error.response.data)
})
}
}
})
const app = new Vue({
store,
data: {
file: null
},
methods: {
selectedFile(event) {
console.log(event);
this.file = event.target.files[0]
},
uploadTodos() {
let formData = new FormData();
formData.append('file', this.file);
for(var pair of formData.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
this.$store.dispatch('uploadTodos', formData);
}
},
el: '#app'
})
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="app">
<input type="file" name="import_file" #change="selectedFile($event)">
<button #click="uploadTodos">
Submit
</button>
</div>

This post answers the second part of the question. At first from what I read maatwebsite/excel version 3.0 does not support import. However I am using version 3.1.0 which does support imports. However the method for importing still does not suppport Excel::load(). You should instead use Excel::import() and follow the given rules for passing in parameters. Which of course can be modified to suit your needs. But anyways here is a simple example of how I am using it for anyone interested.
First create import file for whatever model it is. For me it is Todos.
<?php
namespace App\Imports;
use App\Todo;
use Maatwebsite\Excel\Concerns\ToModel;
class TodoImport implements ToModel
{
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new Todo([
'user_id' => $row[0],
'todo' => $row[1],
]);
}
}
next you have your controller handling the file, and passing it to the todosimport file
public function importExcel(Request $request)
{
if (empty($request->file('file')->getRealPath())) {
return back()->with('success','No file selected');
}
else {
Excel::import(new TodoImport, $request->file('file'));
return response('Import Succesful, Please Refresh Page');
}
}
notice the Excel::import(). I pass in the new Todo model and the file received.
of course for me since I am doing it by ajax I use this route to ping the method
Route::post('/import', 'TodosController#importExcel');

Related

Vuejs, components and cache

i was looking for a way to 'cache' some API fetched data in my application, and find out it can simply be done with localStorage (or even sessionsStorage)
While it works fine, i'm looking for a way to have all the cached data in one localStorage entry (in JSON)
The problem is i can't figure out how to build the JSON tree without overriding previous item. Let me explain :
On my dashboard, I have two components, one that load categories, the other loading last articles. Each component uses a different endpoint.
this is how i set cache (for categories) :
methods: {
async getCategories() {
let response = await fetch('https://carreblanc.zendesk.com/api/v2/help_center/fr/categories.json', {
method: 'GET',
headers: zendeskHeaders,
})
if (response.status === 200) {
let data = await response.json();
this.categories = data.categories
// Build cache
let cacheData = []
cacheData.push(data)
sessionStorage.setItem('zhc_db_cat_cache', JSON.stringify(cacheData))
}
else {
console.log("Something went wrong (" + response.status + ")");
return Promise.reject(response);
}
},
setUrl(url) {
return decodeURI(url.substring(url.indexOf('-') + 1))
}
},
for articles :
methods: {
async getArticles() {
let response = await fetch('https://carreblanc.zendesk.com/api/v2/help_center/fr/articles.json', {
method: 'GET',
headers: zendeskHeaders,
})
if (response.status === 200) {
let data = await response.json();
this.articles = data.articles
// Build cache
let cacheData = []
cacheData.push(data)
sessionStorage.setItem('zhc_db_art_cache', JSON.stringify(cacheData));
}
else {
console.log("Something went wrong (" + response.status + ")");
return Promise.reject(response);
}
},
setUrl(url) {
return decodeURI(url.substring(url.indexOf('-') + 1))
}
},
this results in two entries in the sessionStorage :
I want only one item, let's say 'zhc_cache', in which i can update data while components load..
for example : dashboard: {
categories: {
......
},
articles: {
......
}
}
I think there's something going on with the async function, but i can't figure it out,
any help will be appreciated :)
Thanks
If i understand it correctly, you just want one JSON object with categories AND data?
In that case, i think you should create an object ("zn_cache"), in which you add categories and articles
//If you want to keep both functions, just set vuejs data variables (categories and articles) and affect them the data you fetched from the api
this.categories = data.categories
this.articles = data.articles
Then you can create a global object with both (check the syntax I'm not 100% sure)
let dashboard = { 'categories' : this.categories, 'articles' : this.articles}
Then you can store this bad boy
#antoinehenrio
works like a charm ;)
here is the dashboard now
const zendeskConfig = {
categoriesEndpoint: 'https://carreblanc.zendesk.com/api/v2/help_center/fr/categories.json',
articlesEndpoint: 'https://carreblanc.zendesk.com/api/v2/help_center/fr/articles.json'
};
import Categories from '/skin/frontend/carreblanc/revamp/js/zendesk/hc/components/dashboard/categories.js'
import Recents from '/skin/frontend/carreblanc/revamp/js/zendesk/hc/components/dashboard/recent-activity.js'
export default {
components: {
Categories,
Recents
},
data() {
return {
categories: [],
articles: [],
}
},
methods: {
getData() {
Promise.all([
fetch(zendeskConfig.categoriesEndpoint).then(res => res.ok && res.json() || Promise.reject(res)),
fetch(zendeskConfig.articlesEndpoint).then(res => res.ok && res.json() || Promise.reject(res))
]).then(([resCategories, resArticles]) => {
this.categories = resCategories.categories
this.articles = resArticles.articles
let cacheData = { dashboard: { 'categories' : this.categories, 'articles' : this.articles} }
sessionStorage.setItem('zhc_db_cache', JSON.stringify(cacheData))
})
}
},
created() {
this.getData()
},
template: `
<div class="row">
<div class="col col-12">
<Categories :categories=this.categories />
</div>
<div class="col col-12">
<Recents :articles=this.articles />
</div>
</div>
`
}
and the result :
I have to reconfigure the other views, but it should be easy now
Thanks again !

Method Not Allowed (PUT) while using Django Rest paired with Vue.js frontend

I'm trying to make a PUT request to my RestAPI using Vue.js. It totally works in my ModelViewSet view, but i encounter Method Not Allowed (PUT) when i try it through Vue.js. I'm also able to use "POST" in the same onSubmit function.
Here is the code for onSubmit function in my QuestionEditor.vue:
<template>
<div class="container mt-2">
<h1 class="mb-3">Ask a Question</h1>
<form #submit.prevent="onSubmit">
<textarea v-model="question_body" class="form-control" placeholder="What do you want to ask?" row="3">
</textarea>
<br>
<button type="submit" class="btn btn-success">
Publish
</button>
</form>
<p v-if="error" class="muted mt-2">{{ error }}</p>
</div>
</template>
<script>
import { apiService } from "../common/api.service.js"
export default {
name: "QuestionEditor",
props: {
slug: {
type: String,
required: null
}
},
data() {
return{
question_body: null,
error: null
}
},
methods: {
onSubmit() {
if (!this.question_body){
this.error = "You can't send an empty question!";
} else if (this.question_body.length > 240) {
this.error = "Exceeding 240-character limit!";
} else {
let endpoint = "/api/questions/";
let method = "POST";
if (this.slug !== undefined) {
endpoint += `${ this.slug }/`;
method = "PUT";
}
apiService(endpoint, method, { content: this.question_body })
.then(question_data => {
this.$router.push({
name: 'Question',
params: {slug: question_data.slug}
})
})
}
}
},
async beforeRouteEnter(to, from, next){
if (to.params.slug !== undefined) {
let endpoint = `/api/questions/${to.params.slug}/`;
let data = await apiService(endpoint);
return next(vm => (vm.question_body = data.content));
} else{
return next();
}
},
created() {
document.title = "Editor - QuestionTime";
}
}
</script>
<style scoped>
</style>
and here is apiService function used to retrieve data from API:
import { CSRF_TOKEN } from "./csrf_token.js"
async function getJson(response) {
if (response.status === 204) return '';
return response.json()
}
function apiService(endpoint, method, data) {
const config = {
method: method || "GET",
body: data !== undefined ? JSON.stringify(data) : null,
headers: {
'content-type': 'application/json',
'X-CSRFTOKEN': CSRF_TOKEN
}
};
return fetch(endpoint, config)
.then(getJson)
.catch(error => console.log(error))
}
export { apiService };
and finally the api view:
class QuestionViewSet(viewsets.ModelViewSet):
queryset = Question.objects.all().order_by("-created_at")
lookup_field = "slug"
serializer_class = QuestionSerializer
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
update: question serializer:
class QuestionSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
created_at = serializers.SerializerMethodField(read_only=True)
slug = serializers.SlugField(read_only=True)
answers_count = serializers.SerializerMethodField()
user_has_answered = serializers.SerializerMethodField()
class Meta:
model = Question
exclude = ["updated_at"]
def get_created_at(self, instance):
return instance.created_at.strftime("%B %d %Y")
def get_answers_count(self, instance):
return instance.answers.count()
def get_user_has_answered(self, instance):
request = self.context.get("request")
return instance.answers.filter(author=request.user).exists()
and url.py for api:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import QuestionViewSet, AnswerCreateAPIView, AnswerListAPIView, AnswerRUDAPIView, AnswerLikeAPIView
router = DefaultRouter()
router.register(r"questions", QuestionViewSet)
urlpatterns = [
path("", include(router.urls)),
path("questions/<slug:slug>/answer/", AnswerCreateAPIView.as_view(), name="answer-create"),
path("questions/<slug:slug>/answers/", AnswerListAPIView.as_view(), name="answer-list"),
path("answers/<int:pk>/", AnswerRUDAPIView.as_view(), name="answer-detail"),
path("answers/<int:pk>/like/", AnswerLikeAPIView.as_view(), name="answer-like"),
]
Update2: permissions file:
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.
Solved!!! I downgraded from web-pack version 1 alpha to 0.4.3 and now everything seems to work fine. It was my mistake to go with a semi-developed version.
I think I might now what happens.
You either need to add pk field in your serializer (fields in Meta has to have id field)
or your endpoint is wrong.
} else {
let endpoint = "api/questions/";
let method = "POST";
if (this.slug !== undefined) {
endpoint += `${ this.slug }/`;
method = "PUT";
}
apiService(endpoint, method, { content: this.question_body })
.then(question_data => {
this.$router.push({
name: 'Question',
params: {slug: question_data.slug}
})
})
If all you do in here is changing the method, then it's not enough, as you POST to /resource but PUT to /resource/<id>.
Therefore, PUT to /resource is not allowed.
You can override this logic in ModelViewSet to take the id from body, but I wouldn't recommend doing that.
EDIT: I just noticed you add endpoint += ${ this.slug }/;. Could you please show how your endpoint for PUT looks like over here? With this slug. Is that a proper id in your serializer?
i think you have to allow cors headers on your server side
pip install django-cors-headers
in django app settings.py:
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
CORS_ORIGIN_ALLOW_ALL = True
Looks like your issue in permissions
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
My guess is that your IsAuthorOrReadOnly is causing this. Could you please provide source code for it?

Axios GET not including params in Nuxt template

I want to pass an id to axios so that I can switch url dynamically.
My axios request in my template is as follows:
async asyncData({ params }) {
const { data } = await axios.get('http://localhost:8000/api/', {
params: {
id: 1
}
})
return { data }
}
The request being passed to my api is:
GET /api/?id=1
but I need
GET /api/1
What is happening here?
It looks like the asyncData function is called once when the page is loaded. I am still no wiser as to why it does not accept params in the way outlined in the docs and numerous tutorials, but it would not refresh the page because it is never called again.
To refresh the page data with a new api call, you need to return the axios promise from within the methods part of the export. The code below does the axios get request first, then adds or subtracts 1 from the id with plus and minus functions.
<script>
import axios from 'axios'
export default {
head() {
return {
title: 'Weather'
}
},
data: function() {
return { counter: 1 }
},
methods: {
plus: function(counter, data, datalength) {
this.counter += 1
axios.get('http://localhost:8000/api/' + this.counter).then(res => {
console.log(this.counter)
console.log(res.data)
return (this.data = res.data)
})
},
minus: function(counter, data) {
if (this.counter >= 2) {
this.counter -= 1
axios.get('http://localhost:8000/api/' + this.counter).then(res => {
console.log(this.counter)
console.log(res.data)
return (this.data = res.data)
})
} else {
this.counter = 1
}
}
},
async asyncData({ params, counter }) {
let { data } = await axios.get('http://localhost:8000/api/1')
return { data }
}
}
</script>
If anybody wants to elaborate or post a better solution, please go ahead - but I'm posting this because I searched so many tutorials and nothing worked until I found a way to interpret the documentation, which is certainly not beginner-friendly.

How to mock an image with a fixture in cypress

I'm using cypress to test my VueJS application. The one thing I'm having trouble with is mocking an image to be displayed on the page. For my use case, I'm simply loading a user profile with the following code:
describe('Test Login', () => {
it('Can Login', () => {
cy.server();
cy.route({
method: 'GET',
url: '/api/account/',
response: 'fx:profile.json',
});
cy.route('**/media/demo1.png', 'fx:demo1.png');
});
});
fixtures/profile.json
{
"avatar": "http://localhost:8080/media/demo1.png",
"username": "cypress",
"email": "email#cypress.io",
"pk": 1,
"is_staff": true,
"is_superuser": true,
"is_active": true
}
The profile fixture data is loading correctly in the test. In my fixtures folder, I also have a demo1.png file. I am expecting this image to be loaded and displayed on the page during my test, but it is being displayed as a broken image.
In the network tab, it shows demo1.png as a broken image with a 200 response code and type of text/html.
The cypress documentation mostly discusses images in the context of uploading images, but I haven't been able to find an example of how I can mock an image that is loaded through a <img> tag. Is there an easier way of doing this?
I am not sure if this answer can help you. But at least it is a workaround for this problem ;-)
Say we have a HTML like this:
<html>
<body>
<button id="button">load</button>
<div id="profile">
</div>
<script>
function httpGetAsync(theUrl, callback)
{
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(JSON.parse(xmlHttp.responseText));
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
document.getElementById("button").addEventListener("click", () => {
httpGetAsync("/api/account/", (result) => {
var div = document.querySelector("#profile");
var img = document.createElement("img");
img.src = result.avatar;
div.appendChild(img)
})
})
</script>
</body>
</html>
source: HTTP GET request in JavaScript?
And you want to load the profile after the click was done. Then you can use MutationObserver to replace the img.src.
First, write the MutationObserver:
var observeDOM = (function(){
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
return function( obj, callback ){
if( !obj || !obj.nodeType === 1 ) return; // validation
if( MutationObserver ){
// define a new observer
var obs = new MutationObserver(function(mutations, observer){
callback(mutations);
})
// have the observer observe foo for changes in children
obs.observe( obj, { childList:true, subtree:true });
}
else if( window.addEventListener ){
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
}
})();
(heavily copy & pasted from Detect changes in the DOM)
Now you are able to do this:
describe('Test Login', () => {
it('Can Login', () => {
var win = null;
cy.server();
cy.route({
method: 'GET',
url: '/api/account/',
response: 'fx:profile.json'
});
cy.visit("index.html").then(w => {
cy.get("#profile").then(pro => {
var e = pro[0];
observeDOM(e, (m) => {
// add a red dot image
m[0].addedNodes[0].src = "data:image/png;base64,"+
"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGP"+
"C/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IA"+
"AAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1J"+
"REFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jq"+
"ch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0"+
"vr4MkhoXe0rZigAAAABJRU5ErkJggg=="
})
})
cy.get("button").click()
})
});
});
(yeah at least some lines of code are written on my own ;-P)
You can read the image from the img.src attribute from the fixtures folder. For the sake of simplicity I have used a static base64 string here.
And the result:
We are not using this kind of stuff in our aurelia app but I tried similar things in a private project some time ago.

Angular 2 download PDF from API and Display it in View

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.
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:
Download pdf file using jquery ajax
Receive zip file, angularJs
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 ]
})
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.
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 */ }
);
}
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
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>
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);
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);
}
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.
http
.post(url, data, {
responseType: "blob",
observe: "response"
})
.pipe(
map(response => {
saveAs(response.body, "fileName.pdf");
})
);
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:
FileSaver
Angular Http Client