How to use vuetify's custom sort? - sorting

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
>

Related

Vuetify - How to access element in v-data-table at current cursor position

I'm using v-data-table to list locations that are also shown on a map.
It would be nice to synchronize the highlighting so that if the cursor hovers over a
location in the table, the same location would be highlighted on the map.
Is there a way to access the element in v-data-table where the cursor is currently hovering over?
As one of the possible solutions you can override #items slot and include native mouseover event into <tr> tag:
<span class="text-h6">{{ `Last hovered location: ${hoveredLocation}` }}</span>
<v-data-table
:headers="headers"
:items="locations"
:items-per-page="5"
>
<template #item="{ item }">
<tr #mouseover="onMouseOver(item)">
<td>{{ item.city }}</td>
<td>{{ item.country }}</td>
</tr>
</template>
</v-data-table>
...
data () {
return {
hoveredLocation: null,
headers: [
{ text: 'City', value: 'city' },
{ text: 'Country', value: 'country' },
],
locations: [
{ id: 3, city: 'Paris', country: 'France'},
...
],
}
},
methods: {
onMouseOver(item) {
this.hoveredLocation = `${item.city} (${item.country})`;
}
}
Possibly you need to combine several events like mouseover/out, mouseenter/leave, etc.
Test this at CodePen.

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

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.

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 alter a value in a v-data-table

I am new to vuetify and I'm trying to take the value in a column of a v-data-table and convert it to it's text equivalent, but can't find out how to do it.
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Type', value: 'typeName(type)' },
]
I have tried typeName as a computed value and as a method:
typeName(typId) {
return ...
},
How do I get this to work?
Yes, you can format the column value by adding a explict function
Here is the working codepen which reverse the value of first column: https://codepen.io/chansv/pen/GRRjaqj?editors=1010
If you have headers type, no need to call the function from here,
instead we need to call from column by adding a template
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Type', value: 'type' },
]
In the HTML , add a template with slot points to body
<v-data-table
:headers="headers"
:items="items" >
<template v-slot:body="{items}">
<tbody>
<tr v-for="item in items" :key="item.name">
<td>{{item.name}}</td>
<td>{{typeName(item.type)}}</td>
</tr>
</tbody>
</template>
</v-data-table>
Inside data property add a property typeName
data() {
return {
typeName: (type) => type.substring(2),
}
}

Pushing ajax data to vue component

Having the following vue component
<employee-times :employees="{{$employees}}" :supplies="{{$supplies}}" :commits="{{$commits}}" :times="{{$times}}"></employee-times>
where I render the passed props
<template>
<div>
<select class="form-control input" v-model="currentYear" #change="filter()">
<option v-for="option in yearsOptions" v-bind:value="option">
{{ option }}
</option>
</select>
<tr v-for="employee,key in _employees" v-if="commits[key]">
<td>{{ key }}</td>
<td>{{ employee[0].first_name }}</td>
<td>{{ employee[0].last_name }}</td>
<td>{{ employee[0].nick }}</td>
<td>{{ employee[0].role }}</td>
<td>{{ employee[0].skill }}</td>
<td v-for="n in 12">
<div v-if="_commits[key][n]">{{ _commits[key][n].hours }}</div>
<div v-else> </div>
</td>
</tr>
</div>
</template>
than I try to filter the ajax data on change but the data will not update
here is the script what I'm trying, but from method function I'm not able to push the new data to the template
<script>
export default {
name: 'employee-times',
props: ['supplies', 'times', 'commits', 'employees'],
components: {
},
created() {
axios.get('/api/v1/roles', {
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
}).then(response => {
th
is.roles = response.data
}).catch(error => {
})
this._times = this.times;
this._commits = this.commits;
this._supplies = this.supplies;
this._employees = this.employees;
},
data() {
return {
count: 0,
yearsOptions: [],
_employees: {},
_supplies: {},
_times: {},
_commits: [],
currentYear: null,
currentStatus: 1,
currentPosition: 0,
statusOptions: [
{'id': '1',
'text': 'Active'
}, {'id': '0',
'text': 'Inactive'
}],
currentSkillset: 'all',
skillsetOptions: [
{'id': 'all',
'text': 'All'
}, {'l1': 'l1',
'text': 'L1'
}, {'l1': 'l2',
'text': 'L2'
}, {'l1': 'l3',
'text': 'L3'
}, {'l1': 'l4',
'text': 'L4'
}, {'l1': 'l5',
'text': 'L5'
}],
status: {},
roles: {},
skillsets: {}
};
},
mounted() {
this.currentYear = moment().format('Y')
var from = moment().subtract('4', 'years').format('Y')
var to = moment().add('2', 'years').format('Y')
while (from <= to) {
this.yearsOptions.push(from);
from++;
}
},
watch: {
'_employees': function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
}
},
methods: {
commit() {
},
clear() {
},
months() {
return moment.monthsShort()
},
filter() {
var data = {
year: this.currentYear,
status: this.currentStatus,
position: this.currentPosition,
//skill: currentSkillset
}
axios.post('/api/v1/supply/track-times', data, {
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
}).then(response => {
this._employees = {}
this.$set(this, '_employees', JSON.parse(response.data.employees))
this.$set(this,'_times', JSON.parse(response.data.times))
this.$set(this,'_supplies', JSON.parse(response.data.supplies))
this.$set(this, '_commits', JSON.parse(response.data.commits))
}).catch(error => {
})
}
},
};
</script>
What i missed in this case?
There's a problem with v-for and tables (ref 'v-for with 2 every time') that requires a template wrapper.
See example CodePen.
<div id='app'>
<table>
<template v-for="(employee, key) in employees">
<tr>
<td>{{ employee.first_name }}</td>
<td>{{ employee.last_name }}</td>
</tr>
</template>
<table>
</div>
It also appears that you do need to remove the underscores (try changing employee to _employee in the codepen).
Remove the "_" prefix from your data properties, then it should work. Vue uses underscores for internal stuff, so best avoid using it (see https://v2.vuejs.org/v2/api/#data)

Resources