I am using Vue (2.0) in my project. WorkingArea component get a object via props. Words in the object are rendered by 'vfor' in WorkingArea component and they are create a sentence. I add external field named "status" the object in before component mounted. Object status can be active or inactive. I think that when status is active, color of word is changed red. Although the object is updated, component did not triggered for rendering. I'm sharing below WorkingArea component:
<template>
<div id='sentence' class="drtl mt-3">
<p :class="word.status == 'active' ? active : inactive" v-for="(word, index) in hadithObject.hadith_words" :key="index" :id='index'>
{{ word.kelime}}
</p>
</div>
<b-button variant="danger" #click="nextWord()" >next</b-button>
</template>
<script>
export default {
props: {
hid:String,
ho: Object,
},
data() {
return {
hadithObject: null,
cursor: 0,
//css class binding.
inactive: 'inactive',
active: 'active',
}
},
beforeMount () {
this.hadithObject = this.ho;
this.hadithObject.hadith_words.forEach(item => {
item.status = this.inactive;
});
},
nextWord(){
// when click to button, status of word is set active.
this.hadithObject.hadith_words[this.cursor].status = this.active;
this.cursor += 1;
}
</script>
<style lang="scss" scoped>
#import url('https://fonts.googleapis.com/css?family=Amiri&display=swap');
.inactive{
font-family: 'Amiri', serif;
font-size: 23px;
line-height: 2.0;
display: inline-block;
color: black;
}
.drtl{
direction: rtl;
}
.active{
color: red;
font-family: 'Amiri', serif;
font-size: 23px;
line-height: 2.0;
display: inline-block;
}
</style>
-------UPDATED FOR SOLUTION--------
After #Radu Diță answers, I examine shared this link. I learned that Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
My mistake is trying first item. "newtWord" method is updated like below:
nextWord(){
var newItem = this.hadithObject.hadith_words[this.cursor];
newItem.status = this.active;
this.$set(this.hadithObject.hadith_words,this.cursor,newItem);
this.cursor += 1;
}
You are updating hadithObject's keys. They are not reactive as they aren't added from the beginning.
Look over the caveats regarding reactivity.
You have 2 options:
either assign the object again this.hadithObject = Object.assign({}, ...this.hadithObject)
use Vue.set to set the new keys on the object.
I am creating a slider in vuejs and am using vue2-dropzone plugin for file uploads where each slide (slide-template.vue) has a vue2-dropzone component.
When app loads, image files are manually added in each vue2-dropzone (manuallyAddFile plugins API) queried from image API (hosted on heroku)
The issue is when I delete the first slide, calling the parent's (slider.vue) method removeSlideFn (passed down to child as prop) from child (slide-template.vue) component first slide is deleted but not entirely the dropzone images of the first slides are not destroyed and remains in the DOM, instead images of slide2, (the next slide) are deleted from the DOM (Pls try it once on codesandbox demo to actually know what I am mean). This does not happen when I delete slide2 or slide3 but only on slide1.
CodeSandBox Demo
App.vue
<template>
<div id="app">
<img width="15%" src="./assets/logo.png">
<slider />
</div>
</template>
<script>
import slider from "./components/slider";
export default {
name: "App",
components: {
slider
}
};
</script>
components\slider.vue (parent)
<template>
<div>
<hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
<slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
<slide-template
:slideItem="slideItem"
:slideIDX="idx"
:removeSlideFn="removeCurrSlide" />
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
<hooper-pagination slot="hooper-addons"></hooper-pagination>
</hooper>
<div class="buttons has-addons is-centered is-inline-block">
<button class="button is-info" #click="slidePrev">PREV</button>
<button class="button is-info" #click="slideNext">NEXT</button>
</div>
</div>
</template>
<script>
import {
Hooper,
Slide,
Pagination as HooperPagination,
Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";
import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";
export default {
data() {
return {
sliderRef: "SlideShow 1",
slideList: [],
hooperSettings: {
autoPlay: false,
centerMode: true,
progress: true
},
hooperStyle: {
height: "265px"
}
};
},
methods: {
slidePrev() {
this.$refs.carousel.slidePrev();
},
slideNext() {
this.$refs.carousel.slideNext();
},
//Removes slider identified by IDX
removeCurrSlide(idx) {
this.slideList.splice(idx, 1);
},
// Fetch data from firebase
getSliderData() {
let that = this;
let mySliderRef = slideShowsRef.child(this.sliderRef);
mySliderRef.once("value", snap => {
if (snap.val()) {
this.slideList = [];
snap.forEach(childSnapshot => {
that.slideList.push(childSnapshot.val());
});
}
});
}
},
watch: {
getSlider: {
handler: "getSliderData",
immediate: true
}
},
components: {
slideTemplate,
Hooper,
Slide,
HooperPagination,
HooperNavigation
}
};
</script>
components/slide-template.vue (child, with vue2-dropzone)
<template>
<div class="slide-wrapper">
<slideTitle :heading="slideItem.heading" />
<a class="button delete remove-curr-slide" #click="deleteCurrSlide(slideIDX)" ></a>
<vue2Dropzone
#vdropzone-file-added="fileWasAdded"
#vdropzone-thumbnail="thumbnail"
#vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
:destroyDropzone="false"
:include-styling="false"
:ref="`dropZone${ slideIDX }`"
:id="`customDropZone${ slideIDX }`"
:options="dropzoneOptions">
</vue2Dropzone>
</div>
</template>
<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "#dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";
export default {
props: ["slideIDX", "slideItem", "removeSlideFn"],
data() {
return {
dropzoneOptions: {
url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
thumbnailWidth: 150,
autoProcessQueue: false,
maxFiles: 1,
maxFilesize: 2,
addRemoveLinks: true,
previewTemplate: this.template()
}
};
},
components: {
slideTitle,
vue2Dropzone
},
methods: {
template: function() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<img data-dz-thumbnail/>
</div>
<div class="dz-details">
<!-- <div class="dz-size"><span data-dz-size></span></div> -->
<!-- <div class="dz-filename"><span data-dz-name></span></div> -->
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>`;
},
thumbnail: function(file, dataUrl) {
var j, len, ref, thumbnailElement;
if (file.previewElement) {
file.previewElement.classList.remove("dz-file-preview");
ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j];
thumbnailElement.alt = file.name;
}
thumbnailElement.src = dataUrl;
return setTimeout(
(function(_this) {
return function() {
return file.previewElement.classList.add("dz-image-preview");
};
})(this),
1
);
}
},
// Drag & Drop Events
async manuallyAddFiles(zoneData) {
if (zoneData) {
let dropZone = `dropZone${this.slideIDX}`;
asyncForEach(zoneData, async fileInfo => {
var mockFile = {
size: fileInfo.size,
name: fileInfo.originalName || fileInfo.name,
type: fileInfo.type,
id: fileInfo.id,
childZoneId: fileInfo.childZoneId
};
let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
fileInfo.id
}`;
let objURL = await generate_ObjURLfromImageStream(url);
this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
});
}
},
fileWasAdded(file) {
console.log("Successfully Loaded Files from Server");
},
deleteCurrSlide(idx) {
this.removeSlideFn(idx);
}
}
};
</script>
<style lang="scss">
.slide-wrapper {
position: relative;
}
[id^="customDropZone"] {
background-color: orange;
font-family: "Arial", sans-serif;
letter-spacing: 0.2px;
/* color: #777; */
transition: background-color 0.2s linear;
// height: 200px;
padding: 40px;
}
[id^="customDropZone"] .dz-preview {
width: 160px;
display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
width: 80px;
height: 80px;
margin-left: 40px;
margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
width: inherit;
height: inherit;
// border-radius: 50%;
background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
width: 100%;
}
[id^="customDropZone"] .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
display: none;
}
.dz-size {
border: 2px solid blue;
}
#previews {
border: 2px solid red;
min-height: 50px;
z-index: 9999;
}
.button.delete.remove-curr-slide {
padding: 12px;
margin-top: 5px;
margin-left: 5px;
position: absolute;
right: 150px;
background-color: red;
}
</style>
slide-title.vue (not that important)
<template>
<h2 contenteditable #blur="save"> {{ heading }} </h2>
</template>
<script>
export default {
props: ["heading"],
methods: {
save() {
this.$emit("onTitleUpdate", event.target.innerText.trim());
}
}
};
</script>
utils.js (utility)
export async function generate_ObjURLfromImageStream(url) {
return await fetch(url)
.then(response => {
return response.body;
})
.then(rs => {
const reader = rs.getReader();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
// When no more data needs to be consumed, break the reading
if (done) {
break;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
}
// Close the stream
controller.close();
reader.releaseLock();
}
});
})
// Create a new response out of the stream
.then(rs => new Response(rs))
// Create an object URL for the response
.then(response => {
return response.blob();
})
.then(blob => {
// generate a objectURL (blob:url/<uuid> list)
return URL.createObjectURL(blob);
})
.catch(console.error);
}
Technically this is how the app works, slider.vue loads & fetches data from database (firebase) and stores in a data array slideList, loops over the slideList & passes each slideData (prop slideItem) to vue-dropzone component (in slide-template.vue), when dropzone mounts it fires the manuallyAddFiles(slideItem.zones) on the #vdropzone-mounted custom event.
The async manuallyAddFiles() fetches image from an API (hosted on heroku), creates (generate_ObjURLfromImageStream(url)) a unique blob URL for the image (blob:/) and then calls plugins API dropZone.manuallyAddFile() to load the image into the corresponding dropzone.
To delete the current slide, child's deleteCurrSlide() calls parent's (slider.vue) removeSlideFn (passed as prop) method with the idx of current slide. The removeSlideFn use splice to remove the item at the corresponding array idx this.slideList.splice(idx, 1).
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
CodeSandBox Demo
I am not sure what is causing the issue, may it's due to something in the vue's reactivity system OR Vue's Array reactivity caveat that is causing this.
Can anybody pls help me understand & resolve this and if possible point out the reason to the root of the problem.
Your help is much appreciated.
Thanks,
I think you probably missunderstand what is going on:
In VueJS there is a caching method which allow the reusing of existing component generated: - Each of your object are considered equals when rendered (at a DOM level).
So VueJS remove the last line because it is probably ask the least calculation and then recalcul the expected state. There are many side case to this (sometime, the local state is not recalculated). To avoir this: As recommended in the documentation, use :key to trace the id of your object. From the documentation:
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values...
new Vue({
el: "#app",
data: {
counterrow: 1,
rows: [],
},
methods: {
addrow: function() {
this.counterrow += 1;
this.rows.push({
id: this.counterrow,
model: ""
});
},
removerows: function(index) {
this.rows.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<table>
<tr>
<td><input type="text" name="test1" /></td>
<td><button class="btn" #click="addrow">add row</button></td>
</tr>
<tr v-for="(row,index) in rows" :key="row.id">
<td><input type="text" name="test2" v-model="row.model" /></td>
<td><button class="btn" #click="removerows(index)">remove </button></td>
</tr>
</table>
</div>
In this code:
I corrected the fact counterrow was never incremented
I added a :key
The documentation of :key
What did you mean by
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
From what I see, the elements are no longer in the DOM
I am using an counter to display some numbers, but they load up when the page loads, so it loads unless I do some button to trigger it.
Found this viewport plugin (https://github.com/BKWLD/vue-in-viewport-mixin) but I weren't able to use it. That's what I need to do, trigger a function when I reach some element (entirely), how to achieve it?
You don't necessarily need a package to do this. Add an event listener to listen to the scroll event, and check if the element is in the viewport every time there's a scroll event. Example code below - note that I've added an animation to emphasize the "appear if in viewport" effect.
Codepen here.
new Vue({
el: '#app',
created () {
window.addEventListener('scroll', this.onScroll);
},
destroyed () {
window.removeEventListener('scroll', this.onScroll);
},
data () {
return {
items: [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12
],
offsetTop: 0
}
},
watch: {
offsetTop (val) {
this.callbackFunc()
}
},
methods: {
onScroll (e) {
console.log('scrolling')
this.offsetTop = window.pageYOffset || document.documentElement.scrollTop
},
isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
callbackFunc() {
let items = document.querySelectorAll(".card");
for (var i = 0; i < items.length; i++) {
if (this.isElementInViewport(items[i])) {
items[i].classList.add("in-view");
}
}
}
}
})
.card {
height: 100px;
border: 1px solid #000;
visibility: hidden;
opacity: 0
}
.in-view {
visibility: visible;
opacity: 1;
animation: bounce-appear .5s ease forwards;
}
#keyframes bounce-appear {
0% {
transform: translateY(-50%) translateX(-50%) scale(0);
}
90% {
transform: translateY(-50%) translateX(-50%) scale(1.1);
}
100% {
tranform: translateY(-50%) translateX(-50%) scale(1);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" onscroll="onScroll">
<div v-for="item in items" class="card">
{{item}}
</div>
</div>
Another option is to use an intersection observer - I haven't explored this yet but this tutorial seems good: alligator.io/vuejs/lazy-image. Note that you will need a polyfill for IE.
I am using Vuetable and its awesome.
I am trying to create a top horizontal scroll, which I have done and its working fine. But I need to assign some events on the window.resize.
I created a component such as
<template>
<div class="top-scrollbar">
<div class="top-horizontal-scroll"></div>
</div>
</template>
<style scoped>
.top-scrollbar {
width: 100%;
height: 20px;
overflow-x: scroll;
overflow-y: hidden;
margin-left: 14px;
.top-horizontal-scroll {
height: 20px;
}
}
</style>
<script>
export default {
mounted() {
document.querySelector("div.top-scrollbar").addEventListener('scroll', this.handleScroll);
document.querySelector("div.vuetable-body-wrapper").addEventListener('scroll', this.tableScroll);
},
methods: {
handleScroll () {
document.querySelector("div.vuetable-body-wrapper").scrollLeft = document.querySelector("div.top-scrollbar").scrollLeft
},
tableScroll() {
document.querySelector("div.top-scrollbar").scrollLeft = document.querySelector("div.vuetable-body-wrapper").scrollLeft
}
}
}
</script>
I am calling it above the table such as <v-horizontal-scroll />
I created a mixin as
Vue.mixin({
methods: {
setScrollBar: () => {
let tableWidth = document.querySelector("table.vuetable").offsetWidth;
let tableWrapper = document.querySelector("div.vuetable-body-wrapper").offsetWidth;
document.querySelector("div.top-horizontal-scroll").style.width = tableWidth + "px";
document.querySelector("div.top-scrollbar").style.width = tableWrapper + "px"
}
}
})
And I am calling it when the user component on which Vuetable is being created
beforeUpdate() {
document.addEventListener("resize", this.setScrollBar());
},
mounted() {
this.$nextTick(function() {
window.addEventListener('resize', this.setScrollBar);
this.setScrollBar()
});
},
I want to understand how this resizing event working.
If I change even a single thing in the above code. I am starting to have issues.
Either it doesn't set the width of scroll main div correctly or even this.setScrollBar don't work on resizing.
I am not clear what is the logic behind this and how it is working?
I just started learning Vue and I was wondering, why should I use v-bind for style and not write it regularly in html/css file
Let's say you need to create a progress bar that is not static. You will then need to update the style attribute width for-example.
To accomplish this, we need to programatically edit the width of the element. We 'cannot' to this in plain css, therefore the :style attribute comes in handy.
Let's create an example:
Codepen
HTML
<div id="vue">
<div class="progress-bar">
<div :style="{'width':progress + '%'}" class="progress" />
</div>
<button #click="fakeProgress">Init fake progress</button>
</div>
Css;
.progress-bar, .progress {
border-radius: 20px;
height: 20px;
}
.progress-bar {
width: 250px;
background-color: gray;
}
.progress {
background-color: blue;
width: 0;
transition: all 1s ease;
}
Javascript
new Vue({
el: '#vue',
data: {
progress: 0
},
methods: {
fakeProgress() {
let progress = setInterval(() => {
if(this.progress == 100) {
clearInterval(progress)
} else {
this.progress += 1;
}
}, 50)
}
}
})
As you see here, we bind the progress data attribute to the width value on the fake progress bar. This is just a simple example, but I hope this makes you see its potential. (You could achieve this same effect using the <progress> tag, but that would ruin the explanation.
EDIT; Also want to point out that you are supposed to write all your css as normal as you point out in your question. However, :style is used in cases that you cannot normally use css for. Like the example above where we need css to change from a variable.