VueJS observe plugged parameter - javascript

I'm starting with VueJS 2 and I created a simple plugin which adds parameter to Vue instance.
I have problem because when I update this value my computed properties are still same.
My example plugin's code:
export default function (Vue) {
Vue.MyProperty = "test"
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return Vue.MyProperty
},
"set": function (value) {
Vue.MyProperty = value
return this
}
}
})
}
And my component's code
export default {
"computed": {
"test": function () {
return this.$myProperty
}
}
}
When I changed this.$myProperty in other component my component returns vaid value (in example when I changed from "test" into "newvalue" I can see "newvalue") but computed property test is still old value ("test" in my example).
I tried to use this.$set(this, "$myProperty", value) but this still not working.
How can I use or declare this property to use it in computed or watched properties?

The reason the data value is not automatically updated in the computed is because the property you added to Vue, MyProperty is not an observed property. Fundamentally, Vue's reactivity works because all values added to data are converted into observed properties; under the hood they are converted into getter/setter pairs with some additional code so that when one of those properties changes, Vue knows to propagate the changes to all the things that depend on it's value.
The code in the question, however, just adds a normal property to the Vue object. You can change it, but it's not reactive.
That said, it's relatively easy to make it reactive. I cover how to do this in the comments to my answer here. Basically, instead of adding your property to Vue, just create a new Vue object (which has very low overhead) and make the property you want to be reactive a property of that Vue. Here is a working example.
console.clear()
function MyPlugin(Vue) {
let store = new Vue({data:{MyProperty: "some value"}})
Object.defineProperties(Vue.prototype, {
"$myProperty": {
"get": function () {
return store.MyProperty
},
"set": function (value) {
store.MyProperty = value
return this
}
}
})
}
Vue.use(MyPlugin)
const MyComponent = {
template:`<div>{{test}}</div>`,
"computed": {
"test": function () {
return this.$myProperty
}
}
}
new Vue({
el: "#app",
components:{
MyComponent
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<my-component></my-component>
<button #click="$myProperty = 'new value'">Change</button>
</div>

Related

Why won't my template update with it being bound to a computed property?

I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.
As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.
Template HTML:
<template>
...
<v-text-field
v-model="getNumSets"
placeholder="S"
type="number"
outlined
dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>
Component Logic:
<script>
...
computed: {
getNumSets: {
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
...
</script>
Vuex Store Logic:
...
state: {
designerBucket: []
},
getters: {
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
state.designerBucket.forEach(exercise => {
if (exercise.id == payload.id) {
exercise.numSets = payload.numSets
}
})
}
}
Any insight is very appreciated!
P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.
This turned into a bit of a long-winded answer, but bear with me.
Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.
Reactivity for Arrow Function Getters
One of the things to keep in mind when creating a getter like this, where you return an arrow function:
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
...is that you're no longer returning actual state data from your getter.
This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.
BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.
Computed Caching and Dependency Detection
In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.
As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.
The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
The getter: this.$store.getters['designer/getNumSetsForExercise']
this.id
this.parent
None of these values change when <v-text-field> calls your setter.
This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.
How to Fix it?
Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.
In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:
...
state: {
designerBucket: {}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
// Need to use $set since we're adding a new property to the object
Vue.set(state.designerBucket, payload.id, payload.numSets);
}
}
Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:
<script>
...
computed: {
getNumSets: {
get() {
return this.$store.state.designerBucket[this.id];
},
set(value) {
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
});
}
}
...
</script>
This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.
Edited: First import mapGetters from 'vuex' like this on the top of the script tag.
import { mapGetters } from "vuex"
Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-
computed: {
...mapGetters('designer',['getNumSetsForExercise']),
getNumSets: {
get() {
var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
And see if it works.

Vue Computed Property not updating. Very strange behaviour

Yes, it's another 'Vue computed property is not updating question...
Below is an excerpt of my component with the issue. I have a computed property 'fieldModel' this uses Vue.set to set a new value, then i console log that computed property immediately after assigning it a new value the javascript object updates and is viewable in devtools, the computed property however has not updated, and neither has the DOM.
export default {
props:{
value:{
type:Object,
required:true,
}
},
data() {
return {
model:this.value,
key:'something',
}
},
created() {
var self = this;
setTimeout(function() {
self.fieldModel = 'Apples';
}, 1000);
},
computed:{
fieldModel:{
get() {
return this.model[this.key];
},
set(value) {
var self = this;
self.$set(self.model, self.key, value);
console.log(self.model[self.key], self.fieldModel);
//Logs out 'Apples', undefined,
}
}
}
}
The example code i posted in the original question works correctly, This lead me to break down my code and resolve my issue.
I had this component in a v-for loop with recursive nesting, Another component appeared to mutate the v-model object without updating these components resulting in some very strange behaviour.
I was able to solve the problem by adding a watcher for 'value' to update the model field and a watcher for 'model' to $emit('input') any changes to the model to it's parent.
This results in an infinite loop that crashes the browser, however i was able to resolve that by adding a check to see if the model/value is the same object
Example code is simplified for brevity:
{
watch:{
value(newValue) {
if(this.model != newValue) {
this.model = newValue;
}
},
model(newModel) {
this.$emit('input', newModel)
},
}
}

Vue.js 2: Cant useTwo-way Computed Property in combination with vuex

I cant get Two-way Computed Property in combination with vuex to work.
If there are input changes I want to set getIsUnsavedData to true and "copy"/commit the changes into a new variable $store.state.authenticatedUser.changedData. After there is any change I want the input to get() its value from $store.state.authenticatedUser.changedData instead of $store.state.authenticatedUser.data to display the change.
At fist everything works like expected. If there are input changes, the changed value will be replicated in the $store.state.authenticatedUser.changedData property. The getIsUnsavedData value changes to true and the get() points to the replicated data.
There is only one bug left. Suddenly the computed property never changes although the vuex store is updating correctly. The set() function still gets called, but the get() doesn't .
<ui-textbox #change="valueChanged" v-model="userName"></ui-textbox>
// ...
computed: {
userName: {
get() {
return this.$store.getters.getIsUnsavedData ? this.$store.state.authenticatedUser.changedData.name : this.$store.state.authenticatedUser.data.name
},
set(value) {
this.$store.commit('setChangedUserData', {
key: 'name',
value: value
})
}
}
},
methods: {
valueChanged() {
this.$store.commit('setUnsavedState', true)
}
},
// ....
Try to use mine library
https://github.com/yarsky-tgz/vuex-dot
it's done for such situations, to make code footprint of setters/getters in your code much less.
<template>
<form>
<input v-model="name"/>
<input v-model="email"/>
</form>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
...takeState('user')
.expose(['name', 'email'])
.dispatch('editUser')
.map()
}
}
</script>
(updated to reflect Andor's input)
v-model can point to a computed property but it needs to handle work a bit differently than when it just refers to data.
vue's doc
applying the section on two-way computed properties:
<ui-textbox v-model="userName"></ui-textbox>
// ...
computed: {
userName: {
get () {
return this.$store.state.obj.userName
},
set (value) {
this.$store.commit('updateUserName', value)
}
}
}
I might be missing something about what you're doing but this is how I'd start to solve the problem.

Unable to access props values in data method in vuejs

I have the code (vuejs2) -
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
return { totalCompetetions: this.values.length}
}
})
Nothing is printed on the page but if I change the template value to
template: `<div>{{this.values.length}}</div>`
it prints 15. What am I doing wrong and how can I pass the props to the data?
Any help is much appreciated.
I was unable to assign the prop values to data totalCompetetions in the following way -
data: function () {
return { totalCompetetions: this.values.length}
}
But I was able to do it using the watch, computed, and methods properties.
With watch property -
watch: {
values: function(){
this.totalCompetitions = this.values;
}
}
With computed property -
computed:{
competition:{
get: function(){
return this.values.length;
}
}
With methods property -
methods:{
competitionn: function(){
return this.values.length;
}
}
But for computed and methods properties, I needed to set totalCompetetions in the following way -
For computed -
template: `<div><p>{{totalCompetitions = competition}}</p></div>` //without parenthesis
For methods -
template: `<div><p>{{totalCompetitions = competition()}}</p></div>` //with parenthesis
You code does work.
I guess the problem is your parent component. Did you pass the values correctly? for example:
<competetion-list :values="[1, 2, 3]"></competetion-list>
Besides, for your case I'd say computed properties is a better solution.
computed: {
totalCompetetions () {
return this.values.length
}
}
From the data() method, you should be able to reference the component's properties using this.
Try following:
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
var data = { totalCompetetions: this.values.length}
return data
}
})
As validly mentioned in the comment, if values array is changing later, you may have to put a watcher on the prop and inside watcher, set totalCompetetions as this.values.length.

What's the correct way to pass props as initial data in Vue.js 2?

So I want to pass props to an Vue component, but I expect these props to change in future from inside that component e.g. when I update that Vue component from inside using AJAX. So they are only for initialization of component.
My cars-list Vue component element where I pass props with initial properties to single-car:
// cars-list.vue
<script>
export default {
data: function() {
return {
cars: [
{
color: 'red',
maxSpeed: 200,
},
{
color: 'blue',
maxSpeed: 195,
},
]
}
},
}
</script>
<template>
<div>
<template v-for="car in cars">
<single-car :initial-properties="car"></single-car>
</template>
</div>
</template>
The way I do it right now it that inside my single-car component I'm assigning this.initialProperties to my this.data.properties on created() initialization hook. And it works and is reactive.
// single-car.vue
<script>
export default {
data: function() {
return {
properties: {},
}
},
created: function(){
this.data.properties = this.initialProperties;
},
}
</script>
<template>
<div>Car is in {{properties.color}} and has a max speed of {{properties.maxSpeed}}</div>
</template>
But my problem with that is that I don't know if that's a correct way to do it? Won't it cause me some troubles along the road? Or is there a better way to do it?
Thanks to this https://github.com/vuejs/vuejs.org/pull/567 I know the answer now.
Method 1
Pass initial prop directly to the data. Like the example in updated docs:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
But have in mind if the passed prop is an object or array that is used in the parent component state any modification to that prop will result in the change in that parent component state.
Warning: this method is not recommended. It will make your components unpredictable. If you need to set parent data from child components either use state management like Vuex or use "v-model". https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
Method 2
If your initial prop is an object or array and if you don't want changes in children state propagate to parent state then just use e.g. Vue.util.extend [1] to make a copy of the props instead pointing it directly to children data, like this:
props: ['initialCounter'],
data: function () {
return {
counter: Vue.util.extend({}, this.initialCounter)
}
}
Method 3
You can also use spread operator to clone the props. More details in the Igor answer: https://stackoverflow.com/a/51911118/3143704
But have in mind that spread operators are not supported in older browsers and for better compatibility you'll need to transpile the code e.g. using babel.
Footnotes
[1] Have in mind this is an internal Vue utility and it may change with new versions. You might want to use other methods to copy that prop, see How do I correctly clone a JavaScript object?.
My fiddle where I was testing it:
https://jsfiddle.net/sm4kx7p9/3/
In companion to #dominik-serafin's answer:
In case you are passing an object, you can easily clone it using spread operator(ES6 Syntax):
props: {
record: {
type: Object,
required: true
}
},
data () { // opt. 1
return {
recordLocal: {...this.record}
}
},
computed: { // opt. 2
recordLocal () {
return {...this.record}
}
},
But the most important is to remember to use opt. 2 in case you are passing a computed value, or more than that an asynchronous value. Otherwise the local value will not update.
Demo:
Vue.component('card', {
template: '#app2',
props: {
test1: null,
test2: null
},
data () { // opt. 1
return {
test1AsData: {...this.test1}
}
},
computed: { // opt. 2
test2AsComputed () {
return {...this.test2}
}
}
})
new Vue({
el: "#app1",
data () {
return {
test1: {1: 'will not update'},
test2: {2: 'will update after 1 second'}
}
},
mounted () {
setTimeout(() => {
this.test1 = {1: 'updated!'}
this.test2 = {2: 'updated!'}
}, 1000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app1">
<card :test1="test1" :test2="test2"></card>
</div>
<template id="app2">
<div>
test1 as data: {{test1AsData}}
<hr />
test2 as computed: {{test2AsComputed}}
</div>
</template>
https://jsfiddle.net/nomikos3/eywraw8t/281070/
I believe you are doing it right because it is what's stated in the docs.
Define a local data property that uses the prop’s initial value as its initial value
https://vuejs.org/guide/components.html#One-Way-Data-Flow
Second or third time I run into that problem coming back to an old vue project.
Not sure why it is so complicated in vue, but it can we done via watch:
export default {
props: ["username"],
data () {
return {
usernameForLabel: "",
}
},
watch: {
username: {
immediate: true,
handler (newVal, oldVal) {
this.usernameForLabel = newVal;
}
},
},
Just as another approach, I did it through watchers in the child component.
This way is useful, specially when you're passing an asynchronous value, and in your child component you want to bind the passed value to v-model.
Also, to make it reactive, I emit the local value to the parent in another watcher.
Example:
data() {
return {
properties: {},
};
},
props: {
initial-properties: {
type: Object,
default: {},
},
},
watch: {
initial-properties: function(newVal) {
this.properties = {...newVal};
},
properties: function(newVal) {
this.$emit('propertiesUpdated', newVal);
},
},
This way I have more control and also less unexpected behaviour. For example, when props that passed by the parent is asynchronous, it may not be available at the time of created or mounted lifecycle. So you can use computed property as #Igor-Parra mentioned, or watch the prop and then emit it.
Following up on Cindy's comment on another answer:
Be carful. The spread operator only shallow clones, so for objects
that contain objects or arrays you will still copy pointers instead of
getting a new copy.
Indeed this is the case. Changes within objects inside arrays will still propagate to your components even when a spread operator is employed.
Here was my solution (using Composition API):
setup() {
properties = ref([])
onMounted(() => {
properties.value = props.initialProperties.map((obj) => ({ ...obj }));
})
}
This worked to set the values and prevent them from getting changed, even if the data was changed in the parent component.

Categories

Resources