What is the recommended way to have a computed property react to DOM property changes?
My scenario is this:
I have a computed property that is based on values found in DOM (e.g element width).
The DOM property changes as a result of the view update.
Now the computed property doesn't update as a result of DOM change.
I have made a codepen with a simplified example to illustrate the issue. In the example there I am forcing the computed property to update using a 'dummy' property that changes in the 'updated' hook.
updated(){
console.log('updated')
this.dummy= this.dummy * -1
},
computed:{
width(){
console.log('computed')
const w = (this.$refs.square || {}).clientWidth
return w + this.dummy * 0
}
}
I do understand that if a property updates reacting to DOM changes, and DOM updates reacting to property changes, well, there would be no end to it.
So, is there any clean way to keep the two sync'd?
Thank you
You wouldn't normally rely on DOM properties; you would normally let the DOM be completely determined by data. That way you wouldn't have this issue.
That said, at the cost of an additional render, you could keep your properties in sync by using $nextTick.
data:{
size:20,
width: 20
},
methods:{
onInput(e){
this.size =e.target.value
this.$nextTick(() => this.width = (this.$refs.square || {}).clientWidth)
}
}
And just eliminate your computed entirely.
Updated pen.
Related
I have made some sandbox code of my problem here:
https://codesandbox.io/s/clever-zeh-kdff1z
<template>
<div v-if="started">
<HelloWorld :msg="msg" #exit="exit" #remake="remake" />
</div>
<button v-if="!started" #click="started = !started">start</button>
</template>
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
data() {
return {
started: false,
msg: "Hello Vue 3 in CodeSandbox!",
};
},
methods: {
exit() {
this.started = false;
},
remake() {
this.msg = this.msg + 1;
//this code should recreate our child but...
this.exit();
this.started = true;
// setTimeout(() => {
// this.started = true;
// });
},
},
};
</script>
So! We have 2 components parent and child. The idea is simple - we have a flag variable in our parent. We have a v-if statement for this - hide / show an element depend on the flag value "false" or "true". After we toggle the flag - the child component should be recreated. This is the idea. Simple.
In our parent we have a button which will set the flag variable to "true" and our child will be created and will appear on our page.
Ok. Now we have 2 buttons inside our child.
One button is "exit" which is emit an event so the flag variable of parent will set to "false" and the elemint will disappear from our page(It will be destroyed btw). Works as charm. Ok.
The second button "remake". It emit event so the flag variable will be just toggled (off then on). Simple. We set to "false", we set to "true". So the current child should dissapear, and then imediatly will be created new one.
But here we are facing the problem! Ok, current child is still here, there is no any recreation, it just updates current one... So in child I have checked our lifecycle hooks - created and unmounted via console.log function. And the second button dont trigger them. Start->Exit->Start != Start->Remake.
So can anyone please explain me why this is happening? I cant figure it out.
Interesting thing, if you can see there is some asynchronous code commented in my demo. If we set our flag to "true" inside the async function the child will be recreated and we will see the created hook message but it seems like crutch. We also can add a :key to our component and update it to force rerender, but it also seems like a crutch.
Any explanations on this topic how things work would be nice.
Vue re-uses elements and components whenever it can. It will also only rerender once per tick. The length of a 'tick' is not something you should worry yourself about too much, other than that it exists. In your case the this.exit() and this.started = true statements are executed within the same tick. The data stored in this.started is both true in the last tick and the current tick as it does not end the tick in between the statements, and so nothing happens to your component.
In general you should think in states in Vue rather than in lifecycles. Or in other words: What are the different situations this component must be able to handle and how do you switch between those states. Rather than determining what to do in which point in time. Using :key="keyName" is indeed generally a crutch, as is using import { nextTick } from 'vue'; and using that to get some cadence of states to happen, as is using a setTimeout to get some code to execute after the current tick. The nasty part of setTimeout is also that it can execute code on a component that is already destroyed. It can sometimes help with animations though.
In my experience when people try to use lifecycle hooks they would rather have something happen when one of the props change. For example when a prop id on the child component changes you want to load data from the api to populate some fields. To get this to work use an immediate watcher instead:
watch: {
id: {
handler(newId, oldId) {
this.populateFromApi(newId);
},
immediate: true
}
}
Now it will call the watcher on component creation, and call it afterwards when you pass a different id. It will also help you gracefully handle cases where the component is created with a undefined or null value in one of the props you expect. Instead of throwing an error you just render nothing until the prop is valid.
I'm trying to recreate the doughnut to pie change in behavior as seen here:
https://www.chartjs.org/samples/latest/scriptable/pie.html
I'm using VueJS version of Chart JS and after recreating this it seems to not be reactive at all.
Here is the method that I use to change the chart to the other one:
togglePieDoughnut() {
this.options.cutoutPercentage = 50;
}
As you can see it does not work as intended, even tough I used reactiveprop mixin.
EDIT: To be precise I want to recreate the chart update behaviour as seen in the example on chartjs.org website. I do not want to rerender the chart, rather update it so the transition remains smooth.
Seems like the issue is the template isn't reacting to the data changes. Best way to force template re-render is to bind a key, for our example, we are changing this value, the template will update when its changed:
:key="options.cutoutPercentage"
Codepen example:
https://codesandbox.io/s/vue-chartjs-demo-t8vxu?file=/src/App.vue
What if we add a watch to the options variable and rerender the chart when it happens?
watch: {
options: function() {
this._chart.destroy();
this.renderChart(this.donut, this.options);
}
}
When you look at the code of Piechart.vue, it seems that it only render one time on mounted. Thats why when changing the options, it not gonna reflected in the chart because there is no function to rerender.
The only way is you have to remove the old pie chart and create a new one when options changed. There's a lot of way to do the force re-render, but still the cleanest way is as procoib said, attach a key to it.
When using object as props and update one property in it, the reactivity system will not trigger the change, because the object is the same and only one property updated. Thus, the child component will not get the updated value.
What can you do is to recreated the object with the updated property. See below code:
this.options = Object.assign({}, this.options, { cutoutPercentage: 50 });
And in the child component, use watcher re-render the chart
watch: {
options(newVal) {
}
}
I'm trying to use v-radio-group in conjunction with computed values from Vuex as described similarly here.
Example codepen of the issue I'm facing is here
Whenever a radio button is clicked, a Vuex mutation is called to save the selected value in the state.
However it can be the case that some validation fails inside the mutation and that therefore the value is not changed in the state as expected.
Regardless of what value ends up in the Vuex state, the radio buttons do not truly reflect the current state.
E.g. in the codepen snippet I'd expect the second option (Option 1) never to show as chosen, as the corresponding state is always 0.
As far as I can see this behavior is not only happening when using v-radio-groups.
It happens with all Vuetify components using v-model and computed getters/setters.
So e.g. Vuetifys v-text-input/v-text-field and v-select also show the same behavior.
To sum it up, my questions are the following:
Why is the second option in my codepen example getting selected even as the corresponding state is different?
How can I achieve the expected result (Having Option 1 never shown as selected, even when it is clicked)?
As far as I know Vuetify keeps its own state in their components like v-radio-group.
To change it you need to send updated props. Then it will react and update its own state.
The trouble is that you are performing validation in a mutation. Which is a bad practice in my opinion.
I will show you how to "block" changing state and update v-radio-group so its own state corresponds to what is actually in your $store.state.radioState.
And I will spend some more time to figure out how to performe it in on mutation ;-)
This is not a perfect solution >> my codepen
Your mutation just updates the state.
// store.js
mutations: {
setRadioState (state, data) {
state.radioState = data;
},
},
Your set method do the validation.
// component
computed: {
chosenOption: {
get () {
return this.$store.state.radioState;
},
set (value) {
if (value !== 1) {
this.$store.commit('setRadioState', value)
} else {
const oldValue = this.$store.state.radioState
this.$store.commit('setRadioState', value)
this.$nextTick(() => {
this.$store.commit('setRadioState', oldValue)
})
}
}
}
}
What happens in the set when it fails validation? You save current state to oldValue, you update state so it corresponds to v-radio-group component. And in the $nextTick you change it right back to oldValue. That way v-radio-group gets updated props and change its state to yours.
I'm creating a component that updates props with values from local storage. The props are objects with multiple boolean properties (e.g. this.globalStates.repeat = false). Because I have more than one prop to update, I've created a method for any prop provided as an argument:
mounted(){
this.loadLocalData(this.globalStates, "states")
this.loadLocalData(this.globalSettings, "settings")
},
methods: {
loadLocalData(object, localVariable){ // 'object' receives a prop, 'localVariable' must refer to a string in localStorage (localStorage stores strings only).
const loadedObject = JSON.parse(localStorage.getItem(localVariable)) // Turn localStorage string into JSON object
if (loadedObject && typeof loadedObject === "object"){
for (let item in object){ // iterate through the prop and update each property with the one in loadedObject
object[item] = loadedObject[item] // Why does this work!?
}
}
}
},
Now this actually works, without errors or warnings. I don't understand why though. Normally when I try to modify a prop directly, Vue throws a warning at me:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
Instead of object[item] = loadedObject[item] I tried the following, but that actually failed:
const u = 'update:' + object + '.' + item
this.$emit(u, loadedObject[item] )
What would be the correct way to do this?
The correct way would be to not sync objects, because sync does not support that: https://github.com/vuejs/vue/issues/6241
Alternative solutions are:
Put the method in the parent
Sync each property as a separate prop
Use a data store: https://v2.vuejs.org/v2/guide/state-management.html
I have a computed property - playerId that is referencing an external object that is not updating.
For example:
var player = require('players');
computed: {
playerId: function() {
return player.id
}
}
If I run this call in a click event it outputs the correct ID. However when this gets updated - computed on its own doesn't update. How do I get this to work? Or am I going about this incorrectly?
Computed Property in Vue.js does not get reactive with the data which doesn't have any reactive dependecy. For example, the code below would not be updated in each time, because Date.now() has no reactive dependency. The result of this computed value gets cached and it always returns the same value as the one returned at the first time.
computed: {
now: function () {
return Date.now()
}
}
Then, you probably would like to know how to make your external object reactive. The point is, you better put your object into data section in order to track the change of it with Vue's internal watcher. If your computed property has even one a reactive dependency such referencing data or calling method, it is notified every time the dependency get updated and the content returned from the computed property is going to be updated as well.
https://v2.vuejs.org/v2/guide/computed.html