VueJS $set not making new property in array of objects reactive - vue.js

In my VueJS 2 component below, I can add the imgdata property to each question in the area.questions array. It works - I can see from the console.log that there are questions where imgdata has a value. But despite using $set it still isn't reactive, and the imgdata isn't there in the view! How can I make this reactive?
var componentOptions = {
props: ['area'],
data: function() {
return {
qIndex: 0,
};
},
mounted: function() {
var that = this;
that.init();
},
methods: {
init: function() {
var that = this;
if (that.area.questions.length > 0) {
that.area.questions.forEach(function(q) {
Util.HTTP('GET', '/api/v1/photos/' + q.id + '/qimage').then(function(response) {
var thisIndex = (that.area.questions.findIndex(entry => entry.id === q.id));
var thisQuestion = (that.area.questions.find(entry => entry.id === q.id));
thisQuestion.imgdata = response.data;
that.$set(that.area.questions, thisIndex, thisQuestion);
})
});
}
console.log("area.questions", that.area.questions);
},

Since area is a prop, you should not be attempting to make changes to it within this component.
The general idea is to emit an event for the parent component to listen to in order to update the data passed in.
For example
export default {
name: "ImageLoader",
props: {
area: Object
},
data: () => ({ qIndex: 0 }), // are you actually using this?
mounted () {
this.init()
},
methods: {
async init () {
const questions = await Promise.all(this.area.questions.map(async q => {
const res = await Util.HTTP("GET", `/api/v1/photos/${encodeURIComponent(q.id)}/qimage`)
return {
...q,
imgdata: res.data
}
}))
this.$emit("loaded", questions)
}
}
}
And in the parent
<image-loader :area="area" #loaded="updateAreaQuestions"/>
export default {
data: () => ({
area: {
questions: [/* questions go here */]
}
}),
methods: {
updateAreaQuestions(questions) {
this.area.questions = questions
}
}
}

Here that variable has a value of this but it's bound under the scope of function. So, you can create reactive property in data as below :
data: function() {
return {
qIndex: 0,
questions: []
};
}
Props can't be reactive so use :
that.$set(this.questions, thisIndex, thisQuestion);
And assign your API output to directly questions using this.questions.

Related

Vuejs created and mounted doesn't work properly even if at the same level than methods

I'm experiencing a strange behaviour with created() and mounted() in Vue.js. I need to set 2 lists in created() - so it means those 2 lists will help me to create a third list which is a merge.
Here is the code :
// return data
created () {
this.retrieveSellOffers();
this.getAllProducts();
},
mounted () {
this.mergeSellOffersProducts();
},
methods: {
retrieveSellOffers() {
this.sellerId = localStorage.sellerId;
SellOfferServices.getAllBySellerId(this.sellerId)
.then((response) => {
this.sellOffers = response.data;
console.log("this.sellOffers");
console.log(this.sellOffers);
})
.catch((e) => {
console.log(e);
});
},
getAllProducts() {
ProductServices.getAll()
.then((response) => {
this.products = response.data;
console.log("this.products");
console.log(this.products);
})
.catch((e) => {
console.log(e);
});
},
mergeSellOffersProducts () {
console.log(this.products) // print empty array
console.log(this.sellOffers) // print empty array
for (var i = 0; i < this.sellOffers.length; i++) {
if (this.sellOffers[i].productId === this.products[i]._id) {
this.arr3.push({id: this.sellOffers[i]._id, price: this.sellOffers[i].price, description: this.products[i].description});
}
}
this.arr3 = this.sellOffers;
},
}
//end of code
So my problem is when I enter in mergeSellOffersProducts(), my 2 lists are empty arrays :/
EDIT :
This way worked for me :
async mounted() {
await this.retrieveSellOffers();
await this.getAllProducts();
this.mergeSellOffersProducts();
},
methods: {
async retrieveSellOffers() {
this.sellerId = localStorage.sellerId;
this.sellOffers = (await axios.get('link/api/selloffer/seller/', { params: { sellerId: this.sellerId } })).data;
},
async getAllProducts() {
this.products = (await axios.get('link/api/product')).data;
},
}
I think the reason is: Vue does not wait for the promises to resolve before continuing with the component lifecycle.
Your functions retrieveSellOffers() and getAllProducts() contain Promise so maybe you have to await them in the created() hook:
async created: {
await this.retrieveSellOffers();
await this.getAllProducts();
}
So I tried to async my 2 methods :
async retrieveSellOffers() {
this.sellerId = localStorage.sellerId;
this.sellOffers = (await axios.get('linkhidden/api/selloffer/', { params: { sellerId: '615b1575fde0190ad80c3410' } })).data;
console.log("this.sellOffers")
console.log(this.sellOffers)
},
async getAllProducts() {
this.products = (await axios.get('linkhidden/api/product')).data;
console.log("this.products")
console.log(this.products)
},
mergeSellOffersProducts () {
console.log("here")
console.log(this.sellOffers)
console.log(this.products)
this.arr3 = this.sellOffers;
},
My data are well retrieved, but yet when I enter in created, the two lists are empty...
You are calling a bunch of asynchronous methods and don't properly wait for them to finish, that's why your data is not set in mounted. Since Vue does not await its lifecycle hooks, you have to deal with the synchronization yourself.
One Vue-ish way to fix it be to replace your method mergeSellOffersProducts with a computed prop (eg mergedSellOffersProducts). Instead of generating arr3 it would simply return the merged array. It will be automatically updated when products or sellOffers is changed. You would simply use mergedSellOffersProducts in your template, instead of your current arr3.
If you only want to update the merged list when both API calls have completed, you can either manually sync them with Promise.all, or you could handle this case in the computed prop and return [] if either of the arrays is not set yet.
When you're trying to merge the 2 lists, they aren't filled up yet. You need to await the calls.
async created () {
await this.retrieveSellOffers();
await this.getAllProducts();
},
async mounted () {
await this.mergeSellOffersProducts();
},

How to update a Vuex child key with Object.assign?

When payload.key is a key like foo, the code below is working fine, but how to update the value of a child key like foo.bar.a ?
export const mutations = {
USER_UPDATE(state, payload) {
console.log(payload);
state.user = Object.assign({}, state.user, {
[payload.key]: payload.value
});
}
}
=== EDIT ===
This is called by:
computed: {
...mapState(['user']),
fooBarA: {
get() {
return this.$store.state.user.foo.bar.a
},
set(value) {
this.$store.commit('USER_UPDATE', {
key: 'foo.bar.a',
value
})
}
}
}
You are replacing the whole state.user Object reference with a new Object, which destroys reactivity.
This simplified code does not demonstrate the need to use Object.assign, so in this cas you can simply:
export const mutations = {
USER_UPDATE(state, payload) {
state.user[payload.key] = payload.value
}
}
Which keeps the original state.user Object reference.
I have an approach that works.
Where the payload.key is "foo.bar.a"
const mutations = {
UPDATE_USER(state, payload) {
const setter = new Function(
"obj",
"newval",
"obj." + payload.key + " = newval;"
);
let user = { ...state.user };
setter(user, payload.value);
state.user = user;
}
};
Demo
https://codesandbox.io/s/vuex-store-nested-key-setter-x1n00
Inspiration from
https://stackoverflow.com/a/30360979/815507

Why my vue js data member is not getting updated?

Data part
data () {
return {
containsAd: true
}
},
Method that manipulates the data member containsAd
updated () {
let _this = this
window.googletag.pubads().addEventListener('slotRenderEnded', function (event) {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
_this.containsAd = !event.isEmpty // this is false
console.log('Ad Exists? ', _this.containsAd)
}
})
},
Just to check if the value has changed or not.
check () {
let _this = this
setTimeout(function () {
console.log('Current Value', _this.containsAd)
}, 5000)
}
Resulting Output
I think doing the event listener in the mounted hook will sort your issue.
data() {
return {
containsAd: true
};
},
mounted() {
window.googletag.pubads().addEventListener('slotRenderEnded', event => {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
this.containsAd = ! event.isEmpty // this is false
console.log('Ad Exists?', this.containsAd);
}
});
}
Also using es6 shorthand function will avoid you having to set _this.

how to implement reusable api-calling component in vuejs?

I'm developing a simple vuejs app where I have a few identical APIs serving content that is parsed in a similar way. I would like to make the code to fetch the content common across the various API calls, and only have a need to pass the API endpoint to what fetches the content.
Here's my code
var content = new Vue({
el: '#story',
data: {
loaded: [],
current: 0,
hasMore:"",
nextItems:"",
errors: []
},
mounted() {
axios.get("/storyjs")
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
}).catch(e => {
this.errors.push(e)
})
},
methods: {
fetchNext: function() {
axios.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
this.current+=1
}).catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e)
})
},
next: function() {
if (this.current+1 < this.loaded.length) {
this.current+=1
} else {
this.fetchNext()
}
},
prev: function() {
this.current = (this.current-1 >= 0) ? this.current-1 : 0
}
},
delimiters: ['[{', '}]']
})
Right now, I've replicated the above object for stories, poems, and many other things. But I would ideally like to combine them into one. Strategies I tried to search for included having a parent component as this object, but I think I'm probably thinking wrong about some of this.
Really appreciate the help!
I went with mixins. This is the solution I implemented.
apiObject.js (Reusable object)
var apiObject = {
data: function() {
return {
loaded: [],
current: 0,
hasMore: "",
nextItems: "",
errors: []
};
},
methods: {
fetchContent: function(apiEndpoint) {
axios
.get(apiEndpoint)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
})
.catch(e => {
this.errors.push(e);
});
},
fetchNext: function() {
axios
.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
this.current += 1;
})
.catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e);
});
},
next: function() {
if (this.current + 1 < this.loaded.length) {
this.current += 1;
} else if (this.hasMore == true) {
this.fetchNext();
}
},
prev: function() {
this.current = this.current - 1 >= 0 ? this.current - 1 : 0;
}
}
};
story.js (Specific usage)
var storyComponent = Vue.extend({
mixins: [apiObject],
created() {
this.fetchContent("/story");
}
});
new Vue({
el: "#story",
components: {
"story-component": storyComponent
},
delimiters: ["[{", "}]"]
});
and then, you could either define the template in the component itself, or use the inline-template way of creating the template in the html file, which is what I did
output.html with all js files included
<div id="story">
<story-component inline-template>
[{loaded[current].title}]
</story-component>
</div>
There are many ways to tackle this, but perhaps once you reach this level of complexity in the model of the components/application state, the most sensible strategy would be to use a central state store.
See the State Management chapter of the vue guide and possibly the excellent vuex.
There you could factor the common logic in suitable local classes/functions and call them from store actions (for async operations you have to use actions, which will commit mutations with respective state changes at completion of the asynchronous operations.

Is it possible to exchange the render functions of VueJS-Components during runtime?

I've played around with vue-i18n and Vue.compile() and found a very static solution to my problem. While searching for a solution I've tried to dynamically set the render functions during runtime. Unfortunately without any success.
Out of curiosity: Is it possible to exchange the render functions of Components during runtime?
I try to do something like this:
{
props: {
toCompile: {
type: String,
required: true
},
callbackFn: {
type: Function,
default: () => {}
}
},
created (){
let res = Vue.compile(this.toCompile);
this.render = res.render;
this.staticRenderFns = res.staticRenderFns;
}
}
The following approach is working for me:
{
...
methods: {
render: function () {
var createElement = this.$createElement;
return (this._self._c || createElement)("div", {
staticClass: "element"
});
}
},
beforeCreate: function() {
this.$vnode.componentOptions.Ctor.options.render = this.$vnode.componentOptions.Ctor.options.methods.render.bind(this);
}
}
If your want slots as well, use the following render method:
render: function () {
var that = this,
createElement = (this._self._c || this.$createElement),
children = Object.keys(that.$slots).map(function(slot) {
return createElement('template', { slot }, that.$slots[slot]);
});
return createElement('div', [
createElement('component-element, {
attrs: that.$attrs,
on: that.$listeners,
scopedSlots: that.$scopedSlots,
}, children)
]);
}