Data object defined in created function but NOT reactive? - javascript

I have a Vue instance where data property is initialised as an object:
var app = new Vue({
el: '#app',
data: {
obj: { }
},
methods: {
},
created: function() {
this.obj["obj2"] = {}
this.obj["obj2"].count = 0
},
mounted: function() {
setInterval(function() {
this.obj.obj2.count++
console.log(this.obj.obj2.count)
}.bind(this), 1000)
}
})
<div id="app">
{{ obj['obj2'].count }}
</div>
And then when the instance is created I add a property to the obj.
However, when I want to display the object's object property count, it shows 0 and is not reactive. If I defined the whole object in the data, it is reactive but I can't define the object in the data because its data depends on an external source - API, that's why it is filled with data in created function.
The only way how I managed to make it show the current count is by forcing updates on the view but I don't think it's the correct solution.
Any suggestions?

The problem is that Vue can not track completely new properties on its reactive objects. (It's a limitation of JavaScript).
It's described in detail here: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
The short version is: You have to do
created: function() {
Vue.set(this.obj, 'obj2', {})
Vue.set(this.obj.obj2, 'count', 0)
}
or
created: function() {
Vue.set(this.obj, 'obj2', {
count: 0
})
}

Related

Is it possible for Vue.js to automatically update the view when a third-party JSON is updated?

I'm trying to accomplish the following but I don't even know if it is even possible with Vue as I'm struggling to get the desired result:
I have an endpoint for an API which returns many objects within an array.
I am successfully rendering the data within my Vue application but I wanted to know if it is possible for Vue to "track" when the array has been updated with more objects and then render those in the view.
I am using setInterval to perform a GET request every 10 minutes and the new data is going into the object within my data() correctly but the changes are not reflected within the view.
At the moment I am changing a boolean from true to false at the beginning and end respectively so that the view is rendered again with v-if.
My goal is to create a simple Twitter feed app that performs a GET request every 10 minutes, collects the tweets, puts them into my Vue instance and show them in the view without having to reload the page/re-render the component. Like an automatic Twitter feed that just constantly loads new tweets every 10 minutes.
Is this even possible? I've tried using the Vue.set() method but that hasn't made any difference.
If it's not possible, what would be the best way to implement something similar?
Here is my code:
JavaScript:
new Vue({
el: '#app',
data: {
items: [],
},
created() {
this.load();
setInterval(() => this.load(), 5000);
},
methods: {
load() {
axios.get('https://reqres.in/api/users?page=2')
.then(response => {
this.items = response.data.data;
});
}
}
});
HTML
<div id="app">
<p v-for="item in items">
{{ item.first_name }}
</p>
</div>
CodePen: https://codepen.io/tomhartley97/pen/VwZpZNG
In the above code, if the array is updated by the GET request, the chances are not reflected within the view?
Yes it is possible. The way you need to set new reactive properties in your Vue instance is the following:
For Object properties: Vue.set(this.baseObject, key, value)
The baseObject cannot be a Vue instance or the base data() object, so you will have to declare a container property.
For Array entries use native array methods: e.g. Array.prototype.push().
Using Vue.set(array, arrayIndex, newArrayElement) does not work
Hence, your solution might look something line that:
<script>
export default {
data() {
return {
response: [],
};
},
mounted() {
setInterval = (() => this.getData), 600000);
}
methods: {
async getData() {
const res = await request();
const resLength = res.data.length;
for (let i = 0; i < resLength; i++) {
// check if entry is already in array
const entryExists = this.response.some((entry) => {
return entry.id === res.data[i].id
})
if (!entryExists) {
// this will make the array entries responsive, but not nested Objects
this.response.push(res.data[i]);
// to create nested responsive Objects you will have to set them explicitly
// e.g. Vue.set(this.response[this.response.indexOf(res.data[i])], nestedObjectKey, res.data[i].nestedObject)
}
}
}
}
};
</script>
Well, I view the codepen, I known why your view do not get update: the api response always return the same array!
Try to return different data.
The api returns an array, so the data defines
data() {
return {
array: [] // array that api returns
}
}
The template may look like this
<div v-for="item in array">
</div>
And the update methods
update() {
setInterval(async () => {
let resp = await api()
this.array = resp.data.concat(this.array)
}, TEN_MINUTES)
}

VueJS: Replace/Update Array

I currently have an array of object that I am rendering to a table. I am trying to follow the examples provided by Vuejs to use a "single source of truth" shared between multiple vues on the same page.
Overall, I am trying to make it where when vue1.refresh() is triggered, all the vues update their data when the "single source of truth" is updated. However, self.surveys = surveys; only updates the data on vue1.
Note: I am following the guide from https://v2.vuejs.org/v2/guide/state-management.html
// The single source of truth
var cache = {
data: [{...}] // Array of objects
}
var vue1 = new Vue({
el: "#table",
data: {
surveys: cache.data // Points to the single source of truth
},
methods: {
refresh: function(){
var self = this;
// After getting data back from an ajax call
.done(function(surveys) {
self.surveys = surveys;
});
},
}
});
var vue2 = new Vue({
el: "#table",
data: {
surveys: cache.data // Points to the single source of truth
},
methods: {
// Methods
}
});
There are two principles of Vue that will help you here:
In Vue, every data item is a source of truth.
Only the owner of a data item should modify it.
In your example, you have three sources of truth: the one you want to be the single source, and two others that are initialized from it. Also, the one you want to be the source of truth isn't a data item, it is outside Vue.
So to start, you should have a single Vue that represents your entire application and defines any data that represents application-level state:
new Vue({
el: '#app',
data: {
cache: {
data: [...]
}
}
});
The two Vue objects that you created should be children of the application Vue, which is to say, components.
The parent tells the children what the truth is via props. The child can suggest changes to the truth by emitting events to the parent, but the child does not directly modify the truth. That keeps all management of the truth in one place.
You would need to mutate the array, not replace it.
Array.prototype.splice can do this for you, if you don't want to use something like Vuex, as suggested by Vanojx1.
Splice expects specific elements, not a complete array for insertions. Because you have an array you want to use and you need to clear the old one, the syntax is a little odd... You pass this, the start, the count to remove (the entire length), and then the elements to add (concatenated on from your new array).
Array.prototype.splice.apply([self.surveys, 0, self.surveys.length].concat(surveys));
Problem is, you are replacing shared Cache object previously assigned to surveys variable, with new, not shared object. And solution? Do not try to mutate cache object. Just use Vuex. Vuex is simple, real "Vue way" solution.
// The single source of truth
var cache = {
data: [{...}] // Array of objects
}
var vue1 = new Vue({
el: "#table",
data: {
surveys: cache.data // Points to the single source of truth
},
methods: {
refresh: function(){
var self = this;
// After getting data back from an ajax call
.done(function(surveys) {
self.surveys = surveys; // Problem is right here
});
},
}
});
var vue2 = new Vue({
el: "#table",
data: {
surveys: cache.data // Points to the single source of truth
},
methods: {
// Methods
}
});
Try this example, which works like you code - not correct way:
var cache = {
key1: 'Value1'
}
var vue1 = new Vue({
el: '#app1',
data: {
surveys: cache
},
methods: {
replace () {
this.surveys = {key1: 'Replaced'}
}
}
})
var vue2 = new Vue({
el: '#app2',
data: {
surveys: cache
},
methods: {
replace () {
this.surveys = {key1: 'Replaced'}
}
}
})
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
<div id="app1">
Input for Vue1: <input type="text" v-model="surveys.key1">
<button #click="replace">Replace</button>
<p>{{ surveys.key1 }}</p>
</div>
<div id="app2">
Input for Vue1: <input type="text" v-model="surveys.key1">
<button #click="replace">Replace</button>
<p>{{ surveys.key1 }}</p>
</div>
Then try this example, with Vuex, where you can freely replace "cache object" and replacint will affect other instance:
const store = new Vuex.Store({
state: {
cache: {
key1: 'Value1'
}
},
mutations: {
replace (state) {
state.cache = {key1: 'Replaced'}
}
}
})
var vue1 = new Vue({
el: '#app1',
store,
computed: {
surveys () {
return this.$store.state.cache
}
},
methods: Vuex.mapMutations([
'replace'
])
})
var vue2 = new Vue({
el: '#app2',
store,
computed: {
surveys () {
return this.$store.state.cache
}
},
methods: Vuex.mapMutations([
'replace'
])
})
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex#2.4.0/dist/vuex.min.js"></script>
<div id="app1">
Input for Vue1: <input type="text" v-model="surveys.key1">
<button #click="replace">Replace</button>
<p>{{ surveys.key1 }}</p>
</div>
<div id="app2">
Input for Vue1: <input type="text" v-model="surveys.key1">
<button #click="replace">Replace</button>
<p>{{ surveys.key1 }}</p>
</div>
As said in the comment before, you can use vuex to accomplish what you need, everytime you need to pass data between diferent components you can do that with a eventBus or passing props up and down between the components.
When you have a aplication that needs to pass a lot of data and receive it you can use vuex, first you need to install it and then you can do it this way:
you should cut the methods out and place the mounted(), it fires when the component loads, i think it was you need
var vue1 = new Vue({
el: "#table",
data: {
surveys: cache.data // Points to the single source of truth
},
methods: {
}.
mounted() {
var self = this;
// After getting data back from an ajax call
.done(function(surveys) {
self.surveys = surveys;
});
}
});
when you get the response pass it to vuex store, you can do it with a mutation like this:
this.$store.mutation('handlerFunction', self.surveys)
in the vuex you need to have the handlerfunction inside the mutation
mutations: {
// appends a section to the tree
handlerFunction: (state, dataReceived) => {
//then you can do
state.surveys = dataReceived
},
then in your other component you can receive it via a getter, the logic is the same watch vuex for more deaills, you have the main logic of connection here.
Hope it helps!

Parent's data change does not update child component in vuejs

I have the following:
Vue.component('times-updated', {
template: '<span>Times Updated: {{ timesUpdated }}</span>',
data: function() {
return {
timesUpdated: this.$parent.myData.timesUpdated
}
}
});
var vm = new Vue({
el: '#test',
data: function() {
return {
myData: {}
}
}
})
setInterval(function(){
$.ajax({
url: `${window.location.href}/json`, // This just returns an array : array.timesUpdated: 2 etc
}).done(function (data) {
vm.myData = data; // changes this data
});
}, 1000)
and am using the following html:
<div class="test">
<times-updated></times-updated>
</div>
I poll a REST API that returns an array which includes a timesUpdated property:
{
timesUpdated: 5
}
My intention is that every second I use jQuery's $.ajax method to call the API, update the myData data object on vm, which would then update the times-updated component.
The code works on initial page load, the times-updated component can retrieve the value on its parent's myData property, but whilst I have confirms that vm.myData does reflect the new value from the API, the component doesn't update its display to show the new count.
What am i doing wrong?
The data function is only called once during the life cycle of the component; when it is initially created. So essentially your component is just displaying the value as it existed when the component was created.
Additionally, it's generally bad practice to reach out of a component to get a data value. Vue is props down, events up. You should convert your component to use a property.
Vue.component('times-updated', {
props:["times"],
template: '<span>Times Updated: {{ times }}</span>',
})
The fact that you are using a function to define the Vue in this particular case doesn't really matter, it's just not a typical practice. Components require a function because they need an isolated scope.
Here is an example.
That callback is required only in components
// vue instance
new Vue({
data: {
status: true
}
};
// vue components (callback)
Vue.component('custom-component', {
data: function() {
return {
status: false
}
}
});

how to determine if my ractive computed value has changed

For my Layout I have a component which needs to be initialized once rendering is completed, and then again if anything in my data changes.
This would work great, but I rely on computed values for a filtered and changed output for each client and via observe the change event is fired to often. What I do:
let myRactive = new Ractive({
el: '#clientContainer',
template: myTemplate,
magic: true,
modifyArrays: true,
data: {data}, //=> this is an awful lot of Data changing all the Time
computed: {
usefulData(){
let tempdata = this.get('data');
//=> a lot of rearranging, filtering and sorting
// to adapt the data only for this specific client
return tempdata;
}
onrender: function () {
initmyLayoutComponent()
}
});
So I tried to get it this way
myRactive .observe( 'usefulData', function ( newValue, oldValue, keypath)
destroymyLayoutComponent();
initmyLayoutComponent();
});
But this fired a) every time anything in datachanges (even when it was something completely unrelated to usefulData), and b) before ractive has rendered the DOM so the component gets re-initialized to early.
Is there a way to observe only the computed value, or - which would be even better - just observe specific actions in the computed value (like I want to react to added/deleted Objects, but not to changed Values)?
Well what you can do, is to actually send in a clientData obj into the template instead, and then only listen to that data.
let myRactive = new Ractive({
el: '#clientContainer',
template: '<div>{{clientData.name}}</div><input type="text" value="{{clientData.name}}" /><div>{{email}}</div><input type="text" value="{{email}}" /><div>Clientdata changed: {{cnt}}</div>',
magic: true,
modifyArrays: true,
data: {
name: 'hallo',
email: 'a#a.com',
cnt: 0
}, //=> this is an awful lot of Data changing all the Time
computed: {
usefulData() {
let tempdata = this.get('name');
// Create your own data obj
tempdata = {
name: 'world'
};
// set it to the ractive context
this.set('clientData', tempdata);
}
},
oninit: function() {
this.observe('clientData', function(newValue, oldValue, keypath) {
let cnt = this.get('cnt')
cnt += 1;
this.set('cnt', cnt);
console.log('listen only to the computed data');
}, {init: false});
this.get('usefulData');
},
onrender: function() {
// do something
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.9.0-build-123/ractive.min.js"></script>
<div id="clientContainer"></div>

Vue JS data availability among components

I've just stared playing with vueJS inside a laravel app.
I am trying to create a steps wizard and I'm having trouble accessing data from the vue definition inside one of the components.
I have:
new Vue({
el: '#app',
data: {
currentstep : 1,
...
chosenName: "",
And I would like to be able to access chosenName inside
Vue.component('step-controls', {
so that
<button class="btn btn-primary" v-on:click="nextStep()" :disabled="secondstep" v-if="firststep != true && laststep != true">Next</button>
Would be disabled if the value of chosenName is empty string
I would have imagined that it should be something like :
secondstep: function() {
return (this.currentstep == 2 && this.chosenName =='')
},
but chosenName is not one of the props. If I am adding it to the props array, how do I keep it in sync?
Link to fiddle: https://jsfiddle.net/angelin8r/oxse2p3v/3/
Using your fiddle, made the following changes
<step-controls
v-for="step in steps"
:step="step"
:stepcount="steps.length"
:currentstep="currentstep"
#step-change="stepChanged"
:chosen-name="chosenName">
</step-controls>
and
props: ['step', 'stepcount', 'currentstep','chosenName'],
By binding chosenName to the parent property, it will automatically update when the parent's property updates.
Your first problem is that the data property of your Vue element needs to be a function. That function will return an object with your currentStep and currentName. See the docs for reference
So, your component would look like
new Vue({
el: '#app',
data() {
return {
currentStep : 1,
chosenName: "",
};
},
})
Then, you can access and update the data with this.currentStep or this.chosenName

Categories