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)
})
},
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);
}
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));
I'm using Vuex to handle my application state.
I need to make an Ajax Get request to a rest api and then show some objects list.
I'm dispatching an action that loads this data from the server but then I don't know how to handle it on the component.
Now I have this:
//component.js
created(){
this.$store.dispatch("fetch").then(() => {
this.objs = this.$store.state.objs;
})
}
But I don't think that the assignment of the incoming data to the local property is the correct way to handle store data.
Is there a way to handle this better? Maybe using mapState?
Thanks!
There are many ways you can do it, you must experiment and find the one that fits your approach by yourself. This is what I suggest
{ // the store
state: {
something: ''
},
mutations: {
setSomething (state, something) {
// example of modifying before storing
state.something = String(something)
}
},
actions: {
fetchSomething (store) {
return fetch('/api/something')
.then(data => {
store.commit('setSomething', data.something)
return store.state.something
})
})
}
}
}
{ // your component
created () {
this.$store
.dispatch('fetchSomething')
.then(something => {
this.something = something
})
.catch(error => {
// you got an error!
})
}
}
For better explanations: https://vuex.vuejs.org/en/actions.html
Now, if you're handling the error in the action itself, you can simply call the action and use a computed property referencing the value in the store
{
computed: {
something () { // gets updated automatically
return this.$store.state.something
}
},
created () {
this.$store.dispatch('loadSomething')
}
}
I'm trying to understand how to properly watch for some prop variation.
I have a parent component (.vue files) that receive data from an ajax call, put the data inside an object and use it to render some child component through a v-for directive, below a simplification of my implementation:
<template>
<div>
<player v-for="(item, key, index) in players"
:item="item"
:index="index"
:key="key"">
</player>
</div>
</template>
... then inside <script> tag:
data(){
return {
players: {}
},
created(){
let self = this;
this.$http.get('../serv/config/player.php').then((response) => {
let pls = response.body;
for (let p in pls) {
self.$set(self.players, p, pls[p]);
}
});
}
item objects are like this:
item:{
prop: value,
someOtherProp: {
nestedProp: nestedValue,
myArray: [{type: "a", num: 1},{type: "b" num: 6} ...]
},
}
Now, inside my child "player" component I'm trying to watch for any Item's property variation and I use:
...
watch:{
'item.someOtherProp'(newVal){
//to work with changes in "myArray"
},
'item.prop'(newVal){
//to work with changes in prop
}
}
It works but it seems a bit tricky to me and I was wondering if this is the right way to do it. My goal is to perform some action every time prop changes or myArray gets new elements or some variation inside existing ones. Any suggestion will be appreciated.
You can use a deep watcher for that:
watch: {
item: {
handler(val){
// do stuff
},
deep: true
}
}
This will now detect any changes to the objects in the item array and additions to the array itself (when used with Vue.set). Here's a JSFiddle: http://jsfiddle.net/je2rw3rs/
EDIT
If you don't want to watch for every change on the top level object, and just want a less awkward syntax for watching nested objects directly, you can simply watch a computed instead:
var vm = new Vue({
el: '#app',
computed: {
foo() {
return this.item.foo;
}
},
watch: {
foo() {
console.log('Foo Changed!');
}
},
data: {
item: {
foo: 'foo'
}
}
})
Here's the JSFiddle: http://jsfiddle.net/oa07r5fw/
Another good approach and one that is a bit more elegant is as follows:
watch:{
'item.someOtherProp': function (newVal, oldVal){
//to work with changes in someOtherProp
},
'item.prop': function(newVal, oldVal){
//to work with changes in prop
}
}
(I learned this approach from #peerbolte in the comment here)
VueJs deep watch in child objects
new Vue({
el: "#myElement",
data: {
entity: {
properties: []
}
},
watch: {
'entity.properties': {
handler: function (after, before) {
// Changes detected. Do work...
},
deep: true
}
}
});
Personally I prefer this clean implementation:
watch: {
myVariable: {
handler(newVal, oldVal){ // here having access to the new and old value
// do stuff
console.log(newVal, oldVal);
},
deep: true,
/*
Also very important the immediate in case you need it,
the callback will be called immediately after the start
of the observation
*/
immediate: true
}
}
How if you want to watch a property for a while and then to un-watch it?
Or to watch a library child component property?
You can use the "dynamic watcher":
this.$watch(
'object.property', //what you want to watch
(newVal, oldVal) => {
//execute your code here
}
)
The $watch returns an unwatch function which will stop watching if it is called.
var unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
Also you can use the deep option:
this.$watch(
'someObject', () => {
//execute your code here
},
{ deep: true }
)
Please make sure to take a look to docs
Another way to add that I used to 'hack' this solution was to do this:
I set up a seperate computed value that would simply return the nested object value.
data : function(){
return {
countries : {
UnitedStates : {
value: "hello world";
}.
},
};
},
computed : {
helperName : function(){
return this.countries.UnitedStates.value;
},
},
watch : {
helperName : function(newVal, oldVal){
// do this...
}
}
Tracking individual changed items in a list
If you want to watch all items in a list and know which item in the list changed, you can set up custom watchers on every item separately, like so:
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
// NOTE: For mutated objects, newVal and oldVal will be identical.
console.log(newVal);
},
},
created () {
this.list.forEach((val) => {
this.$watch(() => val, this.handleChange, {deep: true});
});
},
});
If your list isn't populated straight away (like in the original question), you can move the logic out of created to wherever needed, e.g. inside the .then() block.
Watching a changing list
If your list itself updates to have new or removed items, I've developed a useful pattern that "shallow" watches the list itself, and dynamically watches/unwatches items as the list changes:
// NOTE: This example uses Lodash (_.differenceBy and _.pull) to compare lists
// and remove list items. The same result could be achieved with lots of
// list.indexOf(...) if you need to avoid external libraries.
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
watchTracker: [],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
console.log(newVal);
},
updateWatchers () {
// Helper function for comparing list items to the "watchTracker".
const getItem = (val) => val.item || val;
// Items that aren't already watched: watch and add to watched list.
_.differenceBy(this.list, this.watchTracker, getItem).forEach((item) => {
const unwatch = this.$watch(() => item, this.handleChange, {deep: true});
this.watchTracker.push({ item: item, unwatch: unwatch });
// Uncomment below if adding a new item to the list should count as a "change".
// this.handleChange(item);
});
// Items that no longer exist: unwatch and remove from the watched list.
_.differenceBy(this.watchTracker, this.list, getItem).forEach((watchObj) => {
watchObj.unwatch();
_.pull(this.watchTracker, watchObj);
// Optionally add any further cleanup in here for when items are removed.
});
},
},
watch: {
list () {
return this.updateWatchers();
},
},
created () {
return this.updateWatchers();
},
});
I've found it works this way too:
watch: {
"details.position"(newValue, oldValue) {
console.log("changes here")
}
},
data() {
return {
details: {
position: ""
}
}
}
Not seeing it mentioned here, but also possible to use the vue-property-decorator pattern if you are extending your Vue class.
import { Watch, Vue } from 'vue-property-decorator';
export default class SomeClass extends Vue {
...
#Watch('item.someOtherProp')
someOtherPropChange(newVal, oldVal) {
// do something
}
...
}
My problem with the accepted answer of using deep: true, is that when deep-watching an array, I can't easily identify which element of the array contains the change. The only clear solution I've found is this answer, which explains how to make a component so you can watch each array element individually.
None of the answer for me was working. Actually if you want to watch on nested data with Components being called multiple times. So they are called with different props to identify them.
For example <MyComponent chart="chart1"/> <MyComponent chart="chart2"/>
My workaround is to create an addionnal vuex state variable, that I manually update to point to the property that was last updated.
Here is a Vuex.ts implementation example:
export default new Vuex.Store({
state: {
hovEpacTduList: {}, // a json of arrays to be shared by different components,
// for example hovEpacTduList["chart1"]=[2,6,9]
hovEpacTduListChangeForChart: "chart1" // to watch for latest update,
// here to access "chart1" update
},
mutations: {
setHovEpacTduList: (state, payload) => {
state.hovEpacTduListChangeForChart = payload.chart // we will watch hovEpacTduListChangeForChart
state.hovEpacTduList[payload.chart] = payload.list // instead of hovEpacTduList, which vuex cannot watch
},
}
On any Component function to update the store:
const payload = {chart:"chart1", list: [4,6,3]}
this.$store.commit('setHovEpacTduList', payload);
Now on any Component to get the update:
computed: {
hovEpacTduListChangeForChart() {
return this.$store.state.hovEpacTduListChangeForChart;
}
},
watch: {
hovEpacTduListChangeForChart(chart) {
if (chart === this.chart) // the component was created with chart as a prop <MyComponent chart="chart1"/>
console.log("Update! for", chart, this.$store.state.hovEpacTduList[chart]);
},
},
I used deep:true, but found the old and new value in the watched function was the same always. As an alternative to previous solutions I tried this, which will check any change in the whole object by transforming it to a string:
created() {
this.$watch(
() => JSON.stringify(this.object),
(newValue, oldValue) => {
//do your stuff
}
);
},
For anyone looking for Vue 3
import { watch } from 'vue';
...
...
watch(
() => yourNestedObject, // first param, your object
(currValue, prevValue) => { // second param, watcher callback
console.log(currValue, prevValue);
},
{ deep: true } // third param, for deep checking
);
You can refer to the documentation here: https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch
Here's a way to write watchers for nested properties:
new Vue({
...allYourOtherStuff,
watch: {
['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
You can even use this syntax for asynchronous watchers:
new Vue({
...allYourOtherStuff,
watch: {
async ['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// Note: `newValue` will be equal to `oldValue` here
// on nested mutations as long as the object itself
// hasn't been replaced.
},
deep: true
}
}
}