When is a computed property not reactive? - vue.js

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>

Related

How watch global variable in component vuejs?

I need global variables for errors. But I don't want set input variable for every component.
How I can watch $errors in component ABC without input variable?
(without <abc :errors="$errors"></abc>)
index.js:
Vue.prototype.$errors = {};
new Vue({
el: '#app',
render: h => h(App),
}
App.vue:
...
name: 'App',
components: {
ABC
}
...
methods:{
getContent() {
this.$errors = ...from axis...
}
Component ABC:
<template>
<div>{{ error }}</div>
</template>
...
watch: {
???
}
Here's an example of how it could be done:
const errors = Vue.observable({ errors: {} })
Object.defineProperty(Vue.prototype, '$errors', {
get () {
return errors.errors
},
set (value) {
errors.errors = value
}
})
new Vue({
el: '#app',
methods: {
newErrors () {
// Generate some random errors
const errors = {}
for (const property of ['name', 'type', 'id']) {
if (Math.random() < 0.5) {
errors[property] = 'Invalid value'
}
}
this.$errors = errors
}
}
})
new Vue({
el: '#app2',
watch: {
$errors () {
console.log('$errors has changed')
}
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<pre>{{ $errors }}</pre>
<button #click="newErrors">New errors</button>
</div>
<div id="app2">
<pre>{{ $errors }}</pre>
</div>
I've created two Vue instances to illustrate that the value really is shared. Clicking the button in the first instance will update the value of $errors and the watch is triggered in the second instance.
There are a few tricks in play here.
Firstly, reactivity can only track the reading and writing of properties of an observable object. So the first thing we do is create a suitable object:
const errors = Vue.observable({ errors: {} })
We then need to wire this up to Vue.prototype.$errors. By defining a get and set for that property we can proxy through to the underlying property within our observable object.
All of this is pretty close to how data properties work behind the scenes. For the data properties the observable object is called $data. Vue then uses defineProperty with get and set to proxy though from the Vue instance to the $data object, just like in my example.
as Estradiaz said:
You can use Vuex and access the value outside of Vue like in this answer: https://stackoverflow.com/a/47575742/10219239
This is an addition to Skirtles answer:
You can access such variables via Vue.prototype.variable.
You can set them directly, or use Vue.set, it works either way.
My code (basically the same as Skirtless):
main.js
const mobile = Vue.observable({ mobile: {} });
Object.defineProperty(Vue.prototype, '$mobile', {
get() { return mobile.mobile; },
set(value) { mobile.mobile = value; }
});
function widthChanged() {
if (window.innerWidth <= 768) {
if (!Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', true);
} else if (Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', false);
}
window.addEventListener("resize", widthChanged);
widthChanged();
Home.vue:
watch: {
'$mobile'(newValue) {
// react to Change in width
}
}

Do all properties need to be present on an object data property in vue?

Say I start out like this:
data() {
return {
user: {}
}
}
And then later on... I make an API call:
let response = await this.axios.get("/api/users/1.json");
this.user = response.data
Is this the proper way to assign the data from my API call to this.user?
Do all of the properties of user need to be defined first?
No, you don't have to declare all properties of user. Just having user in the data with any value different from undefined will suffice.
When the property is in the data, Vue will replace it with a getter and a setter. When you do:
this.user = newValue;
You are actually calling a setter that will map all properties of the newValue into getters themselves.
In the official docs, you can find more info on the Reactivity in Depth page.
Have a look below. Check the demo and the explanation in the image that shows the object that was created (and printed in the console).
var app = new Vue({
el: '#app',
data: {
other: null,
user: null
},
mounted() {
this.user = {
name: 'bob',
age: 10
}
}
})
console.dir(app)
<script src="https://unpkg.com/vue"></script>
<div id="app">
<p>{{ other }}</p>
<p>{{ user }}</p>
</div>
No you dont need to put properties in it, you can do it like this. You create a method and make a axios inside it:
data() {
return {
user: {}
}
},
methods: {
axiosCall: function (){
axios.get("/api/users/1.json").then(response =>{
this.user = response.data
})
}
}

'this' context in functional component's child event handler

I am trying to create custom event handlers for child components/elements of the functional component. The problem is that when using a render() function to create the child components, I cannot access their this context.
Suppose we have the following functional component:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
this.$emit('input', data)
}
for (const prop of Object.keys(context.props.value)){
const child = createElement('input', {
props: {
value: context.props[prop]
},
on: {
input: function(event){
// 'this' is not binded here - it is undefined,
// hence the inputHandler() function is
// rising an error
inputHandler.apply(this, [event.target.value, prop])
}
}
})
template.push(child)
}
return template
}
}
Is it possible to access this context for a vnode, when creating event handler this way?
P.S. Use case info: I want to implement a component that automatically generates <input> elements for a resource and uses two-way binding through v-model directive. I also want to use it in <table> where wrapping in <td> will be required, thus I made the component functional.
Functional components don't a have a "this", because there is no Vue instance for them. This makes them lightweight.
This also means emiting events from them is kind of harder, since you need to implement Vue's logic yourself.
Lacking an instance doesn't mean you cannot events, instead, you need to manually parse context.listeners and call the event handler manually. In the case of v-model, you need to call the input listener:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop, handler){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
// Call handler directly instead of using this.$emit
handler(data)
}
for (const prop of Object.keys(context.props.value)){
console.log(context.props.value, prop)
const child = createElement('input', {
// Small bug fixes in the following section:
domProps: {
value: context.props.value[prop]
},
// End bug fixes
on: {
input: function(event){
// pass `context.listeners.input` instead of binding here
inputHandler(event.target.value, prop, context.listeners.input)
}
}
})
template.push(child)
}
return template
}
}
new Vue({
el: "#app",
components: {
Aggregate
},
data: {
test: {
key1: "val1",
key2: "val2",
}
},
})
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<aggregate v-model="test"></aggregate>
<pre>{{ test }}</pre>
<button #click="test = {...test, ping: 'pong'}">Add key</button>
</div>

Why is it possible to have reactive elements added at mount?

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.

Computed object not updated using sync modifier

I have a computed property that simply formats the date:
computed: {
items() {
return this.licenseItems.map(function(license) {
license.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return license;
});
}
}
I pass licenseItems to a form component with the .sync modifier and emit update:field event from the form. In vue dev tools, I can see that licenseItems (data) is properly updated, but items (computed) is still showing the old data so no re-computation was performed.
I have noticed that if I remove the map and just return the licenseItems object from the computed property, it is updated. Is there some issue with Vue's computed property on mapped objects when using the sync modifier?
You should be aware that you're modifying underlying objects in your computed. From your fiddle:
computed: {
mapped: function() {
return this.items.map(function(item) {
let original = item.date;
item.date = moment(item.date).format('MM/DD/YYYY');
console.log(original + ' => ' + item.date);
return item;
});
}
}
Your incrementDates function also modifies the underlying objects directly.
Because each element of items is an object, item is a reference to that same object, so your routine updates the members of items itself. If you intend to modify the object values, you should use a watch instead of a computed. If you want to have a proper computed that does not modify data items, you need to deep copy the objects.
In my example below, you can see that the data value and the computed value are distinct, but the computed is based on the data. Also, the update event works with sync to update the value in the parent, rather than the value being updated directly in the component. You can enter any format of date that Date understands to set the value.
new Vue({
el: '#app',
data: {
licenseItems: [{
expires_at: Date.now()
}]
},
computed: {
items() {
return this.licenseItems.map(function(license) {
const newItem = Vue.util.extend({}, license);
newItem.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return newItem;
});
}
},
components: {
myUpdater: {
props: ['items'],
methods: {
doUpdate(event, index) {
const newObj = this.items.map(item => Vue.util.extend({}, item));
newObj[index].expires_at = new Date(event.target.value);
console.log("new object", newObj);
this.$emit('update:items', newObj);
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<my-updater :items.sync="licenseItems" inline-template>
<div>
<input v-for="item, index in items" :value="item.expires_at" #change="doUpdate($event, index)">
</div>
</my-updater>
<div v-for="item in items">
{{item.expires_at}}
</div>
</div>