Rendering components in v-for issue - vuejs2

I'm having trouble to figure out how Vue 2 is rendering components within a v-for directive.
Here is a simplified snippet of what I'm working on, a light virtual scroll table.
Codepen
<div id="app">
<div #wheel.prevent="onWheel">
<table>
<tbody>
<tr v-for="item of visibleItems"
:key="item.id">
<td>{{ item.id }}</td>
<td><input :value="item.name"/></td>
</tr>
</tbody>
</table>
</div>
</div>
new Vue({
el: "#app",
data: {
start: 0,
count: 10,
},
computed: {
items() {
return [...Array(100).keys()]
.map((i) => i + 1)
.map((i) => ({
id: i,
name: `Item ${i}`,
}))
},
visibleItems() {
return this.items.slice(this.start, this.start + this.count)
}
},
methods: {
onWheel(event) {
if (event.deltaY < 0)
this.start = Math.max(0, this.start - 1)
else if (event.deltaY > 0)
this.start = Math.min(this.items.length - this.count, this.start + 1)
},
}
})
With this code, when I scroll up, only the top rows are rendered, leaving the others untouched, but when I scroll down, each row is re-rendered.
The node rendering can be seen in navigator console, the nodes are highlighted when they change.
The thing is, I have inputs in the second column, when one is focused and I scroll up, the focus remains, but when I scroll down, because the row is rendered, the input loose it's focus.
Why all rows are rendered instead of only the ones at the end ? How could I tell Vue to render only the new rows ?

Related

changing a single value using v-model / full table is redrawn

I was building an editable table, which began to crawl to a halt when the number of rows started to run in the 100's. This led me to investigate what was going on.
In the example below, when changing the value in the input, the whole table is redrawn, and the ifFunction() function is trigged 4 times.
Why is this happening? Shouldn't Vue be capable of just redrawing the respective cell? Have I done something wrong with the key-binding?
<template>
<div id="app">
<table border="1" cellpadding="10">
<tr v-for="(row, rowKey) in locations" :key="`row_+${rowKey}`">
<td v-for="(column, columnKey) in row" :key="`row_+${rowKey}+column_+${columnKey}`">
<span v-if="ifFunction()">{{ column }}</span>
</td>
</tr>
</table>
<input v-model="locations[0][1]">
</div>
</template>
<script>
export default {
data() {
return {
locations: [
["1","John"],
["2","Jake"]
], // TODO : locations is not generic enough.
}
},
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
}
}
</script>
The data property defines reactive elements - if you change one part of it, everything that's depending on that piece of data will be recalculated.
You can use computed properties to "cache" values, and only update those that really need updating.
I rebuilt your component so computed properties can be used throughout: created a cRow and a cCell component ("custom row" and "custom cell") and built back the table from these components. The row and the cell components each have a computed property that "proxies" the prop to the template - thus also caching it.
On first render you see the ifFunction() four times (this is the number of cells you have based on the data property in Vue instance), but if you change the value with the input field, you only see it once (for every update; you may have to click "Full page" to be able to update the value).
Vue.component('cCell', {
props: {
celldata: {
type: String,
required: true
},
isInput: {
type: Boolean,
required: true
},
coords: {
type: Array,
required: true
}
},
data() {
return {
normalCellData: ''
}
},
watch: {
normalCellData: {
handler: function(value) {
this.$emit('cellinput', {
coords: this.coords,
value
})
},
immediate: false
}
},
template: `<td v-if="ifFunction()"><span v-if="!isInput">{{normalCellData}}</span> <input v-else type="text" v-model="normalCellData" /></td>`,
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
},
mounted() {
this.normalCellData = this.celldata
}
})
Vue.component('cRow', {
props: {
rowdata: {
type: Array,
required: true
},
rownum: {
type: Number,
required: true
}
},
template: `
<tr>
<td
is="c-cell"
v-for="(item, i) in rowdata"
:celldata="item"
:is-input="!!(i % 2)"
:coords="[i, rownum]"
#cellinput="reemit"
></td>
</tr>`,
methods: {
reemit(data) {
this.$emit('cellinput', data)
}
}
})
new Vue({
el: "#app",
data: {
locations: [
["1", "John"],
["2", "Jake"]
], // TODO : locations is not generic enough.
},
methods: {
updateLocations({
coords,
value
}) {
// creating a copy of the locations data attribute
const loc = JSON.parse(JSON.stringify(this.locations))
loc[coords[1]][coords[0]] = value
// changing the whole locations data attribute to preserve
// reactivity
this.locations = loc
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border="1" cellpadding="10">
<tbody>
<tr v-for="(row, i) in locations" is="c-row" :rowdata="row" :rownum="i" #cellinput="updateLocations"></tr>
</tbody>
</table>
<!-- <input v-model="locations[0][1]">
<input v-model="locations[1][1]">-->
{{locations}}
</div>

I have an issue with a vue component not rendering data

I am retrieving data in two steps. All data can be viewed in the Vue dev tools but data retrieved in the second step is not rendered.
My code is as follows:
data() {
return {
activeEmployees: {},
}
},
methods: {
loadEmployees() {
axios.get("/api/organizations/employees/" + this.$route.params.organizationId)
.then(({data}) => (
this.activeEmployees = data.active_employees
))
.then(() => (
this.getUserServices()
));
},
getUserServices() {
for (const element of this.activeEmployees) {
axios.get("/api/organizations/employees/services/" + element.id + "/" + this.$route.params.organizationId)
.then(response => Object.assign(element, response.data));
}
},
},
mounted() {
this.loadEmployees()
}
The data from the getUserServices method (user_services as per screenshot below) is not rendered yet it appears in the Vue dev tools.
Part of the template is as follows:
<tr :key="employee.id" v-for="employee in activeEmployees">
<td class="text-center">
<Avatar :userData="employee"/>
</td>
<td>
<div>{{employee.name}}</div>
<div class="small text-muted">
Registered: {{employee.created_at | formatDate}}
</div>
</td>
<td>
{{employee.user_services}}
</td>
</tr>
And a snapshot of the Vue dev tools is as follows:
The user_services is empty in the view yet available in the dev tools.
How can this be refactored to render all data?
You should use the $set method somewhere to make your data reactive. For example:
axios.get("/api/organizations/employees/services/" + element.id + "/" + this.$route.params.organizationId)
.then(response => {
Object.keys(response.data).forEach(key => this.$set(element, key, response.data[key])
})

vue.js remove object from list with two-way binding

I have an array of items that I want to display as the rows of a table. However the rows themselves have inputs, allowing the properties of those items to be edited and emitted back up through v-model into the parents rows array. when i try to delete a row through button in the child that emits the removeRow event, the correct item in the parent's array is deleted but the html only removes the last row of the array.
<tbody>
<row-component
v-for="(row, idx) in rows"
:key="idx"
v-model="rows[idx]"
#removeRow="expenses.splice(idx, 1)"
></row-component>
</tbody>
I think I'm doing something fundamentally wrong, but can't seem to figure out how else to menage a dynamic list of javascript objects that are bound via v-model (or otherwise).
I'll try and prepare a codepen with my problem.
Do not use array indexes to identify elements (keys) in Vue, because with some operations this numbering refreshes. After removing an element from the center of the array, the elements after this position receive a new index value. After removing an element from a 5-element array, with indexes from 0 to 4, you get a 4-element array with indexes from 0 to 3. Vue sees this change as deleting key 4 (last row). Create your own indexes when adding data inside this data.
Vue.component("table-component", {
template: "#table-template",
data() {
return {
rows: [
{ id: 1, name: "A" },
{ id: 2, name: "B" },
{ id: 3, name: "C" },
{ id: 4, name: "D" },
{ id: 5, name: "E" }
]
};
},
methods: {
removeRow(id) {
this.rows = this.rows.filter(row => row.id !== id);
}
}
});
Vue.component("row-component", {
template: "#row-template",
props: ["value"]
});
new Vue().$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table-component :rows="rows" />
</div>
<script type="text/x-template" id="table-template">
<table v-if="rows.length" border="1" cellpadding="3" cellspacing="3">
<tbody>
<row-component v-for="(row, idx) in rows" :key="row.id" v-model="rows[idx]" #removeRow="removeRow"></row-component>
</tbody>
</table>
</script>
<script type="text/x-template" id="row-template">
<tr>
<td>{{value.id}}</td>
<td>{{value.name}}</td>
<td><button #click="$emit('removeRow', value.id)">X</button></td>
</tr>
</script>

vuejs2 how to get v-for items to display at specific intervals?

Say I have a group of cars and I want to display each row...3 seconds at a time. How can I do this in Vuejs2?
<tbody>
<tr v-for="(car) in cars">
<td><img v-bind:src="car.photo" width="40px" height="40px" alt=""></td>
<td><router-link :to="{path:'/car/' + car.id}" >{{ car.name }}</router-link></td>
<td>{{ car.make }}</td>
<td></td>
<td>{{ car.created }}</td>
</tr>
</tbody>
something like this.
stored what to show currently in currentCarIndex.
use setInterval to change currentCarIndex every 3 seconds
btw, v-for and v-if shouldn't be used together, so I add a <template> tag as an empty wrapper to execute v-for
<template>
<tbody>
<template v-for="(car,i) in cars">
<tr :key="i" v-if="i<=currentCarIndex">
<td><img v-bind:src="car.photo" width="40px" height="40px" alt=""></td>
<td>
<router-link :to="{path:'/car/' + car.id}">{{ car.name }}</router-link>
</td>
<td>{{ car.make }}</td>
<td></td>
<td>{{ car.created }}</td>
</tr>
</template>
</tbody>
</template>
<script>
export default {
data() {
return {
currentCarIndex: 0,
cars: "..."
};
},
mounted() {
const interval = setInterval(() => {
if (this.currentCarIndex + 1 < this.cars.length) this.currentCarIndex++;
else clearInterval(interval);
}, 3000);
}
};
</script>
I was having this exact problem a couple of hours ago on an app I'm working on. I have a list of reviews and I wanted the reviews to display at interval so that it looks like the list is 'filled in' top down so that I can create a cascading effect. Something like this:
The documentations points out that you can use transition-group but personally I wasn't able to get them working for me so what I did is I created a wrapper component with a delay property on it and I passed in the time the component should wait before rendering. I did this using a simple v-if in the component's template.
What you could do is add a show-in and visible-for prop to a wrapper component like this:
<flashing-row v-for="(car, i) in cars" :show-in="i * 3000" :visible-for="2900">
// Stuff inside my row here....
</flashing-row>
and then define flashing-row like this:
Vue.component('flashing-row', {
props: {
showIn: {
type: Number,
required: true,
},
visibleFor: {
type: Number,
required: true,
},
},
data() {
return {
isVisible: false,
};
},
created() {
setTimeout(() => {
// Make component visible
this.isVisible = true;
// Create timer to hide component after 'visibleFor' milliseconds
setTimeout(() => this.isVisible = false, this.visibleFor);
}, this.showIn);
},
template: '<tr v-if="isVisible"><slot></slot></tr>'
});
You can see an example of the code in JSFiddle. This approach is especially good because:
You don't repeat yourself if you're going to be doing this at more than one place.
Makes your code more maintainable and easier to browse, read, and thus understand and modify later on.
And of course you can play around with the props and expand on it depending on what you need. The possibilities are really endless.

Vue: Style list row when selected

I have a list of components called client-row and when I select one I want to change the style. I run into an issue when trying to remove the styling from the previously selected row, when selecting a new row.
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
},
data: function() {
return {
selected: false
}
},
methods: {
select: function() {
// Does not work properly
el = document.querySelector('.chosen_row')
console.log(el)
if ( el ) {
el.className = el.className - "chosen_row"
}
this.selected = true
this.$emit('selected', this.client)
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': selected }">
<td>{{ client.name }}</td>
<td>{{ client.location_name || 'no location found' }}</td>
<td>{{ client.email || 'no email found' }}</td>
<td><button class="btn btn-sm btn-awaken" #click="select()">Select</button></td>
</tr>
</script>
I can properly set the selected property, but cannot seem to remove it reliably.
It is generally bad practice to manually modify DOM elements in components. Instead, I recommend that you change the parent component to have a data field to keep track of which row is selected, and to pass that value into the rows. The row would then check whether its value matches the parent's selected row and apply style if true.
DOM manipulation in components is a sign you are doing things very wrong in vue.
In your case, vue and your manually DOM manipulation are battling each other. Vue is tracking whether to add the chosen_row class on the tr based on whether the child's data field selected is true or not. In your code, you are only ever setting it to true. Vue will always try to include the class for any row you have clicked. Then you are manually removing the class from all rows that were previously clicked, however Vue will still try to add the class because selected is still true in the child components that have been clicked.
You need to do a data oriented approach rather than a DOM manipulation based approach.
Child:
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
selectedClient: Object
},
methods: {
select: function() {
this.$emit('selected', this.client);
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': client === selectedClient }">
<!-- td's removed for brevity -->
<td><button class="btn btn-sm btn-awaken" #click="select">Select</button></td>
</tr>
</script>
Parent:
Vue.component('parent', {
template: '#parent',
data() {
return {
clients: [],
selectedClient: null
};
},
methods: {
clientSelected(client) {
this.selectedClient = client;
}
}
})
<script type="text/x-template" id="parent">
<!-- i dont know what your parent looks like, so this is as simple as i can make it -->
<div v-for="client in clients">
<client-row :client="client" :selected-client="selectedClient" #selected="clientSelected"></client-row>
</div>
</script>
In addition:
Your click event handler on the button can be shortened to #click="select" which is the recommended way of binding methods.