Increasing performance of v-data-table with custom cells and async data loading - performance

I'm creating a page with v-data-table. Some content of this table is loading at mounted stage, but the data for one column should be loaded line-by-line in the background by async API calls after rendering the whole table. Table rows should also be colored based on data returned from API call.
I've already developed this page, but stuck into one issue - when the table contains composite cells that was redefined by item slot (by example, a cell with icons, tooltips or spans), table row update time significantly increases.
According to business logic, the page may contain a large amount of rows, but I can't use v-data-table pagination to reduce entries count at one page.
The question is - how can I update row (in fact, just its color and one cell value) with a little performance loss as possible?
There is a Codepen with this problem. The way of loading data into the page is completely preserved in this Codepen, but API calls was replaced by promises with fixed timeout.
The problem still exists in Codepen. By default all the requests for 100 items have passed in 12-13 seconds (there's a counter at the bottom of the page). When I comment out last td, they're passed just in 7-8 seconds. When I comment out another one td (second from the end), they're passed in 6 seconds. When I increase items count to 1000, row update time is also increases.
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
headers: [
{
text: 'Dessert (100g serving)',
value: 'name',
},
{ text: 'Second name', value: 'secondName' },
{ text: 'Fat (g)', value: 'fat' },
{ text: 'Carbs (g)', value: 'carbs' },
{ text: 'Protein (g)', value: 'protein' },
{ text: 'Max value', value: 'maxValue' },
{ text: 'Actions', value: 'id' },
],
desserts: [],
timerStart: null,
loadingTime: null,
}
},
created() {
this.generateDesserts();
},
mounted() {
this.countMaxValues(this.desserts).then(() => {
this.loadingTime = (Date.now() - this.timerStart) / 1000;
});
},
methods: {
generateDesserts() {
let dessertNames = [
'Frozen Yogurt ',
'Ice cream sandwich ',
'Eclair',
'Cupcake',
'Gingerbread',
'Jelly bean',
'Lollipop',
'Honeycomb',
'Donut',
'KitKat',
null
];
for (let i = 0; i < 100; i++) {
let dessert = {
id: i,
name: dessertNames[Math.floor(Math.random() * dessertNames.length)],
secondName: dessertNames[8 + Math.floor(Math.random() * (dessertNames.length - 8))],
fat: Math.random() * 100,
carbs: Math.floor(Math.random() * 100),
protein: Math.random() * 10
};
this.desserts.push(dessert);
}
},
async countMaxValues(array) {
this.timerStart = Date.now();
for (const item of array) {
await this.countMaxValue(item).catch(() => {
//Even when one request throws error we should not stop others
})
}
},
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
item.maxValue = maxVal < 20 ? null : maxVal;
this.desserts.splice(item.id, 1, item);
});
}
}
})
<div id="app">
<v-app id="inspire">
<v-data-table
:headers="headers"
:items="desserts"
:footer-props='{
itemsPerPageOptions: [-1],
prevIcon: null,
nextIcon: null,
}'
>
<template v-slot:item="props">
<tr :style="{
background: (props.item.maxValue !== null && (props.item.carbs < props.item.maxValue))
? '#ffcdd2'
: (
(props.item.maxValue !== null && (props.item.carbs > props.item.maxValue)
? '#ffee58'
: (
props.item.maxValue === null ? '#ef5350' : 'transparent'
)
)
)}">
<td>{{ props.item.name || '—' }}</td>
<td>{{ props.item.secondName || '—' }}</td>
<td>{{ props.item.fat }}</td>
<td>{{ props.item.carbs }}</td>
<td>{{ props.item.protein }}</td>
<td>
<span>
{{ props.item.maxValue || '—' }}
</span>
<v-btn v-if="props.item.name && props.item.maxValue" icon>
<v-icon small>mdi-refresh</v-icon>
</v-btn>
</td>
<td class="justify-center text-center" style="min-width: 100px">
<v-tooltip bottom v-if="props.item.name && props.item.secondName">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
class="mr-2"
small
>
format_list_numbered_rtl
</v-icon>
</template>
<span>Some action tooltip</span>
</v-tooltip>
<v-tooltip bottom v-if="props.item.name && props.item.secondName">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
class="mr-2"
small
>
edit
</v-icon>
</template>
<span>Edit action tooltip</span>
</v-tooltip>
<v-tooltip bottom v-if="props.item.name === 'KitKat'">
<template v-slot:activator="{ on }">
<v-icon v-on="on"
small
>
delete
</v-icon>
</template>
<span>Delete action tooltip</span>
</v-tooltip>
</td>
</tr>
</template>
</v-data-table>
<p>{{ "Page loading time (sec): " + (loadingTime || '...') }}</p>
</v-app>
</div>

It seems Vue can update DOM more efficient if it is wrap in component (Sorry, I don't know in detail why).
This is your original code in JSFiddle. It will use around 12-13 seconds.
Then I create a component which wrap your entire tr:
const Tr = {
props: {
item: Object
},
template: `
<tr>
... // change props.item to item
</tr>
`
}
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
'tr-component': Tr // register Tr component
},
...
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
// update entire object instead of one property since we send it as object to Tr
let newItem = {
...item,
maxValue: maxVal < 20 ? null : maxVal
}
this.desserts.splice(newItem.id, 1, newItem);
});
}
})
And your html will looks like:
<v-data-table
:headers="headers"
:items="desserts"
:footer-props='{
itemsPerPageOptions: [-1],
prevIcon: null,
nextIcon: null,
}'>
<template v-slot:item="props">
<tr-component :item='props.item'/>
</template>
</v-data-table>
The result will use around 6-7 seconds which is only 1-2 seconds to update DOM.
Or if you find out that your function trigger very fast (In your example use 50ms which is too fast in my opinion) you could try throttle it to less update DOM.
...
methods: {
async countMaxValue(item) {
await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
let maxVal = Math.random() * 100;
let newItem = {
...item,
maxValue: maxVal < 20 ? null : maxVal
}
this.changes.push(newItem) // keep newItem to change later
this.applyChanges() // try to apply changes if it already schedule it will do nothing
});
},
applyChanges () {
if (this.timeoutId) return
this.timeoutId = setTimeout(() => {
while (this.changes.length) {
let item = this.changes.pop()
this.desserts.splice(item.id, 1, item)
}
this.timeoutId = null
}, 1500)
}
}
The result will use around 5-6 seconds but as you can see it's not immediately update.
Or you may try to call your API in parallel such as 10 requests, you could reduce from to wait 100 * 50 ms to around 10 * 50 ms (mathematically).
I hope this help.

Related

How to manipulate axios returned data to print in desired formate

<template>
<div>
<table class="table table-responsive">
<tbody>
<tr v-for="(gameresults, index) in gameresults" :key="index">
<td style="color: #082ad4; font-size: 24px;">{{ gameresults.game_name }}</br>
<h3 style="color:#00d2f1; font-size: 18px;">{{ gameresults.cards }}</h3></td>
<h3 style="color:#00d2f1; font-size: 18px;">{{ this.formattedArray }}</h3></td>
</tr>
</tbody>
</table>
</div></template>
<script>
export default {
props: [''],
mounted() {
console.log('Component mounted.')
},
data() {
return {
gameresults:0,
};
},
methods: {
changeResult() {
let formattedArray = [];
this.gameresults.cards.forEach(str => {
const subs = str.split('');
const subsTwo = subs[2].split(',');
const formattedString = `${subs[1]} - ${subs[0]}-${subsTwo[0]}${subs[4]}-${subsTwo[1]}`;
formattedArray.push(formattedString);
});
console.log('Formatted Data', formattedArray);
}
// this.gameresults[0].cards
},
computed: {
chkgameresults() {
axios.post('/gameresults')
.then(response => {
this.gameresults = response.data ;
this.changeResult();
});
},
},
created () {
this.chkgameresults();
}
};
</script>
ref code axios fetches mysql concat data in array format having 2 keys [game_name and card ] i want card key to be manipulated . when i print card array using this.gameresults[0].card its giving me 123-9,897-0 using {{ gameresults.cards }} inside vu template , i want value to get manipulated like 123-90-897 ( only last 0 gets before the second exp and become 0-897 removing ',' separator
Assuming that your data is an array lets define a method...........
formatData(myData) {
let formattedArray = [];
myData.myValue.forEach(str => {
const subs = str.split('');
const subsTwo = subs[2].split(',');
const formattedString = `${subs[1]} - ${subs[0]}-${subsTwo[0]}${subs[4]}-${subsTwo[1]}`;
formattedArray.push(formattedString);
});
console.log('Formatted Data', formattedArray);
}

Vuetify expansion panel, open state does not follow dataprovider

When the data array changes order, the state of panels that are open does not follow their data positions.
<div id="app">
<v-app id="inspire">
<div>
<div class="text-center d-flex pb-4">
<v-btn #click="changeOrder">Change order</v-btn>
<v-btn #click="removeItem">Remove item</v-btn>
</div>
<v-expansion-panels
v-model="panel"
multiple
>
<v-expansion-panel
v-for="(item, i) in items">
<v-expansion-panel-header>{{ item.name }}</v-expansion-panel-header>
<v-expansion-panel-content>
Lorem ipsum dolor sit amet.
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
</v-app>
</div>
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
panel: [],
items: [
{ id:1, name: 'James', },
{ id:2, name: 'Bernt', },
{ id:3, name: 'Julie', },
{ id:4, name: 'Veronica', },
],
}
},
methods: {
changeOrder () {
this.items = this.items.reverse();
},
removeItem () {
this.items.splice(0, 1);
},
},
})
https://codepen.io/Agint/pen/GRpmBxE
In the demo, open a panel and click the button, and you see the problem. Also same problem when you remove data from the list. If you have one panel open, and you remove it, the sibling is suddenly open.
How do I attack this problem?
Per the docs the value prop of the v-expansion-panels component:
Controls the opened/closed state of content in the expansion-panel. Corresponds to a zero-based index of the currently opened content. If the multiple prop (previously expand in 1.5.x) is used then it is an array of numbers where each entry corresponds to the index of the opened content. The index order is not relevant.
That means that which panel(s) are open has no connection to their content. If you reverse or change the order of the items array, you also need to update the panel array to adjust the open indexes accordingly:
methods: {
changeOrder () {
// reverse the elements in your items array
this.items = this.items.reverse()
// get the max index of elements in your items array
const maxIndex = this.items.length - 1
// set index of each open panel to its inverse
this.panel = this.panel.map(p => maxIndex - p)
},
removeItem () {
this.items.splice(0, 1)
// since the index of each item will be effectively reduced by 1
//
this.panel = this.panel.map(p => p - 1)
},
}

Implement vue draggable in a table. Laravel

I'm trying to create a survey with the option to order the questions that are displayed in a table when creating the survey.
I'm using vue draggable and the example works but I don't know how to use this with a table and still get the rows to be draggable
Example
<draggable v-model="section" #start="drag=true" #end="drag=false">
<div v-for="section in surveySections" :key="section.id">{{section.title}}</div
</draggable>
This is the table
<el-table
:data="form.question_id"
border>
<draggable v-model="surveyQuestions" #start="drag=true" #end="drag=false">
<el-table-column prop="title" label="Pregunta"></el-table-column>
<el-col :xs="5">
<el-table-column fixed="right" label="Operaciones">
<template slot-scope="scope">
<el-button
#click.native.prevent="deleteRow(scope.$index, form.question_id)"
type="text" size="small">
<span class="icon-create">Eliminar</span>
<i class="el-icon-delete-solid"></i>
</el-button>
</template>
</el-table-column>
</el-col>
</draggable>
</el-table>
How can I get this to work?
I must have 50 reputation to comment!
SO.
you can see elementUI Table组件实现拖拽效果
e.g
npm install sortablejs --save
// Element table must specify row-key . Otherwise, the order will be wrong
import Sortable from 'sortablejs'
<template>
<div style="width:800px">
<el-table :data="tableData"
border
row-key="id"
align="left">
<el-table-column v-for="(item, index) in col"
:key="`col_${index}`"
:prop="dropCol[index].prop"
:label="item.label">
</el-table-column>
</el-table>
<pre style="text-align: left">
{{dropCol}}
</pre>
<hr>
<pre style="text-align: left">
{{tableData}}
</pre>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
data() {
return {
col: [
{
label: '日期',
prop: 'date'
},
{
label: '姓名',
prop: 'name'
},
{
label: '地址',
prop: 'address'
}
],
dropCol: [
{
label: '日期',
prop: 'date'
},
{
label: '姓名',
prop: 'name'
},
{
label: '地址',
prop: 'address'
}
],
tableData: [
{
id: '1',
date: '2016-05-02',
name: '王小虎1',
address: '上海市普陀区金沙江路 100 弄'
},
{
id: '2',
date: '2016-05-04',
name: '王小虎2',
address: '上海市普陀区金沙江路 200 弄'
},
{
id: '3',
date: '2016-05-01',
name: '王小虎3',
address: '上海市普陀区金沙江路 300 弄'
},
{
id: '4',
date: '2016-05-03',
name: '王小虎4',
address: '上海市普陀区金沙江路 400 弄'
}
]
}
},
mounted() {
this.rowDrop()
this.columnDrop()
},
methods: {
//行拖拽
rowDrop() {
const tbody = document.querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
onEnd({ newIndex, oldIndex }) {
const currRow = _this.tableData.splice(oldIndex, 1)[0]
_this.tableData.splice(newIndex, 0, currRow)
}
})
},
//列拖拽
columnDrop() {
const wrapperTr = document.querySelector('.el-table__header-wrapper tr')
this.sortable = Sortable.create(wrapperTr, {
animation: 180,
delay: 0,
onEnd: evt => {
const oldItem = this.dropCol[evt.oldIndex]
this.dropCol.splice(evt.oldIndex, 1)
this.dropCol.splice(evt.newIndex, 0, oldItem)
}
})
}
}
}
</script>
<style scoped>
</style>
element ui table Sortable.js

How to use vuetify's custom sort?

I'd like to use custom-sort in my data table. My goal is to sort the table DESC as opposed to the default ASC. But I don't know-how.
This is the start of my data table component:
<v-data-table
:headers="headers"
:items="acts"
hide-actions
class="elevation-1"
>
<template slot="items" slot-scope="props">
<td>{{ props.item.id }}</td>
<td>{{ props.item.name }}</td>
<td class="text-xs-center">{{ props.item.provider.id }}</td>
<td class="text-xs-center">{{ props.item.category.name }}</td>
<td class="text-xs-center">{{ props.item.budget }}</td>
<td class="text-xs-center">{{ props.item.location.name }}</td>
<td class="text-xs-center">{{ props.item.deets }}</td>
<td class="text-xs-center">{{ props.item.keeping_it_100 }}</td>
<td class="text-xs-center"><img width="50" height="50" :src="props.item.inspiration.inspiration"></td>
<td class="justify-center layout px-0">....
And this is the script I'm using:
<script>
export default {
data () {
return {
dialog: false,
customerSort: {
isDescending: true,// I tried this? as the kabab format throws an error
},
headers: [
{ text: 'ID', value: 'id'},
{ text: 'Name', value: 'name' },
{ text: 'Provider', value: 'provider' },
{ text: 'Category', value: 'category' },
{ text: 'Budget', value: 'budget' },
{ text: 'Country', value: 'location', sortable: true },
{ text: 'Keeping it 100%', value: 'keeping_it_100', sortable: false },
{ text: 'deets', value: 'deets', sortable: false },
{ text: 'inspiration', value: 'inspiration', sortable: false },
{ text: 'Cover', value: 'cover', sortable: false },
{ text: 'Actions', value: 'actions', sortable: false }
],
According to docs it is a function prop. But I haven't found an example on how to pass it.
This is a screenshot of the function...
You can use a function like this-
customSort(items, index, isDesc) {
items.sort((a, b) => {
if (index === "date") {
if (!isDesc) {
return compare(a.date, b.date);
} else {
return compare(b.date, a.date);
}
}
});
return items;
}
Where the compare is a function which compares a.date and b.date and returns 1 or -1
isDesc is a variable passed by the table which tells in what order does the user want to sort it. If you want to sort in desc, just use !isDesc in the if-else condition
To use this in your template just use
<v-data-table
:headers="headers"
:items="Data"
:custom-sort="customSort"
>
<template slot="items" slot-scope="props">
<td class="font-weight-black">{{ props.item.date }}</td>
<td class="text-xs-right">{{ props.item.time }}</td>
<td class="text-xs-right">{{ props.item.name }}</td>
</template>
</v-data-table>
To make sure your other fields still work with the normal sort use
customSort(items, index, isDesc) {
items.sort((a, b) => {
if (index === "date") {
if (!isDesc) {
return dateHelp.compare(a.date, b.date);
} else {
return dateHelp.compare(b.date, a.date);
}
} else {
if (!isDesc) {
return a[index] < b[index] ? -1 : 1;
} else {
return b[index] < a[index] ? -1 : 1;
}
}
});
return items;
}
Although it's an old question ...
For special sorting of only one column, you could use the property sort in the headers array.
See also https://vuetifyjs.com/en/api/v-data-table/#headers
Like so:
// in data ...
headers: [
...
{
text: "Date",
sortable: true,
value: "date",
sort: (a, b) => a.time_stamp - b.time_stamp
},
...
]
use it like
<v-data-table
:headers="headers"
...
>
Based on this answer code about custom-filter, I tried using custom-sort.
Please refer to this answer if you apply it to your code.
By the following code, I have confirmed sorting when I click 'Calories' header.
My CodePen
new Vue({
el: '#app',
data() {
return {
food: [
{ name: 'Bakchoi', type: 'vegetable', calories: 100 },
{ name: 'Pork', type: 'meat', calories: 200 },
{ name: 'Chicken Thigh', type: 'meat', calories: 300 },
{ name: 'Watermelon', type: 'fruit', calories: 10 },
],
headers: [
{ text: 'Name', align: 'left', value: 'name' },
{ text: 'Food Type', align: 'left', value: 'type' },
{ text: 'Calories', align: 'left', value: 'calories' },
],
search: '',
};
},
methods: {
customSort(items, index, isDescending) {
// The following is informations as far as I researched.
// items: 'food' items
// index: Enabled sort headers value. (black arrow status).
// isDescending: Whether enabled sort headers is desc
items.sort((a, b) => {
if (index === 'calories') {
if (isDescending) {
return b.calories - a.calories;
} else {
return a.calories - b.calories;
}
}
});
return items;
}
}
})
<script src="https://unpkg.com/vue#2.4.2/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#0.15.2/dist/vuetify.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vuetify#0.15.2/dist/vuetify.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons">
<div id="app">
<v-app>
<v-select
label="Food Type"
:items="['vegetable', 'meat', 'fruit']"
v-model="search"
></v-select>
<v-data-table
:headers="headers"
:items="food"
:search="search"
:custom-sort="customSort"
hide-actions
>
<template slot="items" scope="{ item }">
<td>{{ item.name }}</td>
<td>{{ item.type }}</td>
<td>{{ item.calories }}</td>
</template>
</v-data-table>
</v-app>
</div>
NOTE: the following answer is for Vuetify 1.5.x
A little late to the party here, if all you want to do is sort descending by a single field, then custom-sort it not what you want to use, you're better off using the :pagination.sync prop
Custom sort is used when you want to change the behaviour of the comparison function (e.g sorting based off the reverse or lowercase version of a string, or proper sorting of date strings in the format 'DD-MM-YYYY').
If you want to use the default descending functionality, use the :pagination.sync prop, like so:
<v-data-table
:headers="headers"
:items="acts"
:pagination.sync="pagination"
>
<template v-slot:items="props">...</template>
</v-data-table>
In your script, set pagination:
data () {
return {
pagination: {
sortBy: 'id', // The field that you're sorting by
descending: true
}
}
}
This specifies that you want the table to be initially sorted by descending id - id can be changed to any field name in the dataset.
It's worth noting that this only specifies the default behaviour, and if you have sorting enabled for your other headers, users can still sort the table by any field.
To Build on the response provided by bhaskar
I had to edit the last code sample to the following in order to work on vuetify 2.x. The code sorts the date columns by their epoch time which is stored under the time_stamp key. The code also allows the default sorting of numbers and strings (strings are sorted alphabetically)
customSort(items, index, isDesc) {
items.sort((a, b) => {
if (index[0] == "date") {
if (!isDesc[0]) {
return a.time_stamp - b.time_stamp;
} else {
return b.time_stamp - a.time_stamp;
}
} else if (!(isNaN(a[index[0]]))) {
if (!isDesc[0]) {
return (a[index[0]] - b[index[0]]);
} else {
return (b[index[0]] - a[index[0]]);
}
} else {
if (!isDesc[0]) {
return (a[index[0]] < b[index[0]]) ? -1 : 1;
} else {
return (b[index[0]] < a[index[0]]) ? -1 : 1;
}
}
});
return items;
}
In vuetify 2 just use sortBy="date" and update: sort-desc
<v-data-table
:headers="headers"
:items="acts"
:pagination.sync="pagination"
sortBy="date"
update: sort-desc
>

Expand all data table entries at once in VuetifyJS/VueJS

How to expand all entries of this VuetifyJS/VueJS data table example at once and not only one at the time?
<div id="app">
<v-app id="inspire">
<v-data-table
:headers="headers"
:items="desserts"
hide-actions
item-key="name"
expand
>
<template slot="items" slot-scope="props">
<tr #click="props.expanded = !props.expanded">
<td>{{ props.item.name }}</td>
<td class="text-xs-right">{{ props.item.calories }}</td>
<td class="text-xs-right">{{ props.item.fat }}</td>
</tr>
</template>
<template slot="expand" slot-scope="props">
<v-card flat>
<v-card-text>Peek-a-boo!</v-card-text>
</v-card>
</template>
</v-data-table>
</v-app>
</div>
Here is an example for a single expand:
https://codepen.io/anon/pen/yEWNxE?&editors=101#
There is an open-issue with regards to this feature, make sure to follow it and get notified when it's resolved.
Temporary solution by #zikeji follows:
Add reference to the table:
<v-data-table ref="dTable">
Expand rows manually when component loads:
mounted() {
for (let i = 0; i < this.desserts.length; i += 1) {
const item = this.desserts[i];
this.$set(this.$refs.dTable.expanded, item.name, true);
}
},
Codepen
in Veutify v2.1.13
Just copied data to expanded from dessertson click.
methods: {
expandAll: function() {
console.log("All expanded.");
this.$data.expanded = this.$data.desserts;
},
collapseAll: function() {
console.log("All collapsed.");
this.$data.expanded = [];
}
},
codepen
You should extend the component instead and set the values as such.
MyVDataTable.vue
<script>
import VDataTable from 'vuetify/src/components/VDataTable'
export default {
extends: VDataTable,
props: ['deserts'],
mounted () {
for (let i = 0; i < this.desserts.length; i += 1) {
const item = this.desserts[i];
this.$set(this.expanded, item.name, true);
}
}
}
Then you would replace your VDataTable with this one.
For me the above and other solutions found on google were not working. So i created another approach.
To the row where you usually add your #click expand function you have to add a custom directive.
for example v-open
and pass your props.
<tr #click="props.expanded = !props.expanded" v-open="props">
then inside your component
directives: {
open: {
// directive definition
bind: function (el,binding) {
//only the first row
if( binding.value.index === 0){
binding.value.expanded = true
}
//or all rows
//binding.value.expanded = true
}
}
},

Resources