Sorting a vue 3 v-for table composition api - vue.js

I have successfully created a table of my array of objects using the code below:
<div class="table-responsive">
<table ref="tbl" border="1" class="table">
<thead>
<tr>
<th scope="col" #click="orderby('b.property')">Property</th>
<th scope="col"> Price </th>
<th scope="col"> Checkin Date </th>
<th scope="col"> Checkout Date </th>
<th scope="col" > Beds </th>
</tr>
</thead>
<tbody>
<tr scope="row" class="table-bordered table-striped" v-for="(b, index) in properties" :key="index">
<td> {{b.property}} </td>
<td> {{b.pricePerNight}}</td>
<td> {{b.bookingStartDate}} </td>
<td> {{b.bookingEndDate}} <br> {{b.differenceInDays}} night(s) </td>
<td> {{b.beds}} </td>
</tr>
</tbody>
</table>
</div>
<script>
import {ref} from "vue";
import { projectDatabase, projectAuth, projectFunctions} from '../../firebase/config'
import ImagePreview from "../../components/ImagePreview.vue"
export default {
components: {
ImagePreview
},
setup() {
const properties = ref([]);
//reference from firebase for confirmed bookings
const Ref = projectDatabase .ref("aref").child("Accepted Bookings");
Ref.on("value", (snapshot) => {
properties.value = snapshot.val();
});
//sort table columns
const orderby = (so) =>{
desc.value = (sortKey.value == so)
sortKey.value = so
}
return {
properties,
orderby
};
},
};
</script>
Is there a way to have each column sortable alphabetically (or numerically for the numbers or dates)? I tried a simple #click function that would sort by property but that didn't work

you can create a computed property and return the sorted array.
It's just a quick demo, to give you an example.
Vue.createApp({
data() {
return {
headers: ['name', 'price'],
properties: [
{
name: 'one',
price: 21
},
{
name: 'two',
price: 3
},
{
name: 'three',
price: 5
},
{
name: 'four',
price: 120
}
],
sortDirection: 1,
sortBy: 'name'
}
},
computed: {
sortedProperties() {
const type = this.sortBy === 'name' ? 'String' : 'Number'
const direction = this.sortDirection
const head = this.sortBy
// here is the magic
return this.properties.sort(this.sortMethods(type, head, direction))
}
},
methods: {
sort(head) {
this.sortBy = head
this.sortDirection *= -1
},
sortMethods(type, head, direction) {
switch (type) {
case 'String': {
return direction === 1 ?
(a, b) => b[head] > a[head] ? -1 : a[head] > b[head] ? 1 : 0 :
(a, b) => a[head] > b[head] ? -1 : b[head] > a[head] ? 1 : 0
}
case 'Number': {
return direction === 1 ?
(a, b) => Number(b[head]) - Number(a[head]) :
(a, b) => Number(a[head]) - Number(b[head])
}
}
}
}
}).mount('#app')
th {
cursor: pointer;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<table>
<thead>
<tr>
<th v-for="head in headers" #click="sort(head)">
{{ head }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(data, i) in sortedProperties" :key="data.id">
<td v-for="(head, idx) in headers" :key="head.id">
{{ data[head] }}
</td>
</tr>
</tbody>
</table>
</div>

For any one else who is stuck this is how i solved the problem from https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_sort_table_desc:
//sort table columns
const sortTable = (n) =>{
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("myTable");
switching = true;
//Set the sorting direction to ascending:
dir = "asc";
/*Make a loop that will continue until
no switching has been done:*/
while (switching) {
//start by saying: no switching is done:
switching = false;
rows = table.rows;
/*Loop through all table rows (except the
first, which contains table headers):*/
for (i = 1; i < (rows.length - 1); i++) {
//start by saying there should be no switching:
shouldSwitch = false;
/*Get the two elements you want to compare,
one from current row and one from the next:*/
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/*check if the two rows should switch place,
based on the direction, asc or desc:*/
if (dir == "asc") {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
//if so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
/*If a switch has been marked, make the switch
and mark that a switch has been done:*/
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
//Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/*If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again.*/
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}

Related

How to generate table with rowspan and colspan automatically from json?

I retrive data about different plans from an api and I would like to show a nice formated table, where the same feature on a different plan are merged with a colspan, and a feature that requires 2 rows are merged with a rowspan. But I've been through 3-4 iterations and I always face a different problem. I need your help :)
I looked around and I could find a solution for the rowspan or the colspan but I was never successful in merging both solutions.
Similar question:
How to use dynamic table rowspan in vue.js?
The objective:
Current setup:
<table>
<thead>
<tr>
<th></th>
<th v-for="planInfo in data.plans" class="bg-primary">
<h4 class="mb-0 text-light">
{{ planInfo.name }}
</h4>
</th>
</tr>
</thead>
<tbody>
<template v-for="(_row, rowId) in data.rows" :key="_row.key">
<tr>
<th v-if="remconId === 0" :rowspan="_row.rowspan">{{ remcon }}</th>
<th v-else>{{ _row.name }}</th>
<template class="txt-color" v-for="(planFeat, planFeatId) in objectToArray(data.plans, data.rows[rowId].key)" :key="planFeatId">
<td class="txt-color" v-if="_row.key !== 'remote_conn'">
<span v-if="planFeat" v-html="planFeat"></span>
</td>
<template v-else="_row.key === 'remote_conn'">
<td>{{ planFeat }}</td>
</template>
</template>
</tr>
<!-- <tr v-for="(remcon, remconId) in plan.values.remote_conn">
</tr> -->
</template>
</tbody>
</table>
<script setup lang="ts">
const data = {
rows: [
{
name: "Feature A",
key: "feata"
},
{
name: "Feature B",
key: "featb"
},
{
name: "Feature C",
key: "featc",
rowSpan: 2,
},
{
name: "Feature D",
key: "featd"
},
],
plans: [
{
"name": "Plan 1",
"feata": "yes",
"featb": 5,
"featc": {
"value1": 10,
"value2": 5
},
"featd": "no",
},
{
"name": "Plan 2",
"feata": "no",
"featb": 10,
"featc": {
"value1": 0,
"value2": 1
},
"featd": "no",
},
]
}
const objectToArray = (objArr, key) => {
console.log("Looking for ", key, objArr)
return Array.from(objArr, plan => plan.values[key])
}
</script>
Another type of attempt
Some scripts I've been trying below. Unoptimized, probably stupid code, from a differently formatted json where the features are stored in an array, but it wasn't very intuitive:
// Check if the next TD have the same value, if so, increment colSpan by one
let sameCount = 0;
const colspanCount = (i, j, isRecursion = false) => {
console.log(`Col ${j}, Row ${i}`)
if(!isRecursion) {
sameCount = 1;
} else {
console.log(`Is recursion, ${j} ${i}`)
}
// Is j in-range?
if(j >= data.plans.items.length - 1) {
console.log(`${j+1} is out of range, max is ${attr.value.plans.items.length - 1}`)
// This is the last column, there is nothing after that. return 1
if(isRecursion) return false;
} else
// Next value is the same as this one, check the next one
if(attr.value.plans.items[j].features[i] === attr.value.plans.items[j+1].features[i]) {
sameCount++;
console.log(`${i} is the same. ${attr.value.plans.items[j].features[i]} = ${attr.value.plans.items[j+1].features[i]}`);
let nextIsSame = colspanCount(i, j+1, true);
if(nextIsSame) {
if(isRecursion) return true;
} else {
if(isRecursion) return false;
}
}
console.log(`== End, ${sameCount}`)
return sameCount;
}
// Check if we need to add an additional TD
// Don't if the previous TD have the same value
let isFirstVar = true;
const isFirst = (i, j) => {
console.log(`Col ${j}, Row ${i}`)
isFirstVar = true;
// Is j in-range?
if(j <= 0) {
console.log(`${j-1} is out of range`)
return isFirstVar;
// This is the last column, there is nothing after that. return 1
} else
// Next value is the same as this one, check the next one
if(attr.value.plans.items[j].features[i] === attr.value.plans.items[j-1].features[i]) {
isFirstVar = false;
console.log(`${i} is the same. ${attr.value.plans.items[j].features[i]} = ${attr.value.plans.items[j-1].features[i]}`);
}
// if(i <= items.length - 1 && j <= items[i].length - 1) {
// console.log(i, j)
// }
// if((len - 1 < j++) && items[i][j] == items[i][j++]) {
// return 2;
// }
console.log(`== End is it first? ${isFirstVar}`)
return isFirstVar;
}
The json data that goes with the above script:
const attr = ref({
plans: {
features: [
"feata",
"featb",
"featc",
{
name: "featC",
rowspan: 2
}
],
items: [
{
name: "",
features: [
"feata_value", "featb_value", "featc1_value", "featc2_value",
],
},
]
},
})
I would use those 2 functions in the TD like so, where i is the "plan" and j the the "feature" number in a loop:
<td class="txt-color"
:colspan="colspanCount(i, j)" v-if="isFirst(i, j)">
<span v-if="planFeat" v-html="planFeat"></span>
</td>
But I couldn't make this works with rowspan, as the next feature would be on the same row as another one...

Sorting table doesnt working Vue

I found this pen https://codepen.io/cfjedimaster/pen/BYpJgj But it doesn't work in my implementation. I can't understand what i am doing wrong
<table>
<thead>
<tr class="header">
<th #click="sort('campaign')">Campaign</th> // click heandler
<th #click="sort('time')">Time</th>
<th #click="sort('views')">Views</th>
<th #click="sort('visitors')">Visitors</th>
<th #click="sort('ctr')">CTR</th>
<th #click="sort('cpc')">CPC</th>
<th #click="sort('cpv')">CPV</th>
<th #click="sort('cpm')">CPM</th>
<th #click="sort('status')">Status</th>
</tr>
</thead>
<tbody> //target table
<tr v-for="item in data" :key="item.id"> // loop
<td>{{item.name}}</td> //row
<td>{{item.time}}</td>
<td>{{item.views}}</td>
<td>{{item.visitors}}</td>
<td>{{item.ctr}}</td>
<td>{{item.cpc}}</td>
<td>{{item.cpv}}</td>
<td>{{item.cpm}}</td>
<td>{{item.status}}</td>
</tr>
</tbody>
</table>
data(){
return{
data: [], // data
currentSort:'campaign', // dynamic property
currentSortDir:'asc'
}
},
methods:{ // methods
getData(){
this.data = this.$store.state.graphArr // get data from store
},
sort(s) { // sort function
//if s == current sort, reverse
if(s === this.currentSort) { // if statement
this.currentSortDir = this.currentSortDir==='asc'?'desc':'asc'; }
this.currentSort = s; // setting currentSortDir
}
},
computed:{ // computed
sortedCats() { // where this function should be called
return this.data.sort((a,b) => { // ES Lint highlits "this". Unexpetced side effect
let modifier = 1;
if(this.currentSortDir === 'desc') modifier = -1;
if(a[this.currentSort] < b[this.currentSort]) return -1 * modifier;
if(a[this.currentSort] > b[this.currentSort]) return 1 * modifier;
return 0;
});
}
}
Maybe i should somewhere call sortedCats function? And the role of computed property at all in this case?
You need to loop through sortedCats array to get the desired result instead of data as sortedCats returns new value every time sort is clicked through computed property.
<tr v-for="item in sortedCats" :key="item.id">

Date range inside loop of multiple datatable in the same page

I came from the issue [https://datatables.net/forums/discussion/51949/looping-multiple-datatables-on-the-same-page#latest] and found an issue that comes from filtering of dates: if I filter and on change of this date range, it triggers table.draw() for the first one if it is inside of the loop of multiple datatable in the same page
My intention is to have the data range to work on each datatable individually
Here is a sample of what I already done
http://live.datatables.net/magokusa/4/edit
HTML
<div class="container">
<h3>Table 1</h3>
<div class="input-group input-group-sm">
<input type="date" id="dateFromupper" placeholder="Date from" value="2017-04-10">
<div>
<div>to</div>
</div>
<input type="date" id="dateToupper" placeholder="Date to" value="2018-09-10">
</div>
<table id="upper" data-action="https://demo.wp-api.org/wp-json/wp/v2/posts?per_page=5" class="display nowrap datatable" width="100%">
<thead>
<tr>
<th>col1</th>
<th>col2</th>
<th>col3</th>
</tr>
</thead>
</table>
<hr>
<h3>Table 2</h3>
<div class="input-group input-group-sm">
<input type="date" id="dateFromlower" placeholder="Date from" value="2016-04-10">
<div>
<div>to</div>
</div>
<input type="date" id="dateTolower" placeholder="Date to" value="2018-09-12">
</div>
<table id="lower" data-action="https://css-tricks.com/wp-json/wp/v2/posts?per_page=5" class="display nowrap datatable" width="100%">
<thead>
<tr>
<th>col1</th>
<th>col2</th>
<th>col3</th>
</tr>
</thead>
</table>
</div>
JS
$(document).ready( function () {
$.each($('.datatable'), function () {
var dt_id = $(this).attr('id');
var dt_action = $(this).data('action');
$.fn.dataTable.ext.search.push(
function (settings, data, dataIndex) {
var min = $("#dateFrom" + dt_id).val();
var max = $("#dateTo" + dt_id).val();
if (min != null && max != null) {
min = new Date(min);
max = new Date(max);
var startDate = new Date(data[1]);
if (min == null && max == null) {
return true;
}
if (min == null && startDate <= max) {
return true;
}
if (max == null && startDate >= min) {
return true;
}
if (startDate <= max && startDate >= min) {
return true;
}
} else {
return true;
}
}
);
$("#dateFrom" + dt_id + ", #dateTo" + dt_id).change(function () {
table.draw();
});
if (dt_action != null) {
var dt_ajax = dt_action;
var table = $('#' + dt_id).DataTable({
ajax: {
"url": dt_ajax,
"dataSrc": ""
},
columns: [
{ "data": "status" },
{ "data": "date" },
{ "data": "date_gmt" },
]
});
} else {
var table = $('.datatable').DataTable({
retrieve: true,
responsive: true,
});
}
});
});
Since you already are declaring two different filters, you can just check if the current draw process is related to the filter itself :
$.fn.dataTable.ext.search.push(
function (settings, data, dataIndex) {
if (settings.sInstance != dt_id) return true
...
}
)

v-model in a nested v-for of a multidimensional array

Hi I want to create a table with the days of the selected month where you can a add an employee and mark meals you want to assign to the employee.
I almost there, I can add a row to the table and mark the meals by day but when a second row is added the same meals are marked, all the meals are binded by day if i mark a meal in a row it marks for all the rows.
Here is the code and a jsfiddle
Html
<div id="app">
<span class="demonstration">Pick a month</span>
<input type="month" v-model="month">{{month}}<br><br>
<button #click="addEmployee()">Add a employee</button><br>
Mark meals for the employee<br>
<table border="1">
<thead>
<tr>
<th rowspan="3">Name</th>
<th :colspan="calendar.length*3">days of the month</th>
</tr>
<tr>
<th colspan="3" v-for="day in calendar">{{day.date}}</th>
</tr>
<tr>
<template v-for="c in calendar">
<th>b</th>
<th>l</th>
<th>d</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="(item, indexItem) in list" :key="indexItem">
<td>
<input type="text" v-model="item.name">
</td>
<template v-for="(day, indexDay) in item.days">
<td>
<input type="checkbox" v-model="item.days[indexDay].breakfast">
</td>
<td>
<input type="checkbox" v-model="item.days[indexDay].lunch">
</td>
<td>
<input type="checkbox" v-model="item.days[indexDay].dinner">
</td>
</template>
</tr>
</tbody>
</table>
</div>
Vue
new Vue({
el: "#app",
data: {
month: '',
list: [
]
},
computed: {
calendar () {
let selected = new Date(this.month)
let daysOfMonth = new Date(selected.getFullYear(), selected.getMonth() + 1, 0)
let days = [{}]
for (var i = 0; i < daysOfMonth.getDate(); i++) {
days[i] = {
date: selected.getFullYear().toString() + '-' + (selected.getMonth() + 1).toString() + '-' + (i + 1).toString(),
breakfast: false,
lunch: false,
dinner: false
}
}
return days
}
},
methods: {
addEmployee () {
let cal = []
cal = this.calendar
this.list.push(
{
name: '',
days: cal
}
)
}
}
})
https://jsfiddle.net/patogalarzar/v8h0knt7/
You are sharing the same object on every row, which means when one row is update, all the rest get updated as well.
Computed method is not the right tool here. I suggest you create a method to generate the calendar object.
methods: {
createCalander (month) {
let selected = new Date(month)
let daysOfMonth = new Date(selected.getFullYear(), selected.getMonth() + 1, 0)
let days = [{}]
for (var i = 0; i < daysOfMonth.getDate(); i++) {
days[i] = {
date: selected.getFullYear().toString() + '-' + (selected.getMonth() + 1).toString() + '-' + (i + 1).toString(),
breakfast: false,
lunch: false,
dinner: false
}
}
return days
}
}
}
Now you can create the computed property using this method, passing this.month.
On the add employees you would be using the new method to generate the list.
addEmployee () {
let cal = []
cal = this.getCalander(this.month)
this.list.push(
{
name: '',
days: cal
}
)
}
Now that you are not using the same object, the rows will not update together.
Your mistake was to use the same object on every row.
I've updated the jsfiddle
Change your addEmployee method to avoid point to same object:
addEmployee () {
let cal = []
cal = JSON.parse(JSON.stringify(this.calendar))
this.list.push(
{
name: '',
days: cal
}
)
}
More proper way to create a method call getCalendar and let cal = this.getCalendar()
this is because all employees reference the same object calendar, you can deep copy the object, or try this way

multi dimensional array filter in vue js

i am trying to filter from multi dimensional array in vue js.
first i am storing response from axios in a variable like
fetchUsersDetails() {
var vm = this;
axios.get('school/api/user',{headers: getHeader()}).then(response => {
Vue.set(vm.$data, 'userList', response.data.data)
//console.log(this.userList)
})
},
on doing console.log(this.userList) iam getting
0:{
name:rajesh
city:dhanbad
state:jharkhand
student_session{
0:{
class_id:1
session_id:1
}
}
}
1:{
name:rohan
city:dhanbad
state:jharkhand
student_session{
0:{
class_id:1
session_id:1
}
}
}
2:{
name:rahul
city:dhanbad
state:jharkhand
student_session{
0:{
class_id:2
session_id:1
}
}
}
3:{
name:ramesh
city:dhanbad
state:jharkhand
student_session{
0:{
class_id:3
session_id:1
}
}
}
and so on...
now in html
<table class="table">
<tr>
<th style="display: none">Id</th>
<th>Sl. No.</th>
<th>Name</th>
</tr>
</thead>
<tfoot>
<tr>
<th style="display: none">Id</th>
<th>Sl. No.</th>
<th>Name</th>
</tr>
</tfoot>
<tbody>
<tr v-for="(studentDetails, index) in filterUserLists">
<td style="display: none">{{studentDetails.user_token}}</td>
<td>{{index+1}}</td>
<td>
<a #click="showModal(studentDetails)" data-toggle="modal" data-target="#showModal" >{{studentDetails.first_name}}</a>
</td>
</tr>
</tbody>
and i am filtering my userList
filterUserLists: function () {
if(this.userList)
{
var list= this.userList
.filter(item => item.student_session.class_id==="1" )
}
console.log(list)
},
but i am getting empty list on my console though in my userList student_session is present with all values
i am new to vue js, so please help me
thankx in advance...
you can use computed
computed: {
filterUserLists () {
var filtered = [];
for (var i = 0; i < this.userList.length; i++) {
if (this.userList[i].student_session.class_id == "1") {
filtered.push(this.userList[i]);
}
}
return filtered;
}
}
This seems to be rather a problem with your filter because you try to access the secound array directly.
For me it worked with
userList.filter(item => item.student_session[0].class_id===1 )
and
userList
.filter(item => item.student_session
.filter((item2 =>item2.class_id===1 )) )
Or just use two loops like everyone does for a two dimensional array.
for(var i =0; i < userList.length; i++){
...
for(var j=0; j < userList[i].student_session.length; j++){
if(userList[i].student_session[j].class_id===1){
...
}
}
}
If you declared filterUserList unter methods you have to use it as function in the v-for
<tr v-for="(studentDetails, index) in filterUserLists()">
You try to access the properties .user_token and .first_name but these are never declared.