When I try to upload a user photo with Vue, the formData just seems to be an empty object. The Multer middleware then fails because there is no req.file for it read.
User.vue
<form enctype="multipart/form-data">
<div class="pl-3 label">User Photo</div>
<v-file-input
accept='image/*'
label="Upload Photo"
v-model="photo"
></v-file-input>
<v-btn class="mr-5" color="blue lighten-3" #click="submit">
save changes
</v-btn>
export default {
data: () => ({
photo: null
}),
methods: {
submit() {
let formData = new FormData();
if (this.photo) {
formData.append('photo', this.photo)
}
this.$store.dispatch('updateMe', {
photo: formData
});
}
}
};
Vuex Store
export default new Vuex.Store({
state: {
photo: null
},
mutations: {
setUpdatedUser(state, payload) {
state.name = payload.name;
state.email = payload.email;
state.photo = payload.photo;
}
},
actions: {
async updateMe({ commit, getters }, payload) {
let id = localStorage.getItem('userId');
let user = {
photo: payload.photo
};
try {
let token = getters.token;
const response = await axios.patch(
`http://localhost:3000/api/v1/users/updateMe`,
user,
{
headers: { Authorization: 'Bearer ' + token }
}
);
commit('setUpdatedUser', response.data.updatedUser);
} catch (err) {
console.log(err);
}
}
}
User Controller
const multerStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/users');
},
filename: (req, file, cb) => {
console.log(file)
const ext = file.mimetype.split('/')[1];
cb(null, `user-${req.user.id}-${Date.now()}.${ext}`);
}
});
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image. Please upload only images.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadUserPhoto = upload.single('photo');
Most other examples I have compared to do not use v-model for the input binding. Does that make a difference?
In an application where I am handling file uploads the files get posted to the node backend.
I also v-model in my component so that shouldn't be the problem.
Frontend method:
upload() {
const data = new FormData()
data.append('file', this.file, this.file.name) // Filename is optional
data.append('additionalInformation', this.additionalInformation)
axios.
post('/api/v1/upload/', data)
.then(response => /* response handling */)
.catch(error => /* error handling */)
}
The node endpoint looks like this:
app.post('/api/v1/upload/', multer().single('file'), (request, response) => {
upload(request.body, request.file, response, config)
})
Reference: FormData.append()
MyView.vue :
<template>
<v-container fluid>
<v-row>
<v-col v-for="obj in myObjs" :key="obj.ref">
<MyChild :obj="obj" />
</v-col>
</v-row>
</v-container>
</template>
export default class MyView extends Vue {
myObjs: MyObj[] = []
mounted () {
this.myObjs = await api.getGreatObjs()
// this.myObjs is like expected
}
}
MyView.spec.ts :
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
jest.mock('axios')
jest.spyOn(api, 'getGreatObjs')
.mockImplementation( () => new Promise((resolve, reject) => {
resolve([new MyObj({name: 'toto', value: 45}), ...])
}))
})
it('should render', () => {
wrapper = mount(MyView, {
stubs: ['router-link']
})
expect(wrapper.findAll(MyChild).length).toBe(2)
})
Content of wrapper.html():
<v-container fluid=\"\"><v-container fluid=\"\"><v-row></v-row></v-container></v-container>
I also tried this (MyView.spec.ts):
describe('P1RealTime.vue', () => {
let wrapper: any
let localVue: any
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
})
it('should render', () => {
wrapper = mount(MyView, {
localVue: localVue,
stubs: ['router-link']
})
expect(wrapper.html()).toEqual('')
})
})
Console result :
Expected: ""
Received: "<div class=\"container container--fluid\"><div class=\"container container--fluid\"><div></div></div>"
There is nothing inside "row" (<v-row></v-row>) or "div" (<div></div>) but there should be multiple MyChild ; so, I don't understand.
Could you help me ?
I have a form with multiple inputs that includes file input too. Now, I want to pass these data on the onSubmit function. but, there is an issue, in the quasar documentation, I didn't see instruction about file upload by Axios in the script part.
I read Uploader in the quasar doc and also I read this one from Stackoverlow, But I didn't work for me.
Also, this is my templates code:
<template>
<div class="q-pa-md q-mt-md">
<q-card class="my-card">
<q-form
#submit="onSubmit"
class="q-gutter-md"
>
<div class="row justify-center">
<q-uploader
label="Upload your music"
color="purple"
accept=".mp3"
:max-file-size="20000000"
square
flat
#add="file_selected"
bordered
/>
</div>
<div class="row justify-center">
<q-btn label="Edit" type="submit" color="primary" v-if="song_id" class="q-ma-md" />
<q-btn label="Add" type="submit" color="primary" v-else class="q-ma-md" />
<q-btn label="Cancel" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</div>
</template>
And the methods part:
file_selected: function (file) {
console.log(file)
this.selected_file = file[0]
this.check_if_document_upload = true
},
onSubmit: function () {
const url = '/core/v1/api/songs/upload'
const fileData = new FormData()
fileData.append('file_data', this.selected_file)
fileData.append('song_id', this.song_id)
this.$axios.post(url, fileData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function () {
console.log('SUCCESS!!')
})
.catch(function () {
console.log('FAILURE!!')
})
And data part:
data: () => ({
selected_file: '',
check_if_document_upload: false,
song_id: '',
song_data: {
status: true
},
dashData: []
}),
If quasar uploads isn't working for you and you are using state management vuex, you could attempt writing custom code to accomplish what you want. Try this for sending the post request using axios
createEvents({ commit }, payload) {
const stuff = {
title: payload.title,
location: payload.location,
description: payload.description,
image = payload.image;
};
let formData = new FormData();
bodyFormData.set('title', stuff.title); //done for the text data
formData.append("imageUrl", stuff.image); //done for file data
axios
.post({
method: 'post',
url: 'myurl',
data: formData,
headers: {'Content-Type': 'multipart/form-data' }
})
.then(response => {
commit("createEvents", response.data);
})
.catch(err => err.data);
}
}
And for the submit function(method), it should look something like this
createEvent(){
const newEvent = {
title: '',
location: '',
description: '',
image: this.image,
};
this.$store.dispatch("createEvents", newEvent);
};
finally, the form itself in your code. The image should be referenced with a simple
<input type='file' ref='image'> and the rest of your form can be normal
<form>
<input type='text' v-model='text'>
<-- more of the same -->
<input type='file' ref='image'>
// prevent is to keep the page from reloading when the form gets submitted,
// just a precaution measure
<button type=submit #click.prevent=createEvent()>submit</button>
</form>
Hope this helped
I found my issue. I should change #add to #added in the template.
<template>
<div class="q-pa-md q-mt-md">
<q-card class="my-card">
<q-form
#submit="onSubmit"
class="q-gutter-md"
>
<div class="row justify-center">
<q-uploader
label="Upload your music"
color="purple"
accept=".mp3"
:max-file-size="20000000"
square
flat
#added="file_selected"
bordered
/>
</div>
<div class="row justify-center">
<q-btn label="Edit" type="submit" color="primary" v-if="song_id" class="q-ma-md" />
<q-btn label="Add" type="submit" color="primary" v-else class="q-ma-md" />
<q-btn label="Cancel" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</div>
</template>
If you want to keep the QUploader functionalities, status changes, upload progress, in my case I made the component extension and it works fine, maybe it is not efficient because I had to add my own methods: upload, __uploadFiles.
Don't add method to override __runFactory, since I omitted batch load option, and I will always use factory as function.
QUploader source:
Part Code from Quasar Components Uploader -> xhr mixin.js
upload () {
if (this.canUpload === false) {
return
}
const queue = this.queuedFiles.slice(0)
this.queuedFiles = []
if (this.xhrProps.batch(queue)) {
this.__runFactory(queue)
}
else {
queue.forEach(file => {
this.__runFactory([ file ])
})
}
},
__runFactory (files) {
this.workingThreads++
if (typeof this.factory !== 'function') {
this.__uploadFiles(files, {})
return
}
const res = this.factory(files)
if (!res) {
this.$emit(
'factory-failed',
new Error('QUploader: factory() does not return properly'),
files
)
this.workingThreads--
}
else if (typeof res.catch === 'function' && typeof res.then === 'function') {
this.promises.push(res)
const failed = err => {
if (this._isBeingDestroyed !== true && this._isDestroyed !== true) {
this.promises = this.promises.filter(p => p !== res)
if (this.promises.length === 0) {
this.abortPromises = false
}
this.queuedFiles = this.queuedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'failed') })
this.$emit('factory-failed', err, files)
this.workingThreads--
}
}
res.then(factory => {
if (this.abortPromises === true) {
failed(new Error('Aborted'))
}
else if (this._isBeingDestroyed !== true && this._isDestroyed !== true) {
this.promises = this.promises.filter(p => p !== res)
this.__uploadFiles(files, factory)
}
}).catch(failed)
}
else {
this.__uploadFiles(files, res || {})
}
},
__uploadFiles (files, factory) {
const
form = new FormData(),
xhr = new XMLHttpRequest()
const getProp = (name, arg) => {
return factory[name] !== void 0
? getFn(factory[name])(arg)
: this.xhrProps[name](arg)
}
const url = getProp('url', files)
if (!url) {
console.error('q-uploader: invalid or no URL specified')
this.workingThreads--
return
}
const fields = getProp('formFields', files)
fields !== void 0 && fields.forEach(field => {
form.append(field.name, field.value)
})
let
uploadIndex = 0,
uploadIndexSize = 0,
uploadedSize = 0,
maxUploadSize = 0,
aborted
xhr.upload.addEventListener('progress', e => {
if (aborted === true) { return }
const loaded = Math.min(maxUploadSize, e.loaded)
this.uploadedSize += loaded - uploadedSize
uploadedSize = loaded
let size = uploadedSize - uploadIndexSize
for (let i = uploadIndex; size > 0 && i < files.length; i++) {
const
file = files[i],
uploaded = size > file.size
if (uploaded) {
size -= file.size
uploadIndex++
uploadIndexSize += file.size
this.__updateFile(file, 'uploading', file.size)
}
else {
this.__updateFile(file, 'uploading', size)
return
}
}
}, false)
xhr.onreadystatechange = () => {
if (xhr.readyState < 4) {
return
}
if (xhr.status && xhr.status < 400) {
this.uploadedFiles = this.uploadedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'uploaded') })
this.$emit('uploaded', { files, xhr })
}
else {
aborted = true
this.uploadedSize -= uploadedSize
this.queuedFiles = this.queuedFiles.concat(files)
files.forEach(f => { this.__updateFile(f, 'failed') })
this.$emit('failed', { files, xhr })
}
this.workingThreads--
this.xhrs = this.xhrs.filter(x => x !== xhr)
}
xhr.open(
getProp('method', files),
url
)
if (getProp('withCredentials', files) === true) {
xhr.withCredentials = true
}
const headers = getProp('headers', files)
headers !== void 0 && headers.forEach(head => {
xhr.setRequestHeader(head.name, head.value)
})
const sendRaw = getProp('sendRaw', files)
files.forEach(file => {
this.__updateFile(file, 'uploading', 0)
if (sendRaw !== true) {
form.append(getProp('fieldName', file), file, file.name)
}
file.xhr = xhr
file.__abort = () => { xhr.abort() }
maxUploadSize += file.size
})
this.$emit('uploading', { files, xhr })
this.xhrs.push(xhr)
if (sendRaw === true) {
xhr.send(new Blob(files))
}
else {
xhr.send(form)
}
}
Result: With AXIOS - Component Vue that extend from QUploader
<script lang="ts">
import { QUploader } from 'quasar';
export default class AxiosUploader extends QUploader {
constructor(props) {
super(props);
}
AxiosUpload() {
if (this.canUpload === false) {
return;
}
const queue = this.queuedFiles.slice(0);
this.queuedFiles = [];
const factory = this.factory(queue);
queue.forEach(file => {
this.workingThreads++;
this.uploadFiles([file], factory);
});
}
uploadFiles(files, factory) {
const form = new FormData(),
headers = {};
factory.headers.forEach(head => {
headers[head.name] = head.value;
});
factory.formFields.forEach(field => {
form.append(field.name, field.value);
});
form.append(factory.fieldName, files[0], files[0].name);
let uploadIndex = 0,
uploadIndexSize = 0,
uploadedSize = 0,
maxUploadSize = 0,
aborted;
const xhr = this.$axios.post(factory.url, form, {
headers,
onUploadProgress: (e: ProgressEvent) => {
if (aborted === true) {
return;
}
const loaded = Math.min(maxUploadSize, e.loaded);
this.uploadedSize += loaded - uploadedSize;
uploadedSize = loaded;
let size = uploadedSize - uploadIndexSize;
for (let i = uploadIndex; size > 0 && i < files.length; i++) {
const file = files[i],
uploaded = size > file.size;
if (uploaded) {
size -= file.size;
uploadIndex++;
uploadIndexSize += file.size;
this.__updateFile(file, 'uploading', file.size);
} else {
this.__updateFile(file, 'uploading', size);
return;
}
}
}
});
this.xhrs.push(xhr);
this.$emit('uploading', { files, xhr });
xhr
.then(res => {
this.uploadedFiles = this.uploadedFiles.concat(files);
files.forEach(f => {
this.__updateFile(f, 'uploaded');
});
this.$emit('uploaded', { files, xhr });
})
.catch(err => {
aborted = true;
this.uploadedSize -= uploadedSize;
this.queuedFiles = this.queuedFiles.concat(files);
files.forEach(f => {
this.__updateFile(f, 'failed');
});
this.$emit('failed', { files, xhr });
})
.finally(() => {
this.workingThreads--;
this.xhrs = this.xhrs.filter(x => x !== xhr);
});
files.forEach(file => {
this.__updateFile(file, 'uploading', 0);
file.xhr = xhr;
file.__abort = () => {
xhr.abort();
};
maxUploadSize += file.size;
});
this.$emit('uploading', { files, xhr });
this.xhrs.push(xhr);
}
}
</script>
The component to use is AxiosUploader instead of q-uploader,
and instead of calling the upload () method, I call the AxiosUpload method. You could adapt to your needs
I have vue SPA and I trying to upload some images and text. I try using postman to test my server code and it's work, but my client-side still error.
I think I'm still mistaken for handle req.file.
addImage.vue
<template>
<div>
<b-form-group label-cols-sm="3" description="image title" label="Title">
<b-form-input v-model="newImage.title"></b-form-input>
</b-form-group>
<b-form-group label="my Image" label-for="file" label-cols-sm="2">
<b-form-file id="file" v-model="newImage.image"></b-form-file>
</b-form-group>
<b-button variant="primary" class="pl-5 pr-5" #click.prevent="addImage">Save</b-button>
</div>
</template>
<script>
export default {
data() {
return {
newImage: {
title: '',
image: null,
},
};
},
methods: {
addImage() {
this.$store.dispatch('addProduct', this.newProduct);
}
},
};
store.js
actions: {
addImage(context, newImage) {
const config = {
headers: {
token: localStorage.getItem('token'),
},
};
Axios
.post(`${baseUrl}/product`, newImage, config)
.then(({ newImage }) => {
context.commit('ADD_IMAGE', newImage);
})
.catch((err) => {
console.log(err.message);
});
}
}
When you want to upload an image you have to set the content type and create FormData, like this:
let data = new FormData();
for (var i = 0; i < files.length; i++) {
let file = files.item(i);
data.append('images[' + i + ']', file, file.name);
}
const config = {
headers: { 'content-type': 'multipart/form-data' }
}
return axios.post('/api/images', data, config)
<template>
<v-app id="inspire" dark >
<v-layout v-for = "post in posts" :key = "post.id">
<v-flex xs3>
<v-card >
<v-card-media>
<img
:src = "post.postImage">
</v-card-media>
<v-card-title>
{{post.description}}
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-app>
</template>
<script>
import PostService from '#/service/PostService'
export default {
data(){
return {
posts: []
}
},
mounted (){
this.getPosts()
},
methods: {
async getPosts() {
const response = await PostService.fetchPosts()
this.posts = response.data.posts
}
}
}
</script>
const multer = require('multer');
const Post = require('./models/post')
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/');
},
filename: (req, file, cb ) => {
cb(null, Date.now() + file.originalname);
}
})
const fileFilter = (req, file, cb) => {
if(file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
}else {
cb(null, false);
}
}
const upload = multer({
storage: storage,
fileFilter,
limits: {
fileSize: 1024 * 1024 * 5
}
});
app.get('/posts', (req, res) => {
Post.find({},'postImage description', (error, posts) => {
if(error) {
console.log(error)
}
res.send({
posts
})
}).sort({_id: -1})
})
Hi all, i was about to display images using multer on my front end. but it does only show imagelink or image destination, but i already added some images in my backend. So how can i show images to my front-end using vue dynamically. I did use v-for for each data in my database. what should i input to my v-card-media to get images dynamically.