I'm new to Vue.js and trying to display data that is requested from a server. I have created a new Vue app and defined a method:
methods: {
getData: function() {
// making a request, parsing the response and pushing it to the array.
console.log(arr);
return arr;
}
}
The method works OK and I can log the array to the console on a button click.
<button v-on:click="getData">Get me some data</button>
However, I'm not sure how to actually use this array in the app. I would like save it to a property and then display it for the user. At first, I was thinking that I could use computed properties like this:
computed: {
values: function() {
return this.getData;
}
}
... and display it to the user with a for loop:
<p v-for="value in values">{{ value }}></p>
At least this solution did not produce the desired result. I've probably misunderstood some of Vue's logic here.
You need to use data property.
data() {
return {
values: []
}
},
methods: {
getData: function() {
this.values = arr;
}
}
And loop the values
Related
I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.
As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.
Template HTML:
<template>
...
<v-text-field
v-model="getNumSets"
placeholder="S"
type="number"
outlined
dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>
Component Logic:
<script>
...
computed: {
getNumSets: {
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
...
</script>
Vuex Store Logic:
...
state: {
designerBucket: []
},
getters: {
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
state.designerBucket.forEach(exercise => {
if (exercise.id == payload.id) {
exercise.numSets = payload.numSets
}
})
}
}
Any insight is very appreciated!
P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.
This turned into a bit of a long-winded answer, but bear with me.
Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.
Reactivity for Arrow Function Getters
One of the things to keep in mind when creating a getter like this, where you return an arrow function:
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
...is that you're no longer returning actual state data from your getter.
This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.
BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.
Computed Caching and Dependency Detection
In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.
As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.
The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
The getter: this.$store.getters['designer/getNumSetsForExercise']
this.id
this.parent
None of these values change when <v-text-field> calls your setter.
This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.
How to Fix it?
Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.
In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:
...
state: {
designerBucket: {}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
// Need to use $set since we're adding a new property to the object
Vue.set(state.designerBucket, payload.id, payload.numSets);
}
}
Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:
<script>
...
computed: {
getNumSets: {
get() {
return this.$store.state.designerBucket[this.id];
},
set(value) {
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
});
}
}
...
</script>
This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.
Edited: First import mapGetters from 'vuex' like this on the top of the script tag.
import { mapGetters } from "vuex"
Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-
computed: {
...mapGetters('designer',['getNumSetsForExercise']),
getNumSets: {
get() {
var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
And see if it works.
Coming from Knockout.js, where you can simply create an observable everywhere by defining it, is there something similar in Vue.js?
let vm = {
someOtherVar: ko.observable(7),
entries: ko.observableArray()
};
function addServerDataToEntries(data) {
data.myComputed = ko.pureComputed(() => vm.someOtherVar() + data.bla);
vm.entries.push(data);
}
addServerDataToEntries({ bla: 1 });
In my Vue.js project, I'm getting a list of objects from the server. For each of those objects, I want to add a computed property that I can use in a v-if binding. How can I achieve that?
I'm not familiar with the way Knockout does it but it sounds like a Vue computed. Create a data object to hold your fetched data:
data() {
return {
items: null
}
}
Imagine fetching it in the created hook (or Vuex, wherever):
async created() {
const response = await axios.get(...);
this.items = response.data;
}
Create your computed:
computed: {
itemsFormatted() {
if (!this.items) return null;
return this.items.map(item => {
// Do whatever you want with the items
});
}
}
Here is a demo using this pattern where I'm loading some data and printing out a filtered result from it. Let me know if I misunderstood what you're looking for. (You can see the original fetched data in the console.)
In the store, I have an action to update some data, the action looks like this:
setRoomImage({ state }, { room, index, subIndex, image }) {
state.fullReport.rooms[room].items[index].items[subIndex].image = image;
console.log(state.fullReport.rooms[room].items[index].items[subIndex])
},
Because all of this data is dynamic so I have to dynamically change the nested values and can't directly hard code the properties.
The data looks like this:
fullreport: {
rooms: {
abc: {
items: [
{
type: "image-only",
items: [
{
label: "Main Image 1",
image: ""
},
{
label: "Main Image 2",
image: ""
}
]
}
]
}
}
}
When I dispatch the action, In the console I can see that the value of the sub-property image is successfully mutated, but if I access the VueX store from the Vue DevTools inside Chrome, I see that value doesn't change there. Here is the console output:
Please, can somebody tell why is it happening? As I know that data is successfully changing, but somehow the state isn't showing it and hence my components do not rerender.
I also tried using Vue.set instead of simple assignment, but still no luck :(
Vue.set(
state.fullReport.rooms[room].items[index].items[subIndex],
"image",
image
);
Edit:
Following David Gard's answer, I tried the following:
I am also using Lodash _ (I know making whole copies of objects isn't good), this is the mutation code block.
let fullReportCopy = _.cloneDeep(state.fullReport);
fullReportCopy.rooms[room].items[index].items[subIndex].image = image;
Vue.set(state, "fullReport", fullReportCopy);
Now In the computed property, where the state.fullReport is a dependency, I have a console.log which just prints out a string whenever the computed property is re-computed.
Every time I commit this mutation, I see the computed property logs the string, but the state it is receiving still doesn't change, I guess Vue.set just tells the computed property that the state is changed, but it doesn't actually change it. Hence there is no change in my component's UI.
As mentioned in comments - it quickly gets complicated if you hold deeply nested state in your store.
The issue is, that you have to fill Arrays and Objects in two different ways, hence, consider whether you need access to their native methods or not. Unfortunately Vuex does not support reactive Maps yet.
That aside, I also work with projects that require the dynamic setting of properties with multiple nested levels. One way to go about it is to recursively set each property.
It's not pretty, but it works:
function createReactiveNestedObject(rootProp, object) {
// root is your rootProperty; e.g. state.fullReport
// object is the entire nested object you want to set
let root = rootProp;
const isArray = root instanceof Array;
// you need to fill Arrays with native Array methods (.push())
// and Object with Vue.set()
Object.keys(object).forEach((key, i) => {
if (object[key] instanceof Array) {
createReactiveArray(isArray, root, key, object[key])
} else if (object[key] instanceof Object) {
createReactiveObject(isArray, root, key, object[key]);
} else {
setReactiveValue(isArray, root, key, object[key])
}
})
}
function createReactiveArray(isArray, root, key, values) {
if (isArray) {
root.push([]);
} else {
Vue.set(root, key, []);
}
fillArray(root[key], values)
}
function fillArray(rootArray, arrayElements) {
arrayElements.forEach((element, i) => {
if (element instanceof Array) {
rootArray.push([])
} else if (element instanceof Object) {
rootArray.push({});
} else {
rootArray.push(element);
}
createReactiveNestedFilterObject(rootArray[i], element);
})
}
function createReactiveObject(isArray, obj, key, values) {
if (isArray) {
obj.push({});
} else {
Vue.set(obj, key, {});
}
createReactiveNestedFilterObject(obj[key], values);
}
function setValue(isArray, obj, key, value) {
if (isArray) {
obj.push(value);
} else {
Vue.set(obj, key, value);
}
}
If someone has a smarter way to do this I am very keen to hear it!
Edit:
The way I use the above posted solution is like this:
// in store/actions.js
export const actions = {
...
async prepareReactiveObject({ commit }, rawObject) {
commit('CREATE_REACTIVE_OBJECT', rawObject);
},
...
}
// in store/mutations.js
import { helper } from './helpers';
export const mutations = {
...
CREATE_REACTIVE_OBJECT(state, rawObject) {
helper.createReactiveNestedObject(state.rootProperty, rawObject);
},
...
}
// in store/helper.js
// the above functions and
export const helper = {
createReactiveNestedObject
}
Excluding the good practices on the comments.
That you need is: to instruct Vue when the object change (Complex objects are not reactive). Use Vue.set. You need to set the entire object:
Vue.set(
state,
"fullReport",
state.fullReport
);
Documentation: https://v2.vuejs.org/v2/api/#Vue-set
I am currently experiencing an issue where the computed() property is not able to get data. Although data was already initiated in created() property. Am I doing it wrong? Please advise how I can fix this issue.
const randomPlayers = {
template:
`
<input type="text" v-model="search_player">
<div v-for="player in modPlayers" v-if="list_of_random_players!=null">
<p>{{player.firstname}}</p>
<p>{{player.lastname}}</p>
<div>
`,
props: ['data'],
data (){
return{
list_of_random_players: null,
search_player: null
}
},
created(){
this.get_random_players()
},
computed: {
modPlayers(){
return this.list_of_random_players.filter( person => {
return !this.search_player ||
( person.firstname.toLowerCase().indexOf(this.search_player.toLowerCase()) > -1 || person.lastname.toLowerCase().indexOf(this.search_player.toLowerCase()) > -1)
})
}
},
methods: {
get_random_players: function(){
$.post(
url:'random_url'
data: {
players: data
}
).done((success)=>{
this.list_of_random_players: JSON.parse(success)
})fail((err)=>{
console.log(err)
})
}
}
}
I get the following two errors:
(1) TypeError: Cannot read property 'filter' of null
(2) TypeError: this.list_of_random_players.filter is not a function
From Vue: "When a Vue instance is created, it adds all the properties found in its data object to Vue’s reactivity system. When the values of those properties change, the view will “react”, updating to match the new values."
So data is a function that returns an object but as mentioned by #Sovalina you are not returning it so you cannot access its properties. You need to add return and change null to []:
data() {
return {
list_of_random_players: [],
search_player: []
}
},
or you can do without return and like a regular object:
data: {
list_of_random_players: [],
search_player: []
}
When your Vue component is used multiple times, it is better to use it like a function(first case).
"When defining a component, data must be declared as a function that returns the initial data object. Why? Because there will be many instances created using the same definition. If we still use a plain object for data, that same object will be shared by reference across all instance created! By providing a data function, every time a new instance is created we can call it to return a fresh copy of the initial data."
Reference:link
It might be just a typo but you need to add : to methods as well.
I have the code (vuejs2) -
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
return { totalCompetetions: this.values.length}
}
})
Nothing is printed on the page but if I change the template value to
template: `<div>{{this.values.length}}</div>`
it prints 15. What am I doing wrong and how can I pass the props to the data?
Any help is much appreciated.
I was unable to assign the prop values to data totalCompetetions in the following way -
data: function () {
return { totalCompetetions: this.values.length}
}
But I was able to do it using the watch, computed, and methods properties.
With watch property -
watch: {
values: function(){
this.totalCompetitions = this.values;
}
}
With computed property -
computed:{
competition:{
get: function(){
return this.values.length;
}
}
With methods property -
methods:{
competitionn: function(){
return this.values.length;
}
}
But for computed and methods properties, I needed to set totalCompetetions in the following way -
For computed -
template: `<div><p>{{totalCompetitions = competition}}</p></div>` //without parenthesis
For methods -
template: `<div><p>{{totalCompetitions = competition()}}</p></div>` //with parenthesis
You code does work.
I guess the problem is your parent component. Did you pass the values correctly? for example:
<competetion-list :values="[1, 2, 3]"></competetion-list>
Besides, for your case I'd say computed properties is a better solution.
computed: {
totalCompetetions () {
return this.values.length
}
}
From the data() method, you should be able to reference the component's properties using this.
Try following:
Vue.component('competetion-list', {
template: `<div>{{totalCompetetions}}</div>`,
props: ['values'],
data: function () {
var data = { totalCompetetions: this.values.length}
return data
}
})
As validly mentioned in the comment, if values array is changing later, you may have to put a watcher on the prop and inside watcher, set totalCompetetions as this.values.length.