re-populate a directive array reactively in Vue.js - javascript

I have an array defined in my data() which gets populated through a custom directive in its bind hook as below:
import Vue from 'vue'
export default {
el: '#showingFilters',
name: "Filters",
data() {
return {
country: '' // v-modelled to a <select>
states: [],
}
},
directives: {
arraysetter: {
bind: function(el, binding, vnode) {
vnode.context[binding.arg] = Object.keys(el.options).map(op => el.options[op].value);
},
},
},
methods: {
countryChangeHandler() {
this.states.splice(0)
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
res.states.forEach( (element,i) => {
Vue.set(this.states, i, element.urlid)
});
})
},
}
The problem starts when I want to re-populate the states array in the countryChangeHandler() method (when #change happens for the country select tag).
I used splice(0) to make the array empty first and I have then used Vue.set to make the re-population reactive, but Vue still doesn't know about it!!! The array has the correct elements though! I just don't know how to make this reactive.
PS: I searched to do this without forEach but $set needs an index.
I'd appreciate any help here.

This solution should work and maintain reactivity.
You should be able to replace the entire array without using splice or set.
I have used a closure to capture this because sometimes the fetch call interferes with the this reference, even inside a lambda expression.
countryChangeHandler() {
this.states = []
const that = this
fetch(`/scripts/statejson.php?country=${this.country}`)
.then(response => response.json())
.then(res => {
that.states = res.states.map(it=>it.urlid)
})
},

Related

Array in data method gets updated in one method but still empty in another method

In my app I am trying to update an array. First I get data from the database and add it to the array and in another method I want to use that array. But the array does not get updated.
If I use my array exerciseList in the DOM it has the data but in the getExercises funciton the length of the array is still 0. It like I run the method before the data is added to the array or something like that.
Any Idea why this is not working?
data: () => ({
exerciseList: []
});
created() {
this.getDataBaseCollection("Exercise", this.exerciseList); // array gets information here
}
mounted() {
this.getExercises();
},
methods: {
getDataBaseCollection: function (CollectionName, list) {
db.collection(CollectionName).onSnapshot(snapshot => {
snapshot.forEach(doc => {
list.push(doc.data());
});
});
},
getExercises: function () {
console.log(this.exerciseList.length); // length is 0 ?? array seems empty
}
},
I think a key missing part may be updating the component's exerciseList variable , not the list argument. They are not the same variable. Objects are passed by reference but arrays are passed to functions by value only which makes list it's own variable independent from excerciseList. This is rough code that shows some ways to make sure exerciseList is updated and how to know when the values are all in the array.
// include exerciseListLoaded to flag when all data is ready
data: () => ({
exerciseList: [],
exerciseListLoaded: false
});
created() {
this.getDataBaseCollection("Exercise"); // array gets information here
}
mounted() {
// based on timing of the `onSnapshot` callback related to `mounted` being called, this may likely still result in 0
console.log("Mounted");
this.getExercises();
},
watch: {
// watch for all data being ready
exerciseListLoaded () {
console.log("All Loaded");
this.getExercises();
}
},
methods: {
// be sure to update the exerciseList on the component
getDataBaseCollection: function (CollectionName) {
// being careful about `this` since within `onSnapshot` I suspect it will change within that function
const componentScope = this;
db.collection(CollectionName).onSnapshot(snapshot => {
snapshot.forEach(doc => {
componentScope.exerciseList.push(doc.data());
// could also still update `list` here as well if needed
});
// setting this allows the component to do something when data is all loaded via the `watch` config
componentScope.exerciseListLoaded = true;
});
},
getExercises: function () {
console.log(this.exerciseList.length); // length is 0 ?? array seems empty
}
},
when you use this inside a function it refers to the function not the vue instance so you may use that may work with you:
getExercises() {
console.log(this.exerciseList.length);
}

Unable to set Vue.js data values inside axios response

I have created an axios request to my api for two routes. Using the response data I sort posts into the correct columns inside an array. This all works as it should but then when I come to assigning the value of this array to an array inside data() i get the following error;
TypeError: Cannot set property 'boardPosts' of null
at eval (SummaryBoard.vue?2681:90)
at wrap (spread.js?0df6:25)
So I figured maybe something was wrong with the array I was trying to assign. So I tried to assign boardPosts a simple string value and I still get the same error. Why can I not set the value of boardPosts inside my axios response?
my code;
import axios from 'axios';
export default {
name: 'SummaryBoard',
data() {
return {
boardPosts: '',
}
},
created() {
this.getBoardData();
},
methods:
getBoardData() {
function getBoardColumns() {
return axios.get('http://localhost:5000/api/summary-board/columns');
}
function getBoardPosts() {
return axios.get('http://localhost:5000/api/summary-board/posts');
}
axios.all([getBoardColumns(), getBoardPosts()])
.then(axios.spread(function(columnData, postData) {
let posts = postData.data;
// add posts array to each object
let columns = columnData.data.map(obj => ({...obj, posts: []}));
posts.forEach((post) => {
// If column index matches post column index value
if(columns[post.column_index]){
columns[post.column_index].posts.push(post);
}
});
console.log(columns);
this.boardPosts = 'hello';
}))
.catch(error => console.log(error));
}
}
}
That's because you're using not using an arrow function in axios.spread(...). This means that you do not preserve the lexical this from the VueJS component, as function() {...} will create a new scope for itself. If you change it to use arrow function, then the this in the callback will refer to your VueJS component instance:
axios.all([getBoardColumns(), getBoardPosts()])
.then(axios.spread((columnData, postData) => {
// Rest of the logic here
}))
.catch(error => console.log(error));

My mutations aren't working! How do I correct this?

I am trying to set the breakfastMenu array in state as shown below but I can't see the array being filled in my vue-devtools.
I have properly set-up the Vuex methods and checked twice, also I didn't receive any sort of error. So, why do I have a logical error in my code?
store.js:
export default new Vuex.Store({
state: {
menu: [],
breakfastMenu: [],
lunchMenu: [],
dinnerMenu: []
},
mutations: {
'SET_MENU': (state, menuMaster) => {
state.menu = menuMaster;
},
'SET_BREAKFAST_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_LUNCH_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
'SET_DINNER_MENU': (state, order) => {
state.breakfastMenu.unshift(order);
},
},
actions: {
initMenu: ({ commit }, menuMaster) => {
commit('SET_MENU', menuMaster)
},
initBreakfastMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 1) {
commit('SET_BREAKFAST_MENU', element)
}
});
},
initLunchMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 2) {
commit('SET_LUNCH_MENU', element)
}
});
},
initDinnerMenu: ({ commit, state }) => {
state.menu.forEach((element) => {
if (element.categoryId == 3) {
commit('SET_DINNER_MENU', element)
}
});
},
},
getters: {
getBreakfastMenu(state) {
return state.breakfastMenu
},
getLunchMenu(state) {
return state.lunchMenu
},
getDinnerMenu(state) {
return state.dinnerMenu
},
}
})
Breakfast.vue:
import { mapActions, mapGetters } from 'vuex';
export default {
data() {
return {
breakfastArray: []
};
},
methods: {
...mapActions(['initBreakfastMenu']),
...mapGetters(['getBreakfastMenu']),
},
created() {
this.initBreakfastMenu;
this.breakfastArray = this.getBreakfastMenu;
}
};
No error messages so far!
I need the breakfastMenu array filled in store.js.
Please help out!
A few thoughts.
Firstly, this line:
this.initBreakfastMenu;
You aren't actually calling the method. It should be:
this.initBreakfastMenu();
Next problem is this:
...mapGetters(['getBreakfastMenu']),
The line itself is fine but it's inside your methods. It should be in the computed section.
You haven't included any sample data for state.menu but it's also worth noting that initBreakfastMenu won't do anything unless there is suitable data inside state.menu. I suggest adding some console logging to ensure that everything is working as expected there.
SET_BREAKFAST_MENU, SET_LUNCH_MENU and SET_DINNER_MENU are all modifying state.breakfastMenu. I would assume that this is incorrect and each should be modifying their respective menu.
I would also note that using local data for breakfastArray is suspicious. Generally you'd just want to use the store state directly via the computed property rather than referencing it via local data. This is not necessarily wrong, you may want to detach the component data from the store in this way, but keep in mind that both are referencing the same array so modification to one will also affect the other. You aren't taking a copy of the array, you're just creating a local reference to it.
You should also consider whether you actually need the 4 menu types within your state. If breakfastMenu, lunchMenu and dinnerMenu are all just derived from menu then you'd be better off just implementing those using getters. getters are the store equivalent of computed properties and can contain the relevant filtering logic to generate their value from state.menu.
initBreakfastMenu is an action and you may want to use this.initBreakfastMenu()

Vuex: How to store the result of a getter in the state?

I am having a problem in my Vuex.Store:
I would like to get an object (getter.getRecipe) using two state entries as search criteria (state.array & state.selected) with a getter. Then I would like to store that result in my state (state.recipe) in order to be able to update it within components (i.e., changing one key of the recipe object based on client action).
However, I have no idea how I can store the result of getters in my state ("this.getters.getRecipe" is not working...).
Very helpful for hints. Thanks a lot.
//store.js (vuex store)
export const store = new Vuex.Store({
state: {
array: [recipe1, recipe2],
selected: 0,
recipe: this.getters.getRecipe()
},
getters: {
getRecipe: (state) => {
return state.array[state.selected]
}
}
})
Instead, you should use Method style getters with arguments:
You can also pass arguments to getters by returning a function. This is particularly useful when you want to query an array in the store:
A basic example:
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
Then, to use the example:
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
or, from inside a component:
this.$store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
This way you ensure your state remains a pure data structure that follows the one way data flow set forth by the flux pattern.

Trouble triggering reactivity on changed Vuex objects

I'm modifying the value of an existing property on an object that is in an array of objects in my Vuex.store. When I update the store, it is not triggering a re-render of my computed property that is accessing the store. If I reset the stored value to an empty array, and then set it again to my new array, it'll trigger the change. But simply updating the property of the array of objects does not trigger a change.
I have tried using Vue.set() like the docs talk about, and that updates the store, but still does not trigger a re-render of the computed property. What am I missing? Using Vue 2.2.4 and Vuex 2.2.0.
//DEBUG: An example of the updated post I'm adding
let myNewScheduledPost = {
id: 1,
name: 'James'
};
this.$store.dispatch('addScheduledPost', post);
//DEBUG: My store
const options = {
state: {
scheduledPosts: [
{ id: 1, name: 'Jimmy'}
],
},
mutations: {
scheduledPosts: (state, scheduledPosts) => {
//This triggers the reactivity/change so my computed property re-renders
//But of course seems the wrong way to do it.
state.scheduledPosts = [];
state.scheduledPosts = scheduledPosts;
//Neither of these two lines triggers my computed property to re-render, even though there is a change in scheduledPosts
state.scheduledPosts = scheduledPosts;
Vue.set(state, 'scheduledPosts', scheduledPosts);
},
},
actions: {
addScheduledPost({ commit, getters }, newScheduledPost) {
let scheduledPosts = getters.scheduledPosts;
const idx = scheduledPosts.findIndex(existingScheduledPost => existingScheduledPost.id === newScheduledPost.id);
//If the post is already in our list, update that post
if (idx > -1) {
scheduledPosts[idx] = newScheduledPost;
} else {
//Otherwise, create a new one
scheduledPosts.push(newScheduledPost);
}
commit('scheduledPosts', scheduledPosts);
//DEBUG: This DOES have the correct updated change - but my component does not see the change/reactivity.
console.log(getters.scheduledPosts);
}
},
getters: {
scheduledPosts: (state) => {
return state.scheduledPosts;
}
}
};
//DEBUG: Inside of my component
computed: {
mySortedPosts()
{
console.log('im being re-rendered!');
return this.$store.getters.scheduledPosts.sort(function() {
//my sorted function
});
}
}
Your problem is if you are wanting to access a portion of the state you don't use a getter https://vuex.vuejs.org/en/state.html.
computed: {
mySortedPosts(){
return this.$store.state.scheduledPosts
}
}
Getters are for computed properties in the store https://vuex.vuejs.org/en/getters.html. So in your case you might create a getter to sort your scheduled posts then name it sortedScheduledPosts and then you can add it to your components computed properties like you are now.
The key thing is your getter needs to have a different name then your state property just like you would in a component.

Categories