List of components not updating - vue.js

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

Related

How to keep track of an array change in Vue.js when the index is a dynamic value?

I am building an app using Node.js and Vue.
My DATA for the component is the following:
data() {
return {
campaign: {
buses: [],
weeks: [
{
oneWayBuses: [],
returnBuses: []
}
]
},
busesMap: {
// id is the bus ID. Value is the index in the campaign.buses array.
},
};
},
I fill the buses and weeks array in MOUNTED section in two separate methods after getting the data from the server:
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
So the idea is that my busesMap looks like busesId keys and index values:
busesMap = {
'k3jbjdlkk': 0,
'xjkxh834b': 1,
'hkf37sndd': 2
}
However, when I try to iterate over weeks, v-if does not update so no bus info is shown:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[busesMap.busId]">
<strong>{{ campaign.buses[busesMap.busId].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap.busId].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
On the other side, if I shorten the v-if condition to campaign.buses, then I get into the condition but campaign.buses[busesMap.busId] is still undefined, so I get an ERROR trying to display busLabel and oneWayDepartureDate
I've read vue in depth documentation, but couldn't come up with a resolution.
Any gotchas you can find out?
Try this:
async mounted(){
await responseForWeeks
await responseForBuses
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
// this is partial since it is what you provided
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
}
Basically you want to make sure that before your component loads your data is in place. You can also create computed properties which will force re rendering if dependencies are changed and they are in the dom.
Actually, the problem was indeed in the HTML.
When trying to access the object keys, better use [] intead of a dot .
Final HTML result would be as follows:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[[busesMap[busId]]]">
<strong>{{ campaign.buses[busesMap[busId]].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap[busId]].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
What was happening is that previously campaign.buses[busesMap.busId] did not exist, thus not rendering anything. Solved to campaign.buses[busesMap[busId]]. Also used claudators for the displayed moustache sintach.
Hope it helps someone else messing with Objects!

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.

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

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);
}
});

Vue + Vue-Paginate: Array will not refresh once empty

I am using vue-paginate in my app and I've noticed that once my array is empty, refreshing its value to an array with contents does not display.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
>
<transition-group name="zoom">
<div v-for="recipe in paginated('recipes')" :key="recipe.id">
<recipe class=""
:recipe="recipe"
:ref="recipe.id"
></recipe>
</div>
</transition-group>
</paginate>
This is how things get displayed, and my recipe array changes depending on a search. If I type in "b" into my search, results for banana, and bbq would show. If I typed "ba" the result for bbq is removed, and once I backspace the search to "b" it would re-appear as expected.
If I type "bx" every result is removed and when I backspace the search to "b", no results re-appear.
Any idea why this might happen?
UPDATE
When I inspect the component in chrome I see:
currentPage:-1
pageItemsCount:"-15-0 of 222"
Even though the list prop is:
list:Array[222]
Paginate needs a key in order to know when to re-render after the collection it's looking at reaches a length of zero. If you add a key to the paginate element, things should function as expected.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
:key="recipes ? recipes.length : 0" // You need some key that will update when the filtered result updates
>
See "Filtering the paginated list" is not working on vue-paginate node for a slightly more in depth answer.
I found a hacky workaround that fixed it for my app. First, I added a ref to my <paginate></paginate> component ref="paginator". Then I created a computed property:
emptyArray () {
return store.state.recipes.length == 0
}
then I created a watcher that looks for a change from length == 0 to length != 0:
watch: {
emptyArray: function(newVal, oldVal) {
if ( newVal === false && oldVal === true ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator.goToPage(page)
}
}, 100)
}
}
}
The timeout was necessary otherwise the component thought there was no page 1.
Using :key in the element has certain bugs. It will not work properly if you have multiple search on the table. In that case input will lose focus by typing single character. Here is the better alternative:
computed:{
searchFilter() {
if(this.search){
//Your Search condition
}
}
},
watch:{
searchFilter(newVal,oldVal){
if ( newVal.length !==0 && oldVal.length ===0 ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator[0].goToPage(1)
}
}, 50)
}
}
},

VuesJS, generate randomkey in v-for loop

Good evening,
My problem is this: I have a loop that displays simple divs.
I have a method that specifies the dimensiosn of my div (mandatory in my case). However, when I call the method a second time by changing the sizes of the divs, it does not work because there is no re-render.
To overcome this, I generate a guid on my: key of v-for with a variable such as:
<div v-for="task in tasks" :key="task.id + guid()">...blabla...</div>
Is it possible to generate this code directly during the loop to avoid concatenation?
<div v-for="(task, maVar=guid()) in tasks" :key="maVar">...blabla...</div>
PS : code for guid() method :
guid() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16))
}
Thanks
You could create a computed property that returns an array of task with a guid added, or if you want to leave tasks untouched, return an object containing each task plus a guid,
computed: {
tasksWithGuid: function() {
return this.tasks.map(task => { return {task, key: task.id + guid() } })
}
}
<div v-for="taskWithGuid in tasksWithGuid" :key="taskWithGuid.key">
{{taskWithGuid.task.someProperty}}
</div>
There is a simpler, more concise technique shown below. It avoids polluting the iterated object with a redundant property. It can be used when there is no unique property in the objects you iterate over.
First in your viewmodel add the method to generate a random number (e.g. with Lodash random)
var random = require('lodash.random');
methods: {
random() {
return random(1000);
}
}
Then in your template reveal the index in v-for and randomize it in v-bind:key with your random() method from the viewmodel by concatenation.
<div v-for="(task, index) in tasks" v-bind:key="index + random()">
// Some markup
</div>
This is as clean as easy.
However note this approach would force redrawing each item in the list instead of replacing only items that differ. This will reset previously drawn state (if any) for unchanged items.
I do like this
function randomKey() {
return (new Date()).getTime() + Math.floor(Math.random() * 10000).toString()
}