Multiple file upload and preview (Images and Videos) - javascript

I have a child component that has an input field that is hidden behind a slotted element. The parent will provide the slotted click event element, but also show a preview of the file(s), with the ability to delete them above.
I'm not sure how to work this select and preview functionality when working between a child/parent relationship.
What I have below is as far as I got, but I'm just confused at this point as to where to go.
The slot works to trigger the event in the child, but I get a "TypeError: Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'." when trying to actually get things to render as currently written.
Where am I going wrong with this?
If you need anymore information please let me know! Cheer!
NOTE: I also need to make this compatitible with V-model, but I don't know how to do that currently.
UploadMediaFiles (Child Component)
<template>
<div class="upload-media-files">
<input
id="input-file"
type="file"
accept="*"
multiple
#change="addMedia"
class="_add-media-input"
ref="input"
/>
<label for="input-file">
<slot :openFileDialog="openFileDialog">
<img
src="https://www.clipartmax.com/png/middle/142-1422132_png-file-svg-upload-file-icon-png.png"
alt=""
/>
</slot>
</label>
</div>
</template>
<style lang="sass" scoped>
input
display: none
</style>
<script>
export default {
name: 'UploadMediaFiles',
props: {
multiple: { type: Boolean },
accept: { type: String },
},
data() {
return {
files: [],
}
},
computed: {},
methods: {
async addMedia(event) {
const files = event.target.files || event.dataTransfer.files
if (!files.length) return
console.log(`files → `, files)
this.files.push(files)
this.$emit('selected', this.files)
},
openFileDialog() {
this.$refs.input.click()
},
},
}
</script>
SelectAndPreviewFiles (Parent Component)
<template>
<div class="select-and-preview-files">
<div v-if="selectedFiles">
<div :key="index" v-for="(selectedFile, index) in selectedFiles">
<img :src="selectedFile" alt="" />
<button #click="deleteFile(index)">Delete</button>
</div>
</div>
<!-- <img />
//OR
<video /> -->
<!-- <img :src="selectedFile" alt="" />-->
<UploadMediaFiles #selected="(files) => selectFiles(files)" v-slot="{ openFileDialog }">
<button #click="openFileDialog">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Circle-icons-upload.svg/1200px-Circle-icons-upload.svg.png"
alt=""
/>
</button>
</UploadMediaFiles>
</div>
</template>
<style lang="sass" scoped>
img
width: 20%
margin: auto
display: block
margin-bottom: 10px
</style>
<script>
import UploadMediaFiles from '../atoms/UploadMediaFiles.vue'
export default {
name: 'SelectAndPreviewFiles',
components: {
UploadMediaFiles,
},
props: {},
data() {
return {
selectedFiles: [],
}
},
computed: {},
methods: {
selectFiles(files) {
this.selectedFiles.push(files)
this.previewImage(files)
},
previewImage(files) {
var vm = this
for (var index = 0; index < files.length; index++) {
var reader = new FileReader()
reader.onload = function (event) {
const imageUrl = event.target.result
vm.files.push(imageUrl)
}
reader.readAsDataURL(files[index])
}
},
deleteFile(index) {
this.selectedFiles.splice(index, 1)
},
},
}
</script>
CodePen without the parent-child relationship
https://codepen.io/LovelyAndy/pen/gOmYGKO?editors=0001

The problem is the input value for multiple selected files is an array of FileLists, which itself is a list of File objects. However, previewImage() seems to assume that the value is an array of File objects.
files[index] is actually a FileList, which is not an acceptable argument to reader.readAsDataURL(), leading to the error.
To resolve the issue, iterate each FileList in the array:
export default {
methods: {
selectFiles(files) {
this.selectedFiles.push(files);
this.previewImage(files);
},
previewImage(files) {
var vm = this
for (var index = 0; index < files.length; index++) {
const fileList = files[index]
fileList.forEach(file => {
var reader = new FileReader()
reader.onload = function (event) {
const imageUrl = event.target.result
vm.selectedFiles.push(imageUrl)
}
reader.readAsDataURL(file)
})
}
},
}
}
demo

Related

Vue.js - passing value from parent to child does not work (as expected)

I pass the value from parent template to child template under this scheme:
parentModel -> parentTemplate -> prop -> childModel -> childTemplate.
That is, when getting in a child model, I need to handle value before installing in template... but it doesn't work!
My method is similar to a kludge =(
Parent:
<template>
<section class="login-wrapper border border-light">
<form id="add-form" class="form-signin" enctype="multipart/form-data" #submit.prevent="send">
<label>
<span>Images</span>
<input type="file" id="files" ref="files" multiple #change="addFile()"/>
</label>
<button type="submit">Submit</button>
</form>
<div id="files-container">
<div v-for="(file, index) in files" class="file-listing" v-bind:key="index">
<Preview :msg="file"></Preview><!-- here I send data to the child with :msg property -->
</div>
</div>
</section>
</template>
<script>
import Preview from "../Partial/ImagePreview.vue"
export default {
name: "ProductAdd",
components: {
Preview
},
data() {
return {
files: []
}
},
methods: {
addFile() {
for (let i = 0; i < this.$refs.files.files.length; i++) {
const file = this.$refs.files.files[i]
this.files.push( file );
}
},
async send() {
/// Sending data to API
}
}
}
</script>
Child:
<template>
<section>
<span>{{ setImage(msg) }}</span><!-- This I would like to avoid -->
<img :src="image_url" alt=""/>
</section>
</template>
<script>
export default {
name: 'ImagePreview',
data: () => {
return {
image_url: ""
}
},
props: [ "msg" ],
methods: {
setImage(data) {
const reader = new FileReader();
reader.onload = (event) => {
this.image_url = event.target.result;
};
reader.readAsDataURL(data);
return null;
}
}
}
</script>
I'm so sorry for a stupid question (perhaps), but I rarely work with frontend.
Now there is such a need =)
PS: I tried using "watch" methods, it doesn't work in this case. When changing an array in the parent component, these changes are not passed to child
But its work.. I see selected image preview
const Preview = Vue.component('ImagePreview', {
data: () => {
return {
image_url: ""
}
},
template: `
<section>
<span>{{ setImage(msg) }}</span><!-- This I would like to avoid -->
<img :src="image_url" alt=""/>
</section>
`,
props: [ "msg" ],
methods: {
setImage(data) {
const reader = new FileReader();
reader.onload = (event) => {
this.image_url = event.target.result;
};
reader.readAsDataURL(data);
return null;
}
}
});
new Vue({
name: "ProductAdd",
components: {Preview},
data() {
return {
files: []
}
},
methods: {
addFile() {
for (let i = 0; i < this.$refs.files.files.length; i++) {
const file = this.$refs.files.files[i]
this.files.push( file );
}
},
async send() {
/// Sending data to API
}
}
}).$mount('#container');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id='container'>
<section class="login-wrapper border border-light">
<form id="add-form" class="form-signin" enctype="multipart/form-data" #submit.prevent="send">
<label>
<span>Images</span>
<input type="file" id="files" ref="files" multiple #change="addFile()"/>
</label>
<button type="submit">Submit</button>
</form>
<div id="files-container">
<div v-for="(file, index) in files" class="file-listing" v-bind:key="index">
<Preview :msg="file"></Preview><!-- here I send data to the child with :msg property -->
</div>
</div>
</section>
</div>

VUEJS i want import image file directly in variable

i want import image file from specific link into my vue variable can someone help me
i tried with required but it doesn't work this.imagefile = require(linkofimage)
does anyone know how to solve it
I suggest you to import first the images as constants and assign them to your vue data properties in hooks or methods:
<template>
<div>
<img :src="imageDirect" alt="alert">
<img :src="imageOnHook" alt="alert">
<img :src="imageOnMethod" alt="alert">
<img :src="imageRequire" alt="alert">
</div>
</template>
<script>
const image = require('#/assets/alert_logo_card.png')
export default {
data: () => ({
imageDirect: image,
imageOnHook: null,
imageOnMethod: null,
imageRequire: null,
}),
mounted() {
this.imageOnHook = image
this.imageRequire = require('#/assets/alert_logo_card.png')
this.assignImage()
},
methods: {
assignImage() {
this.imageOnMethod = this.imageDirect
}
}
}
</script>
I'm using the same image just for example purpose.
Something like this also will work:
methods: {
assignImage() {
this.imageOnMethod = this.imageDirect
}
}
Showing an image from the network:
<template>
<div>
<img :src="imageFromUrl" alt="alert" width="200" height="200">
</div>
</template>
<script>
export default {
data: () => ({
imageFromUrl: null
}),
mounted() {
setTimeout(() => {
this.requestImage()
}, 2000);
},
methods: {
requestImage() {
const responseFromNetwork = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/Google_Photos_icon_%282020%29.svg/1024px-Google_Photos_icon_%282020%29.svg.png'
this.imageFromUrl = responseFromNetwork
}
}
}
</script>

Vue not showing parts of data received from axios request

So I am trying to create a page that has the possibility of receiving data from an API through a so called "repository", which has been made by us. The data retreival goes absolutely fine and it gets everything. The frontend also shows it correctly, mostly. There are some few random pieces of data that only show up once something has been changed in the frontend.
Example:
I am trying to get some info about a quiz, this quiz has a few main things: a theme, title, description and a time limit. Most of these are being displayed just fine, except for the theme and time limit. The time limit only shows up when I change the theme, same for the theme itself. Also when there are no questions the theme will also not show at first, it will only pop-up when I add a question.
To get data from the api I have created a few nested requests. Here you can see the page that gets the data initially:
<template>
<q-page class="quiz-editor flex">
<div class="question-listing" v-bind:class="getTheme()">
<div class="question-listing-add">
<q-btn color="white" text-color="black" style="float:left" v-on:click="back"><i class="fa fa-arrow-left"/></q-btn>
<q-btn style="background: green; color: white; float:right" v-on:click="addQuestion"><i class="fa fa-plus"/></q-btn>
<div style="clear:both"></div>
<div v-drag-and-drop="options">
<q-list
#reordered="reordered($event, questions);"
>
<QuestionCard
v-for="question in questions"
:key="question.position" :data-id="question.position"
:question="question"
:editMode="true"
#edit="loadQuestion"
#remove="removeQuestion">
</QuestionCard>
</q-list>
</div>
</div>
</div>
<div class="editor-sidebar">
<div class="editor-title">
<h3>{{editorTitle}}</h3>
<EditorGeneralBar v-bind:currentQuiz.sync="quiz" v-if="editorTitle === 'General'" #submitQuiz="submitQuiz" #themeSelected="selectTheme"></EditorGeneralBar>
<EditorQuestionBar v-if="editorTitle === 'Question'" #cancel="backToGeneral" #submitQuestion="editQuestion2"></EditorQuestionBar>
</div>
</div>
</q-page>
</template>
<script>
import QuestionCard from '../components/QuestionCard';
import EditorGeneralBar from '../components/EditorGeneralBar';
import EditorQuestionBar from '../components/EditorQuestionBar';
import QuizTemplateRepository from "../remote/quiz/QuizTemplateRepository";
import QuizFormRepository from "../remote/quiz/QuizFormRepository";
import AnswerRepository from "../remote/quiz/AnswerRepository";
import QuestionRepository from "../remote/quiz/QuestionRepository";
import { mapGetters, mapActions } from "vuex";
export default {
components: {QuestionCard, EditorGeneralBar, EditorQuestionBar},
name: 'QuizEditor',
data() {
return {
count: 1,
editorTitle: "General",
newQuiz: true,
newQuestion: true,
currentQuestion: null,
quiz: {
tn: "",
title: "",
description: "",
timeLimit: 60,
theme: 1
},
options: {
multipleDropzonesItemsDraggingEnabled: false,
dropzoneSelector: ".q-list",
draggableSelector: ".question-card"
},
loadedTemplateHash: "",
currentPosition: 1,
questions: []
}
},
computed: {
...mapGetters("SingleQuizModule", ["getQuiz", "getQuestion"])
},
mounted() {
var QE = this;
//this.loadedTemplateHash = typeof this.$route.params.chosenTemplateHash == 'undefined' ? "" : this.$route.params.chosenTemplateHash
this.loadedTemplateHash = "5fmdkeq82";
if(this.loadedTemplateHash != "") {
QuizTemplateRepository.getTemplate(this.loadedTemplateHash, this.$store.state.authLogin.token).then(function (res) {
QE.quiz = {
tn: QE.loadedTemplateHash,
title: res.data.template.label,
description: res.data.template.description,
};
QuizTemplateRepository.getTemplateContent(QE.quiz.tn, QE.$store.state.authLogin.token).then(function (res) {
const templateContent = JSON.parse(res.data.content.content);
var questions = templateContent.questions;
QE.quiz.theme = templateContent.properties.theme;
QE.quiz.timeLimit = templateContent.properties.timeLimit;
QE.quiz.questions = questions;
QE.saveQuiz(QE.quiz);
// loop through the questions.
questions.forEach(question => {
// get the questions by their question hash.
QuestionRepository.getQuestion(question, QE.$store.state.authLogin.token).then(function (resQuest) {
var vogh = resQuest.data.var[0].vogh;
// get the answers from the question.
AnswerRepository.getAnswerGroupAnswers(vogh, QE.$store.state.authLogin.token).then(function(resAnswer) {
var quest = {
name: resQuest.data.var[0].name,
hash: resQuest.data.var[0].vh,
vogh: resQuest.data.var[0].vogh,
label: resQuest.data.var[0].label,
position: resQuest.data.var[0].position,
description: "",
answers: [],
isNew: false
}
// loop through the answers and add them to the question answer array.
resAnswer.data.varoptiongroup.forEach(answer => {
answer.position = QE.getPositionString(answer.position);
answer.isNew = false;
if(answer.value > 0)
answer.isCorrect = true;
else
answer.isCorrect = false;
quest.answers.push(answer);
});
QE.questions.push(quest);
QE.currentPosition++;
});
QE.saveQuiz(QE.quiz);
});
});
});
});
} else {
this.saveQuiz(this.quiz);
}
},
For the above code the methods part is really big and it all works anyway. The component which shows some generic data of the quiz is the EditorGeneralBar, this loads everything just fine but except for the time limit, which as I said, only shows up when I change a theme. This is how the component looks like:
<template>
<div class="bar-content">
<q-form
#submit="submit"
class="q-gutter-md"
>
<q-input
filled
v-model="quiz.title"
label="Title"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled
type="text"
v-model="quiz.description"
label="Description"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
{{quiz.timeLimit}}
<q-input
filled
type="number"
v-model="quiz.timeLimit"
label="Time limit"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<input type="text" :value="quiz.timeLimit"/>
<q-file filled bottom-slots v-model="quiz.thumbnail" label="Thumbnail">
<template v-slot:before>
<q-icon name="folder_open" />
</template>
<template v-slot:hint>
A thumbnail for the quiz.
</template>
<template v-slot:append>
<q-btn round dense flat icon="add" #click.stop />
</template>
</q-file>
<p>Themes</p>
<div class="theme-list">
<div class="theme-1 theme-preview" v-on:click="selectTheme(1)"></div>
<div class="theme-2 theme-preview" v-on:click="selectTheme(2)"></div>
<div class="theme-3 theme-preview" v-on:click="selectTheme(3)"></div>
<div class="theme-4 theme-preview" v-on:click="selectTheme(4)"></div>
<div class="theme-5 theme-preview" v-on:click="selectTheme(5)"></div>
</div>
<div>
<q-btn label="Save" type="submit" color="primary"/>
</div>
</q-form>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: 'EditorGeneralBar',
data() {
return {
quiz: {}
}
},
props: {
currentQuiz: {
type: Object,
default: function() {
return {
tn: "",
title: "",
description: "",
timeLimit: 60,
theme: 1,
questions: []
}
}
}
},
computed: {
...mapGetters("SingleQuizModule", ["getQuiz"]),
},
mounted() {
this.quiz = this.currentQuiz;
},
methods: {
...mapActions("SingleQuizModule", [
"saveQuiz"
]),
submit:function() {
this.saveQuiz(this.quiz);
this.$emit("submitQuiz");
},
selectTheme:function(theme) {
this.quiz.theme = theme
this.saveQuiz(this.quiz);
this.$emit("themeSelected");
}
},
watch: {
currentQuiz: function(quiz, oldQuiz) {
console.log("seen child quiz: ", quiz);
console.log("seend child old: ", oldQuiz);
this.quiz = quiz;
this.currentQuiz = quiz;
this.currentQuiz.timeLimit = quiz.timeLimit;
}
}
}
</script>
Above console output boils down to this:
seen child quiz: The correct quiz with the correct data which I want to show in the UI
seen child old: The old wrong quiz data, which will will be replaced by the new data in the function
In the case of emited events it stores it to the state, my attempt at fixing this by utilizing vuex. These events will be caught and handled in the QuizEditor page(the first piece of code). These do nothing more than this:
handler: function() {
this.quiz = this.getQuiz //the state getter.
}
And for the getTheme() function this has been made:
getTheme: function() {
return "theme-"+this.quiz.theme;
},
Does anybody know how to correctly handle this "two-way binding" and showing of data in the UI? I made several attempts and this is the final result.

How to call a method in a Vue component from programmatically inserted component

I'm trying to call a method from a child component which is programatically inserted.
Here is my code.
MultipleFileUploader.vue
<template>
<div class="form-group" id="multiple-file-uploader">
<div>
<multiple-file-uploader-part
:name="uploadername" :index="1"
#remove="deleteUploader" #fileselected="fileSelected($event)">
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from './MultipleFileUploaderPart.vue';
let index_count = 1;
export default {
components: {
'multiple-file-uploader-part':MultipleFileUploaderPart,
},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
next_id:1,
}
},
methods: {
fileSelected: function (target) {
var UploaderPart = Vue.extend(MultipleFileUploaderPart);
new UploaderPart().$on('fileselected','fileSelected')
.$mount('#multiple-file-uploader');
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
<style scoped>
</style>
MultipleFileUploaderPart.vue
<template>
<div v-bind:id="name + '['+index+']'">
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="removeClicked"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
</div>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
removeClicked: function () {
document.getElementById(this.name+'[' + this.index + ']' ).remove();
this.$emit('remove', this.index);
},
fileSelectedMethod: function (target) {
this.size = target.files[0].size;
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
<style scoped>
I'm trying to achieve is that when a file input is filled with a file, a MultipleFileUploaderPart is created. And when the file input element in this component is filled, another MultipleFileUploaderPart is inserted.
I'd like to call MultipleFileUploader 's fileSelected method from newly inserted components so that I can create another component.
I also want to remove a MultipleFileUploaderPart component when the delete button is clicked.
How can I achieve this? or is there a better way?
EDIT:
This is what I originally had.
MultipleFileUploader.vue
<template>
<div class="form-group">
<div>
<multiple-file-uploader-part
v-for="uploader in uploaders"
:name="uploadername" :index="uploader.id"
#remove="deleteUploader" #fileselected="fileSelected($event)">
slot
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from "./MultipleFileUploaderPart";
let index_count = 1;
export default {
//name: "MultipleFileUploader",
components: {MultipleFileUploaderPart},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
uploaders: [
{
id: index_count++,
},
]
}
},
methods: {
fileSelected: function (target) {
if(target.value){
this.uploaders.push({
id: index_count++,
})
}
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
MultipleFileUploaderPart.vue
<template>
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="$emit('remove',index)"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
<br>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
checkFileSize: function () {
},
fileSelectedMethod: function (target) {
console.log(target);
console.log(target.files);
this.size = target.files[0].size;
console.log(this.size);
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
And this happens. please click
When I click 'Delete'Button, correct child coponent is deleted but the file in the input form stays there. that's why I'm seeking for another approach.
Declare uploaders as an array of objects that contain all needed props for creation of MultipleFileUploaderPart.
Use v-for on MultipleFileUploaderPart in the main MultipleFileUploader to reactively generate MultipleFileUploaderPart components
Use $emit from MultipleFileUploaderPart to MultipleFileUploader to emit creation and deletion events so that MultipleFileUploader can add or remove elements in the uploaders array.
Please don't delete or create elements from DOM directly, let the VueJs do this work.

Multiple image preview using V-for when uploading (Vue JS)

Image not showing on loop using :ref
I am using FileReader() to read the field.
Vue Component
<div v-for="(image, index) in imgFile" :key="index">
<img :ref="'image'+parseInt( index )">
{{image.name}}
</div>
<input type="file" class="file-upload-default" #change="onFileChange" multiple>
<span style="cursor:pointer" class="file-upload-browse">
<img src="/addmore.png" height="50" width="50">
</span>
export default{
onFileChange(e){
var selectedFiles = e.target.files;
for (var i=0; i < selectedFiles.length; i++){
this.imgFile.push(selectedFiles[i]);
}
if (selectedFiles) {
for (var i=0; i < this.imgFile.length; i++){
let reader = new FileReader();
reader.addEventListener('load', function(){
this.$ref["image"+parseInt( i )][0].src = reader.result;
}.bind(this), false);
reader.readAsDataURL(this.imgFile[i]);
}
}
}
}
I want to show the images which is selected from eventListener.
Improve your architecture by splitting your logic into two components. You can do this like this:
<template>
<div class="parent-component">
<image-component :image="image" v-for="(image, index) in images" :key="index" />
</div>
</template>
<script>
export default {
name: 'ParentComponent',
data () {
return {
images: []
};
}
};
</script>
<template>
<div class="image-component">
<img ref="imgElement">
{{ image.name }}
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: {
image: {
type: Object,
required: true
}
},
methods: {
...
},
mounted () {
// you don't have to loop here, because in the scope of the image-component,
// you only have one `<img>`-tag. the loop is in the parent component
console.log(this.$refs.imgElement);
}
};
</script>

Categories

Resources