How to prevent Computed property getting undefined in vue.js? - vue.js

I'm calculating the container width and height using computed property and assigns it to the canvas in vue js.
export default {
...
computed: {
dimensions() {
return document.getElementById(
'canvas-container'
);
},
},
...
}
<div id="canvas-container">
<canvas
v-bind:id="id"
:height="dimensions.clientHeight"
:width="dimensions.clientWidth"
></canvas>
</div>
But the issue is that I'm getting an undefined error like:
cannot read the property clientHeight of null.
How can I avoid this.?

The canvas-container element doesn't exist yet when the dimensions computed property accesses it. The first render pass generates the virtual DOM but no actual DOM nodes are created yet.
You need to defer accessing the element until the component has mounted and the DOM element will exist.
There's no use using a computed property here since the DOM element is not reactive (it won't automatically update when the element resizes).
If possible, use a ref instead of using getElementById.
new Vue({
el: '#app',
data: {
width: 0,
height: 0,
},
mounted() {
// Element is now available
const el = this.$refs.el
this.width = el.clientWidth
this.height = el.clientHeight
}
})
#app {
border: 1px solid black;
width: 200px;
height: 100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" ref="el">
Element size: {{ width }} x {{ height }}
</div>
If you need the dimensions to automatically update, you'll have to use some other mechanism for observing size changes such as a window resize listener or ResizeObserver.

Related

Define a variable used in js and css

For example, I want to define a global color a = '#FFF', and reference it in js and css to make sure that there is only one color named a in the project. then when the value of a changed, a in js and css also changed. is that possible in vue?
Hmm... I was thinking about watchers watch in combination with CSS variables.
Maybe something like this?
Whenever a changes, the CSS variable --a changes aswell.
You can actually type in any color format you want. Hex, rgb, rgba...
let v = new Vue({
el: "#app",
data: {
a: "red"
},
watch: {
a(val){
document.documentElement.style.setProperty("--a", val);
}
}
})
:root {
--a: red;
}
#app {
height: 100px;
width:100%;
background: var(--a);
transition: background 500ms;
}
p {
background: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p>Color: {{a}}</p>
<input v-model="a">
</div>

Object value is changed but component is not updated

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.

VueJS: Why parent components method unable to delete/destroy child's child (`vue2-dropzone`) component entirely?

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

Initialize components in for loop from array data

Trying initialize custom elements (3 buttons) in for loop but first element missing text.
LeftMenu.vue
<template>
<div id="left-menu">
<MenuButton v-for="mytext in buttonList" v-bind:key="mytext" v-bind:mytext="mytext"/>
</div>
</template>
<script>
import MenuButton from './components/MenuButton.vue'
export default {
name: 'left-menu',
components: {
MenuButton
},
computed: {
buttonList() {
return ["Test1", "Test2", "Test3"];
}
}
}
</script>
<style>
#left-menu {
width: 200px;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
</style>
MenuButton.vue
<template>
<div id="left-menu-button">
{{mytext}}
</div>
</template>
<script>
export default {
name: 'left-menu-button',
props: {
mytext: String
}
}
</script>
<style>
#left-menu-button {
width: 180px;
height: 50px;
margin-left: 10px;
margin-bottom: 5px;
}
</style>
main.js
import Vue from 'vue'
import LeftMenu from './LeftMenu.vue'
import MenuButton from './components/MenuButton.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(LeftMenu)
}).$mount('#left-menu')
new Vue({
render: h => h(MenuButton)
}).$mount('#left-menu-button');
I am new to vue and still trying to figure out how all part are connected and working together. It just seems very strange that I got 3 buttons but only last two of them have text and first one does not...may be someone can point me to my mistake.
You've assigned an id of left-menu-button to each of your buttons. You've then told Vue to mount something into that id. The first element (i.e. first button) with that id will be treated as the mounting element, which blows away the text.
You should remove the ids from all elements within your templates. The only id should be the one within your HTML file. For styling purposes use classes instead of ids. Then create a single Vue instance (just one call to new Vue, not two) targeting the id of the element inside your HTML file.
It is possible to create multiple Vue instance directly using new Vue but that is rarely necessary. To do that you would need to have multiple target elements within your HTML file.

Referring to properties passes as props in Vue.js components

I've started working on a board game prototype and decided to go with Vue.js. I have some experience with JavaScript and everything was going fine ... until I tried to access a property passed with 'props' in a component.
Here's the whole code:
<!DOCTYPE HTML>
<html>
<head>
<title>Board</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style type="text/css">
#board {
width: 600px;
}
.square { width: 100px; height: 100px; margin: 1px; border: 1px solid grey; display: inline-block; float: left; }
</style>
</head>
<body>
<div id="board">
<square v-for="square in squares"></square>
</div>
<script>
var app = new Vue({
el: '#board',
data: {
squares: []
}
})
const rows = 5
const cols = 5
const reservedLocation = { row: 2, col: 2 }
Vue.component('square', {
props: [
'row',
'col',
'type',
],
template: '<div class="square" v-on:click="logLocation"></div>',
methods: {
logLocation: function() {
console.log(this)
console.log("Location: " + this.col + "x" + this.row )
},
},
})
for (var row=0; row<rows; row++) {
for (var col=0; col<cols; col++) {
const type = (row == reservedLocation.row && col == reservedLocation.col) ? 'reserved' : 'empty'
app.squares.push({ row: row, col: col, type: type })
}
}
</script>
</body>
</html>
What's happening there is the "board" div is filled with the "square" components. Each square component has the 'row', 'col' and 'type' properties, passed to it as 'props'. When the user click on a square, the 'logLocation' function of the corresponding component is called and all that function does is, it logs the 'row' and 'col' properties.
Everything works fine except the message logged is: "Location: undefinedxundefined", in other words, both this.col and this.row seems to be undefined. I've checked 'this', and it seems to be the correct component.
I'm sure it's something obvious but I couldn't find an answer in either the official documentation, in tutorials or even here, on Stack Overflow itself – perhaps I'm not using the correct terms.
A bit of new info: the 'row' and 'col' properties are set on the component object and in the '$props' property but the value they return in 'undefined'. Am I, somehow, passing the parameters incorrectly?
Solution
Turns out, there is a section in the Vue.js documentation dedicated specifically to using 'v-for' with components: "v-for with a Component" and here's the relevant portion of the code:
<div id="board">
<square
v-for="square in squares"
:key="square.id"
:row="square.row"
:col="square.col"
:type="square.type"
></square>
</div>
Huge thanks to Stephen Thomas for pointing me in the right direction!
You've defined the props correctly, and you're accessing the props correctly, but you haven't actually set them to any value. The markup:
<square v-for="square in squares"></square>
doesn't pass the props to the component. Perhaps you want something like
<div v-for="row in rows" :key="row">
<div v-for="col in cols" :key="col">
<square :row="row" :col="col"></square>
</div>
</div>
Try to use
console.log("Location: " + this.$props.col + " x " + this.$props.row )