Why is it possible to have reactive elements added at mount? - vue.js

The documentation for Reactivity in Depth explains why adding new root-level reactive properties to an already created instance is not possible (and how to actually add them via this.$set()).
In that case, why an initially empty object can be updated (and reactive) at mount time, after the instance was intialized? Or does the initialization part includes the mount? (though it is possible to mount an instance manually after the initalization)
new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.myobject = {
"x": 1
}
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
Direct further modifications after the mount are not taken into account, in line with the documentation (this.myobject.y = 2 for instance would not work, while this.$set(this.myobject, "y", 2) will be fine)

The code in your sample does not fall into the change detection caveat because you are not adding a property to myobject you are setting myobject to an entirely new object. Vue has no problem detecting object reference changes.
What Vue cannot detect is adding a property to an object that did not already exist. For example if you did this:
mounted() {
setTimeout(() => {
this.myobject.someNewProperty = "some value"
}, 2000)
}
Vue would not detect the change. Here is your example updated to demonstrate that the DOM never changes after the object is changed.
new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.myobject.someNewProperty = "some value"
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
What the documentation means when it says
Vue does not allow dynamically adding new root-level reactive
properties to an already created instance.
Is that you cannot add another property to the data object after the Vue instance is created. For example this code:
var vm = new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.$set(vm.$data, 'newRootLevelProperty', "some value")
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
Results in the warning
[Vue warn]: Avoid adding reactive properties to a Vue instance or its
root $data at runtime - declare it upfront in the data option.
But you can add properties to nested objects (such as myobject) as long as you use $set.

Related

strange behavior when pointing to localStorage with vue getter/setter

I am getting strange behavior from this code. I want to use getter and setter to point to local storage.
The first time the app is rendered, the items are properly retrieved.
Afterwards, when adding a new item to the list, it is not rendered. Also, when adding an item, it will just swap the value at the index of the previous item, after adding the very first item.
<input type="text" v-model="term" #keyup.enter="submit()" />
<ul>
<li v-for="(item, i) in history" :key="i">{{item}}</li>
</ul>
const app = new Vue({
el: '#app',
data: {
term: '',
},
computed: {
history: {
get() {
return JSON.parse(localStorage.getItem('history')) || [];
},
set(value) {
localStorage.setItem('history', JSON.stringify(value));
},
},
},
methods: {
submit() {
this.history = [...this.history, this.term];
this.term = '';
},
},
});
You can check the code here because SO is not allowing to acces localStorage. Remember to refresh the page once you have added the first item and also to investigate what is happening inside the localStorage.
https://codepen.io/bluebrown/pen/dyMMRKj?editors=1010
This code is a port of some alpinejs project. And there it worked.
That said, I can make it work when I write it like below. However, I got curious now, why above example behaves like that.
const app = new Vue({
el: '#app',
data: {
term: '',
history: [],
},
created() {
this.history = JSON.parse(localStorage.getItem('history')) || [];
},
watch: {
history: {
deep: true,
handler(value) {
localStorage.setItem('history', JSON.stringify(value));
},
},
},
methods: {
submit() {
this.history = [...this.history, this.term];
this.term = '';
},
},
});
The reason why it didn't work is because Vue can only observe plain objects. Native objects like localStorage cannot be observed by Vue. If you put something into local storage, Vue won't know about it and your view will not automatically update to display those changes.
Vue mentions this limitation in the docs for the data object:
The data object for the Vue instance. Vue will recursively convert its properties into getter/setters to make it "reactive". The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior.
These limitations apply to Vue's entire reactivity system, including computed properties.

When is a computed property not reactive?

I have a Vue page which gathers information from external JSON sources, and then uses it to compute a property.
Problem: this property is not reactive (= it is not recomputed when underlying data changes)
<div id="app">
<div v-for="tag in filteredTags">{{tag}}</div>
</div>
<script>
new Vue({
el: "#app",
data: {
tags: {},
allTags: {},
},
computed: {
// provides an Array of tags which are selected
filteredTags() {
let t = Object.keys(this.allTags).filter(x => this.allTags[x])
console.log(t)
return t
}
},
mounted() {
// get source tags
fetch("tags.json")
.then(r => r.json())
.then(r => {
this.tags = r
console.log(JSON.stringify(this.tags))
// bootstrap a tags reference where all the tags are selected
Object.keys(this.tags).forEach(t => {
this.allTags[t] = true
});
console.log(JSON.stringify(this.allTags))
})
}
})
</script>
The file which is fetched ({"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}) is correctly processed in mounted(), the output on the console is
{"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}
{"tag1":true,"post":true}
filteredTags is however empty. On the console, I see it displayed (as []) right at the start of the processing of the page, which is initially fine (first compute, when allTags is empty), but is then not computed anymore when allTags changes (after tags.json is fetched, processed and allTags correctly updated).
Why isn't this reactive?
Vue isn't reactive to properties that didn't exist when the object was added to data
Since your tag and allTags are empty objects with no properties (yet), any properties added after aren't reactive automatically.
To solve this you have to use the Vue.Set or this.$set functions provided by Vue.
the Set function accepts the values this.$set(object, key, value)
new Vue({
el: "#app",
data: {
tags: {},
allTags: {},
},
computed: {
// provides an Array of tags which are selected
filteredTags() {
let t = Object.keys(this.allTags).filter(x => this.allTags[x])
return t
}
},
mounted() {
const r = '{"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}';
const rJson = JSON.parse(r);
// You shouldn't need to use $set here as you replace the entire object, instead of adding properties
this.tags = rJson;
Object.keys(this.tags).forEach(t => {
// Change this
//this.allTags[t] = true;
// To this
this.$set(this.allTags, t, true);
});
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
<div v-for="tag in filteredTags">{{tag}}</div>
</div>

Vuex add new array property to an object and push elements to that object inside mutation in a reactive manner

According to the Official docs, vuex mutations has some limitation in reactivity.
When adding a new property to an object we have to do,
Vue.set(obj, 'newProp', 123)
This is fine. But how do we add new array property and push elements to that without breaking the reactivity?
This is what I have done up to now. This is working fine but the problem is this is not reactive. Getters can't recognize the changes happen to the state.
set_add_ons(state, payload) {
try {
if (!state.tour_plan[state.tour_plan.length - 1].add_ons) {
state.tour_plan[state.tour_plan.length - 1].add_ons = [];
}
state.tour_plan[state.tour_plan.length - 1].add_ons.push(payload);
} catch (error) {
console.log("ERR", error)
}
},
How do I convert this to a code which is reactive?
You can use Vue.set to create the add_ons attribute to make it reactive; It should work for array too:
Vue.set(state.tour_plan[state.tour_plan.length - 1], 'add_ons', []);
Demo:
new Vue({
el: '#app',
data: {
items: [{ id: 0 }]
},
methods: {
addArray () {
if (!this.items[this.items.length-1].add_on)
this.$set(this.items[this.items.length - 1], 'add_on', [])
this.items[this.items.length - 1].add_on.push(1)
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="addArray">
Add 1 to add_on create if not exists
</button>
<div v-for="item in items">add_on content: {{ item.add_on }}</div>
</div>

How vue use getter setter on v-model?

<div id="app">
<input v-model="msg"/>
<p>{{ msg }}</p>
</div>
<script>
class A{
}
A.a = 1
new Vue({
el: '#app',
data: {
},
computed: {
msg: {
cache: false,
set: function(val){
A.a = val
},
get: function(){
return A.a
}
}
}
})
</script>
run on jsfiddle
How vue use getter setter on v-model? I tried use getter and setter on v-model, but it didn't work.
Your getters and setters are fine as is. (They're not strictly necessary in this example, since they're not doing anything to modify the user input, but I assume that's a simplification for the purposes of your question.)
There are two separate issues with your code:
the input field is outside the Vue root node, so the framework can't see it. [You corrected this in a late edit to the question.]
You're defining your data (A.a) outside of Vue, so the framework doesn't know to watch it for changes.
For the framework to be reactive to changes you must put the variable A in the data block of the component (and, if you really need an external variable, copy the updated value into it using the setter function).
new Vue({
el: '#app',
data: {
A: { a: 1 } // <-- your external variable, moved to where Vue can see it
},
computed: {
msg: {
set: function(val) {
this.A.a = val;
// If necessary, also copy val into an external variable here
},
get: function() {
return this.A.a
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<input v-model="msg" />
<p>{{ msg }}</p>
</div>
First of all, your input has to be inside the #app element. Yours is currently not even being watched by Vue instance.
<div id="app">
<input v-model="msg"/>
<p>{{ msg }}</p>
</div>
Also, your A.a = 1 doesn't do anything. If you console.log A's value you won't see a anywhere. Instantiate A and add a variable in it's constructor:
class A {
constructor(a) { this.a = a}
}
let myA = new A(0)
with Vue instance like this it will work:
new Vue({
el: '#app',
data: {
a: myA.a = 1
},
computed: {
msg: {
set: function(val) {
this.a = val
},
get: function() {
return this.a
}
}
}
})
However, I'd move class instantiation to data:
data() {
return {
a: new A(1).a
}
},
If you keep a outside of data your setter will work and update the value, but your getter will not since variables outside of Vue instance aren't being observed.
The code to implement a model in vue is simple as:
var v1 = new Vue({
el:'#vue1',
data:{
msg:'demo'
}
});
And the html as:
<div id='vue1'>
<input type='text' v-model='msg' />
<p>
{{msg}}
</p>
</div>
The first problem is the scope. Since in your Vue instance you are providing the element id as #app, all the vue related markup should be inside an element with id app, in your case the div.
Second, the way you save the data, once you use v-model directive, it directly observes the changes in your model and make changes to the dom accordingly. You do not need the getter and setter methods.
Lastly, what was the code about the class A??
Please look into the the javascript manuals because it is well outside the scope of this question to explain all of that part in detail.
Here is the updated fiddle

Access Vue instance's computed properties object

Vue components exposes this.$data. Is there any way to access computed properties in a similar fashion?
They are not exposed on $data, and there is no such thing as this.$computed
There's no built-in way to access an object with the computed properties of a Vue instance.
If you really want an object of computed property name and values for testing purposes you could define your own $computed property using the information in the _computedWatchers property. This might be finicky and I wouldn't use it in production code.
Object.defineProperty(Vue.prototype, '$computed', {
get() {
let computed = {};
Object.keys(this._computedWatchers).forEach((key) => {
computed[key] = this._computedWatchers[key].value;
})
return computed;
}
})
new Vue({
el: '#app',
data() {
return {
foo: 1,
}
},
computed: {
bar() {
return this.foo * 2;
}
},
mounted() {
console.log(this.$computed)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">{{ bar }}</div>