get a photo preview over v-for loop - javascript

I want to have a photo preview in my code below. I'm working with BootstrapVue.
But I don't know where the problem is.. after looking in my Vue devtools I can see that the form-file has the picture, but my "watch" is not working..
It would be very helpful if someone can help me out !
My template:
<template>
<div>
<div v-for="item in files" :key="item.id">
<b-button v-b-toggle="item.id" variant="danger btn-block mb-2">
Upload {{ item.id }}</b-button>
<b-collapse :id="item.id" class="mt-2">
<div class="m-2 mt-3">
<table class="table table-striped mt-2">
<tbody>
<div class="mt-3 mb-2 ml-1">Upload</div>
<b-form-file
:v-model="item.request"
placeholder="Upload ..."
class="mb-2"
></b-form-file>
<b-img
v-if="item.request"
:src="item.src"
class="mb-3"
fluid
block
rounded
></b-img>
</tbody>
</table>
</div>
</b-collapse>
</div>
</div>
</template>
My script:
<script>
const base64Encode = (data) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(data);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
export default {
data() {
return {
files: [
{ id: "1", src: null, request: null },
{ id: "2", src: null, request: null },
{ id: "3", src: null, request: null },
],
};
},
watch: {
files: {
deep: true, //watch changes at objects properties
handler(newValue, oldValue) {
if (newValue !== oldValue) {
if (newValue) {
base64Encode(newValue.src)
.then((value) => {
newValue.src = value;
})
.catch(() => {
newValue.src = null;
});
} else {
newValue.src = null;
}
}
},
},
},
};
</script>

You made a little mistake!
Instead of writing :v-model for b-form-file component use v-model.
In watch property you cannot understand which upload inputs changed. So try using input event handler for b-form-file. Then you can pass index of each files and run base64Encode function.
You can see working version of your code here.

Related

How to pass data after page loading to components in VueJS?

In my project I use Vue.js and Nuxt.js and I have this page.
This page is settings page, where user can changes his settings. As you can see, this is only one page, where user can switch between tabs.
<template>
<div class="account-wrapper">
<div class="avatar" #click="redirect('/account/me')">
<img class='avatar-box' src="../../../assets/img/testava.jpg" alt="ava">
<div class="avatar-text">
<h2 class="nmp">{{ personalSettings.username }}</h2>
<p class="paragraph opacity nmp">Public profile</p>
</div>
</div>
<div class="side-bar">
<div v-for="item in accountHeaderItems" :key="item.title" class="flex">
<div v-if="item.active" class="vertical-line" />
<p :class="[item.active ? 'item item-active' : 'item']" #click="changeSubsection(item)">
{{ item.title }}
</p>
</div>
</div>
<personal-information v-if="currentSection === 'Public account'" :personal-settings="personalSettings" />
<security-settings v-else-if="currentSection === 'Security settings'" :security-settings="securitySettings" />
<site-settings v-else />
</div>
</template>
<script>
import SecuritySettings from '~/components/pageComponents/settings/SecuritySettings'
import PersonalInformation from '~/components/pageComponents/settings/PersonalInformation'
import SiteSettings from '~/components/pageComponents/settings/SiteSettings'
import { getUserSettings } from "~/api";
export default {
name: 'Settings',
components: {
SecuritySettings,
PersonalInformation,
SiteSettings
},
data() {
return {
accountHeaderItems: [
{ title: 'Public account', active: true },
{ title: 'Security settings', active: false },
{ title: 'Appearance settings', active: false },
{ title: 'Notifications', active: false }
],
currentSection: 'Public account',
personalSettings: {},
securitySettings: {},
}
},
async mounted() {
if (localStorage.getItem('token') !== null) await this.getUsersSettings(localStorage.getItem('token'))
else await this.$router.push('/')
},
methods: {
async getUsersSettings(token) {
const userSettings = await getUserSettings(token)
if (userSettings.status === -1)
return this.$router.push('/')
this.personalSettings = userSettings.personalSettings
this.securitySettings = userSettings.securitySettings
},
changeSubsection(item) {
this.currentSection = item.title
this.accountHeaderItems.forEach(header => {
header.active = item.title === header.title
})
},
redirect(path) {
this.$router.push(path)
},
}
}
</script>
The problem is when page loads. When in async mounted() I get data I want to pass it to my components. And here is the problem, when I try to do that it seems to work fine, but there is strange behaviour, I always need to switch between tabs, to make data be visible on page.
For example - in personalSettings object there is field first_name. So, in personal-information component in custom Input I want to show this data in this way (in mounted I make copy of object to prevent mutations):
<Input
v-model="personalInfo.first_name"
:title="'First name'"
:title-class="'small'"
:additional-class="'small'"
/>
...
props: {
personalSettings: {
type: Object,
default: () => {}
}
},
data() {
return {
personalInfo: {},
loading: false,
showPopup: false
}
},
mounted() {
this.personalInfo = this.personalSettings
},
Everything seems to be fine, but, actually, I have to switch to another tab and switch back to this tab to see this data. What's wrong? How can I prevent this behaviour and show data in correct way?
There are many ways to do it, you can use Store, and emit changes and Data you want to use late.
See: https://vuex.vuejs.org/guide/#the-simplest-store

Multiple file upload and preview (Images and Videos)

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

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>

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.

Vue multiselect not allowing new tags to be created

I have a mostly working Vue multiselect where I"m using an autocomplete function through an axios call to a database. As far as returning the DB results and adding them as options, then tags, it works perfectly.
My problem is that I can't create new tags. So if my axios call returns "Testing one" as an option and I select it, it become a tag. However, if no results are returned when I typ "new test" and hit enter, it doesn't become a new tag.
WHat am I doing wrong?
<div id="tagApp">
<multiselect
label="tag_data"
track-by="campaign_tag_id"
v-model="value"
:options="options"
:loading="loading"
:multiple="true"
:taggable="true"
#search-change="val => read(val)"
></multiselect>
</div>
new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
el: "#tagApp",
data() {
return{
value: [],
loading: false,
options: []
}
},
methods: {
read: function(val){
//console.log('searched for', val);
if (val) {
this.loading = true;
this.options = [];
const self = this;
console.log(val);
axios.get('campaigns/search',{params: {query: val}})
.then(function (response) {
self.options = response.data;
console.log(response.data);
});
} else {
this.options = [];
}
}
}
})
I suggest you read their documentation... Everything you need is there.. you have to add taggable as well as handle the creations with #tag="handleTagMethod"
CodePen mirror: https://codepen.io/oze4/pen/ROVqZK?editors=1010
Vue.component("multiselect", window.VueMultiselect.default);
new Vue({
el: "#app",
data: {
value: [],
options: []
},
methods: {
addTag(newTag) {
const tag = {
title: newTag,
// you'll need to add other items specific to your objects
};
this.options.push(tag);
this.value.push(tag);
}
},
mounted() {
var self = this;
axios
.get("https://jsonplaceholder.typicode.com/todos?_start=1&_end=10")
.then(response => {
self.options = response.data;
})
.catch(error => {
alert(error);
});
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vue-multiselect#2.1.0/dist/vue-multiselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-multiselect#2.1.0/dist/vue-multiselect.min.css">
<div id="app">
<div style="width: 600px">
<label class="typo__label">Simple select / dropdown</label>
<multiselect
v-model="value"
:height="300"
:options="options"
:multiple="true"
:taggable="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
tag-placeholder="Add this as new tag"
placeholder="Search or add a tag"
#tag="addTag"
label="title"
track-by="title"
:preselect-first="false"
>
<template slot="selection" slot-scope="{ values, search, isOpen }"><span class="multiselect__single" v-if="values.length && !isOpen">{{ values.length }} options selected</span></template>
</multiselect>
<pre class="language-json"><code>{{ value }}</code></pre>
</div>
</div>

Categories

Resources