How to calculate table height in watch in vue - vue.js

I have a table and I give a ref to it.
<table ref="table"></table>
I want to calculate the height of this table in watcher:
watch: {
buttonClicked: {
immediate: true,
handler() {
this.$nextTick(() => {
const tableElement = this.$refs.table.$el;
const tableOffsetTop = tableElement.getBoundingClientRect().top;
});
},
},
}
But I am getting an error: Uncaught TypeError: Cannot read properties of undefined (reading '$el')
I tried ti fix it with this.$nextTick but this time I cannot calculate it right.
How can I fix it?

Try without $el:
const app = Vue.createApp({
data() {
return {
height: null
}
},
watch: {
buttonClicked: {
handler() {
this.$nextTick(() => {
const tableElement = this.$refs.table;
const tableOffsetTop = tableElement.getBoundingClientRect().top;
this.height = tableElement.getBoundingClientRect().bottom -tableElement.getBoundingClientRect().top
});
},
immediate: true,
},
}
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<table ref="table"><tr><td>table</td></tr></table>
height: {{ height }}
</div>

Related

Vue computed value as data property value

Simplified example component code:
<template>
<section>
<div>{{ z }}</div>
<div>{{ compZ }}</div>
<div>{{ x }}</div>
</section>
</template>
<script>
export default {
name: "example",
data: () => ({
z: false,
x: [{ visible: null }]
}),
mounted() {
this.x[0].visible = this.compZ;
setTimeout(() => (this.z = true), 1e3);
},
computed: {
compZ() {
return this.z;
}
}
};
</script>
After a second results are:
true
true
[ { "visible": false } ]
I need x[n].visible to change when compZ changes. Any ideas on how to cleanly keep reactivity?
This is required, because i have 22 potential steps, that are visible depending on certain flags that can change after initialization.
You can add watcher for your z.
watch: {
z: function (newValue, oldValue) {
// here you can change x.y
}
},
Found a workaround, but i think it's ugly:
<template>
<section>
<div>{{ refFlag1 }}</div>
<div>{{ compRefFlag1 }}</div>
<div>{{ x }}</div>
</section>
</template>
<script>
export default {
name: "example",
data: () => ({
refFlag1: false,
refFlag2: false,
x: [{ visible: null, visibleFunc: "that.compRefFlag1" }]
}),
watch: {
allRelevatFlags: function () {
setTimeout(() => this.updateVisible());
}
},
mounted() {
this.x[0].visible = this.compRefFlag1;
setTimeout(() => (this.refFlag1 = true), 1e3);
},
methods: {
updateVisible() {
// eslint-disable-next-line no-unused-vars
let that = this; // eval doesn't see 'this' scope
this.x.forEach(step => (step.visible = eval(step.visibleFunc)));
}
},
computed: {
allRelevatFlags() {
return `${this.compRefFlag1}${this.compRefFlag2}`;
},
compRefFlag1() {
return this.refFlag1;
},
compRefFlag2() {
return this.refFlag2;
}
}
};
</script>
Watch for changes in any relevant flag and then using JS eval() set the visible flag anew.
There's got to be a better way...

vuejs get ref element height on window resize

I'm trying to get the height of the h1 ref element however it is showing as undefined
how can I get the height of my h1 element?
export default {
data() {
return {
h1: null,
};
},
methods: {
getH1Height() {
console.log(this.h1);
},
},
mounted() {
this.$nextTick(() => {
this.h1 = this.$refs.h1;
});
window.addEventListener('resize', () => {
this.getH1Height();
});
},
};
template.js
<h1
ref="h1">
{{ productTitle }}
</h1>
you can try :
this.$refs.h1.offsetHeight

Updating a prop inside a child component so it updates on the parent container too

So I have a simple template like so:
<resume-index>
<div v-for="resume in resumes">
<resume-update inline-template :resume.sync="resume" v-cloak>
//...my forms etc
<resume-update>
</div>
<resume-index>
Now, inside the resume-updatecomponent I am trying to update the prop on the inside so on the outside it doesn't get overwritten, my code is like so;
import Multiselect from "vue-multiselect";
import __ from 'lodash';
export default {
name: 'resume-update',
props: ['resume'],
components: {
Multiselect
},
data: () => ({
form: {
name: '',
level: '',
salary: '',
experience: '',
education: [],
employment: []
},
submitted: {
form: false,
destroy: false,
restore: false
},
errors: []
}),
methods: {
update(e) {
this.submitted.form = true;
axios.put(e.target.action, this.form).then(response => {
this.resume = response.data.data
this.submitted.form = false;
}).catch(error => {
if (error.response) {
this.errors = error.response.data.errors;
}
this.submitted.form = false;
});
},
destroy() {
this.submitted.destroy = true;
axios.delete(this.resume.routes.destroy).then(response => {
this.resume = response.data.data;
this.submitted.destroy = false;
}).catch(error => {
this.submitted.destroy = false;
})
},
restore() {
this.submitted.restore = true;
axios.post(this.resume.routes.restore).then(response => {
this.resume = response.data.data;
this.submitted.restore = false;
}).catch(error => {
this.submitted.restore = false;
})
},
reset() {
for (const prop of Object.getOwnPropertyNames(this.form)) {
delete this.form[prop];
}
}
},
watch: {
resume: function() {
this.form = this.resume;
},
},
created() {
this.form = __.cloneDeep(this.resume);
}
}
When I submit the form and update the this.resume I get the following:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "resume"
I have tried adding computed to my file, but that didn't seem to work:
computed: {
resume: function() {
return this.resume
}
}
So, how can I go about updating the prop?
One solution:
simulate v-model
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So the steps:
create one prop = value which you'd like to sync to parent component
inside the child component, create one data porperty=internalValue, then uses Watcher to sync latest prop=value to data property=intervalValue
if intervalValue change, emit one input event to notice parent component
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{value}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['value'],
mounted: function () {
this.internalValue = this.value
},
watch: {
value: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<p>{{items}}
<container v-for="(item, index) in items" :key="index" v-model="items[index]">
</container>
</div>
</div>
or use other prop name instead of value (below demo use prop name=item):
Also you can use other event name instead of event name=input.
other steps are similar, but you have to $on the event then implement you own handler like below demo.
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{item}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['item'],
mounted: function () {
this.internalValue = this.item
},
watch: {
item: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
this.$emit('test-input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
},
methods: {
syncChanged: function (target, index, newData) {
this.$set(target, index, newData)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
Event Name=input
<p>{{items}}</p>
<container v-for="(item, index) in items" :key="index" :item="item" #input="syncChanged(items, index,$event)">
</container>
</div>
<hr> Event Name=test-input
<container v-for="(item, index) in items" :key="index" :item="item" #test-input="syncChanged(items, index,$event)">
</container>
</div>
I usually use vuex to manage variables that I will be using in multiple components and like the error says, load them in the various components using the computed properties. Then use the mutations property of the store object to handle changes
In component files
computed: {
newProfile: {
get() {
return this.$store.state.newProfile;
},
set(value) {
this.$store.commit('updateNewProfile', value);
}
},
In the vuex store
state: {
newProfile: {
Name: '',
Website: '',
LoginId: -1,
AccountId: ''
}
},
mutations: {
updateNewProfile(state, profile) {
state.newProfile = profile;
}
}

Computed property "main_image" was assigned to but it has no setter

How can I fix this error "Computed property "main_image" was assigned to but it has no setter"?
I'm trying to switch main_image every 5s (random). This is my code, check created method and setInterval.
<template>
<div class="main-image">
<img v-bind:src="main_image">
</div>
<div class="image-list>
<div v-for="img in images" class="item"><img src="img.image"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Item',
data () {
return {
item: [],
images: [],
}
},
methods: {
fetchImages() {
axios.get(`/api/item/${this.$route.params.id}/${this.$route.params.attribute}/images/`)
.then(response => {
this.images = response.data
})
.catch(e => {
this.images = []
this.errors.push(e)
})
},
},
computed: {
main_image() {
if (typeof this.item[this.$route.params.attribute] !== 'undefined') {
return this.item[this.$route.params.attribute].image_url
}
},
},
watch: {
'$route' (to, from) {
this.fetchImages()
}
},
created () {
axios.get(`/api/item/${this.$route.params.id}/`)
.then(response => {
this.item = response.data
})
.catch(e => {
this.errors.push(e)
})
this.fetchImages();
self = this
setInterval(function(){
self.main_image = self.images[Math.floor(Math.random()*self.images.length)].image;
}, 5000);
},
}
</script>
Looks like you want the following to happen...
main_image is initially null / undefined
After the request to /api/item/${this.$route.params.id}/ completes, it should be this.item[this.$route.params.attribute].image_url (if it exists)
After the request to /api/item/${this.$route.params.id}/${this.$route.params.attribute}/images/ completes, it should randomly pick one of the response images every 5 seconds.
I'd forget about using a computed property as that is clearly not what you want. Instead, try this
data() {
return {
item: [],
images: [],
main_image: '',
intervalId: null
}
},
methods: {
fetchImages() {
return axios.get(...)...
}
},
created () {
axios.get(`/api/item/${this.$route.params.id}/`).then(res => {
this.item = res.data
this.main_image = this.item[this.$route.params.attribute] && this.item[this.$route.params.attribute].image_url
this.fetchImages().then(() => {
this.intervalId = setInterval(() => {
this.main_image = this.images[Math.floor(Math.random()*this.images.length)].image;
})
})
}).catch(...)
},
beforeDestroy () {
clearInterval(this.intervalId) // very important
}
You have to add setter and getter for your computed proterty.
computed: {
main_image: {
get() {
return typeof this.item[this.$route.params.attribute] !== 'undefined' && this.item[this.$route.params.attribute].image_url
},
set(newValue) {
return newValue;
},
},
},

Vue.js can not set options afer setting data

I try to set an option for my Vue component after getting my required data through an API. The data is set correctly when the Vue instance is created but it seems that does not affect my condition.
This is the snippet:
import axios from 'axios';
Vue.component("order-now", {
delimiters: ["${", "}"],
props: {
dynamic: {
type: Boolean,
default: false
},
template: null
},
data() {
return {
order: '',
startInterval: false,
}
},
/**
* created
*/
created() {
this.getOrderNow();
this.$options.template = this.template;
},
mounted() {
if(this.startInterval)
this.$options.interval = setInterval(this.getOrderNow(), 10000);
},
/**
* beforeDestroy
*/
beforeDestroy() {
clearInterval(this.$options.interval);
},
methods: {
/**
* getOrderNow
*
* Receive data from api route
* and store it to components data
*/
getOrderNow() {
axios.get('/rest/order-now').then(({data}) => {
this.order = data.orderNow.order;
this.startInterval = data.orderNow.startInterval;
}).catch(e => {
console.error('Could not fetch data for order string.')
});
}
}
});
I call my getOrderNow() method when the created hook is called. This works fine and my data is set.
As you can see, in the mounted() hook, I try to look if setInterval is set true or false and condionally set an option but setInterval is always false.
I thought that might has been changed after calling my method in the created hook but it does not.
this.startInterval is false because it probably never gets set to true at the time mounted() is applied. The thing is that you set startInterval after the promise returned by axios is resolved, which most likely happens after mounted().
To solve this you can just set interval inside axios.then().
Update after reading a comment (working demo):
const API = {
counter: 0,
getItems() {
return new Promise((fulfill) => {
setTimeout(() => {
fulfill(API.counter++);
})
});
},
};
new Vue({
el: "#app",
data: {
interval: false,
data: '',
},
methods: {
fetchThings() {
API.getItems().then((data) => {
this.data = data;
});
},
},
created() {
this.fetchThings();
this.interval = setInterval(this.fetchThings, 1000);
},
});
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<pre>
{{data}}
</pre>
</div>
And jsfiddle