Vuejs2 - How to update the whole object from the server not losing reactivity? - vue.js

I have a list of objects that can be updated from the database.
So, when I load the list, objects have only id and name.
When I click on an object I load other fields that can be of any length - that's why I don't load them with the objects in the list.
I found that when I update an object it can be difficult to keep reactivity https://v2.vuejs.org/v2/guide/reactivity.html so I need to find some workaround.
this code works almost okay:
axios.get('/api/news', item.id).then(function(response){
if (response){
Object.assign(item, response.data.item);
}
});
But the problem is the fields that have not been presented from the beginning is not 100% reactive anymore. What is going on is a new field has been updated reactively only when I change another, previous one. So, if I show 2 text field with old and new properties, if I change the second property, the field will not be updated until I change the first one.
I got item object from the component:
data () {
return {
items: [],
}
},
and
<div v-for="item in items" #click="selectItem(item)" >
<span>{{item.name}}</span>
</div>
Then item's been passed to the function selectItem.
What is the proper pattern to load new fields and keep them reactive? (NB: it's not the case when I can assign field by field - I want to reuse the same code no matter which object it is, so I need so the solution for updating an object at a time without listing all new fields.)
Note. This code works inside the component.

Completely revised post: Ok, the example you give uses an array, which has its own caveats, in particular that you can't directly set values like vm.items[indexOfItem] = newValue and have it react.
So you have to use Vue.set with the array as the first argument and the index as the second. Here's an example that adds a name property to object items and then uses Vue.set to set the item to a new object created by Object.assign.
new Vue({
el: '#app',
data: {
items: [{
id: 1,
other: 'data'
}, {
id: 2,
other: 'thingy'
}]
},
methods: {
selectItem(parent, key) {
const newObj = Object.assign({}, parent[key], {
name: 'some new name'
});
Vue.set(parent, key, newObj);
setTimeout(() => {parent[key].name = 'Look, reactive!';}, 1500);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item, index in items" #click="selectItem(items, index)">
<span>{{item.name || 'empty'}}</span>
</div>
<pre>
{{JSON.stringify(items, null, 2)}}
</pre>
</div>

Have a look at Change-Detection-Caveats Vue cannot detect property addition or deletion if you use "normal assign" methods.
You must use Vue.set(object, key, value)
Try something like the following:
axios.get('/api/news', item.id).then(function(response){
if (response){
let item = {}
Vue.set(item, 'data', response.data.item)
}
});
Than item.data would than be reactiv.

Can simply use Vue.set to update this.item reactively
axios.get('/api/news', item.id).then(function(response){
if (response){
this.$set(this, "item", response.data.item);
}
});

Related

List of components not updating

I have this template displaying a list of components:
<template>
<v-container id="container">
<RaceItem v-for="(item, index) in items" :key="item + index" />
(...)
When the array "items" is updated (shift() or push() a new item), the list displayed is not updated.
I'm sure I'm missing something on how Vue works... but could anyone explain what's wrong?
The key attribute expects number | string | boolean | symbol. It is recommended to use primitives as keys.
The simplest usage is to use a primitive unique property of each element to track them:
<RaceItem v-for="item in items" :key="item.id" />
... assuming your items have unique ids.
If you use the index as key, every time you update the array you'll have to replace it with an updated version of itself (i.e: this.items = [...items]; - where items contains the mutation).
Here's how your methods would have to look in that case:
methods: {
applyChangeToItem(item, change) {
// apply change to item and return the modified item
},
updateItem(index, change) {
this.$set(
this.items,
index,
this.applyChangeToItem(this.items[index], change)
);
},
addItem(item) {
this.items = [...this.items, item];
},
removeItem(index) {
this.items = [...this.items.filter((_, i) => index !== i)];
}
}
Can you try something like this
Put componentKey on v-container and use forceRender() after your push is done

How can i splice a object from a "props" array and assign it to a predefined "data" attribute with it being accessible and reactive?

My Component:
props: {
answerOptions: Array
},
data: function() {
return {
selectedOption: null,
//selectedOption: {}, also not working
};
},
methods: {
select(option, i){
//this.selectedOption = this.answerOptions.splice(i, 1); not working
//Object.assign(this.selectedOption, this.answerOptions.splice(i, 1)); also not working
this.selectedOption = Object.assign({}, this.answerOptions.splice(i, 1)); //still not
working!
console.log(this.selectedOption) //prints a observable object with alle the correct values
console.log(this.selectedOption.anyAttribute) //prints undefined
}
edit:
thats where the select function is called:
<div
ref="option-elems"
v-on:click="select(option, i)"
v-for="option, i in answerOptions"
>
The answerOption array gets rendered via a for loop just fine. When the select() Method is called on one of the answerOption objects it removes it from the array and updates that rendered list in the ui correctly. I can even conditionally render (v-if="selectedOption") with the selectedOption Object. But for the love of god, im am not able to access any of its Attributes like so: {{ selectedOption.anyAttribute }}
What am i doing wrong here?
You need to get the object inside the array that is called "selectedOption". When you define this, you are still having the object wrapped in an array, so simply get its first item:
this.selectedOption = answers.splice(i, 1)[0];
Demo Here

Vue: Why aren't list's items responsive?

I have a list of items that I pass to the component as props. Then I want to create a computed list mapped on the props list. What I do:
computed: {
assets: function(){
var self = this;
var mappedList = [];
if (self.sourceList && self.sourceList.length){
mappedList = _.map(self.sourceList, function(asset){
return { title: asset.title, id: asset.id};
});
}
return mappedList;
}
}
I can see the mapped list then rendered:
<div v-for="asset in assets" :key="asset.id">
<span>{{asset.title}}</span>
<input v-model="source.title" />
</div>
I can't figure out why but when I change a title it's not changed for the item is being edited.
(If I try just create a new list and use push method to add items into it, the result is the same).
UPD Solution found. As #WW noted some data should be used. I just move the list to the data and watch props and it worked.

vue.js method can't access variable from data object with multiple rows

I am currently learning vue.js and having trouble accessing data in the methods.
data is loaded and set as a global variable (for now, this will probably change but not part of the problem now i think)
through ajax call this data is received:
data":[{"itemId":"58646f066803fa62388b4573","color":"#ffb878","name":"test1","startDate":"04/24/2017","work":"9.25"},{"itemId":"58646f066803fa62388b4572","color":"#ffb878","name":"test2","startDate":"04/24/2017","work":"4.25"},{"itemId":"58646f066803fa62388b4571","color":"#a4bdfc","name":"test3","startDate":"05/01/2017","work":"24.00"}]
which is set as a global (variable data is set outside of the functions) with:
...success: function (jsonObj)
{
data['item'] = jsonObj.data
....
now for the vue part:
var app = new Vue({
el:'#canvas',
data: {
items: data['item']
},
methods: {
moveItem: function(){
console.log("new date: "+this.startDate);
}
}
})
the html:
<div v-for="row in items" class="entirerow" v-bind:id="'row'+row.itemId">
<div class="itemrow">{{ row.name }}</div>
<div class="itemrow"><input type="text" v-model="row.startDate" #change="moveItem"></div>
<div class="itemrowlast">{{ row.work }}</div>
</div>
this nicely shows 3 rows with the correct data in each row. So far so good. But now if I change something in the input value the method moveItem is triggered but states "new date: undefined" in the console.log
I've tried console.log("new date: "+this.items.startDate) as well but no cigar and then it would seem the method wouldn't know which row is handled.
How can I access the correct data in the method, so from a certain row in the loop?
Thanks!
You refer to data object in method (this.startDate) not to item
moveItem: function(){
console.log("new date: "+this.startDate);
}
You can change your code like this
template
#change="moveItem(row)"
script
moveItem: function(row){
console.log("new date: " + row.startDate);
}

Sharing data from a VueJS component

I have a VueJS address lookup component.
Vue.component('address-lookup',
{
template: '#address-lookup-template',
data: function()
{
return {
address: {'name': '', 'town:': '', 'postcode': ''},
errors: {'name': false, 'town': false, 'postcode': false},
states: {'busy': false, 'found': false},
result: {}
}
},
methods:
{
findAddress: function(event)
{
if( typeof event === 'object' && typeof event.target === 'object' )
{
event.target.blur();
}
$.ajax(
{
context: this,
url: '/lookup',
data:
{
'name': this.address.name,
'town': this.address.town,
'postcode': this.address.postcode
},
success: function(data)
{
this.states.busy = false;
this.states.found = true;
this.address.name = data.name;
this.result = data;
}
});
},
reset: function()
{
this.states.found = false;
this.result = {};
}
}
});
Inside my template I've then bound the result like so:
<p>{{ result.formatted_address }}</p>
There is some extra data returned within the result (like a twitter handle) that isn't part of the address lookup template, and occurs on a separate part of the form. For reasons relating to how my form is structured I can't include these inputs within the same template.
I found a way to bind those inputs, although it felt somewhat 'hacky'.
<input type="text" name="twitter" v-model="$refs.lookupResult._data.result.twitter">
That all works fine.
My problem is that the form is included as part of a larger template sometimes in the context of creating a new record, sometimes in the context of editing. When editing a record, the lookup component is removed (using an if server-side, so the template is no longer loaded at all) and when that happens I get this error.
$refs.lookupResult._data.result.twitter": TypeError: Cannot read property '_data' of undefined
This makes sense. lookupResult is defined when I include the template, and when editing I am removing this line:
<address-lookup v-ref:lookup-result></address-lookup>
I've worked around it by including a version of each extra input without the v-model attribute, again using a server-side if. But there are quite a few of these and it's getting a bit messy.
Is there a cleaner approach I could be using to better achieve this?
So I don't know the hierarchy of your layout, it isn't indicated above, but assuming that address-lookup component is a child of your parent, and you in fact need the results of address lookup in that parent, eg:
<parent-component> <!-- where you need the data -->
<address-lookup></address-lookup> <!-- where you lookup the data -->
</parent-component>
then you can simply pass the data props, either top-down only (default) or bidirectionally by defining 'address' for example on your parent's vue data hook:
// parent's data() function
data = function () {
return {
address: {}
}
}
// parent template, passed address with .sync modifier (to make it bi-directional)
<parent-component>
<address-lookup :address.sync='address'></address-lookup>
</parent-component>
// have the props accepted in the address look up component
var addressComponent = Vue.extend({
props: ['address']
})
Now in your $.ajax success function, simply set the props you need on this.address. Of course you can do this with all the props you need: errors, results, state etc. Even better, if you can nest them into a single key on the parent, you can pass the single key for the object containing all four elements instead of all four separately.