Vue - how to bind table column to a data object? - html-table

I'm new to Vue, so go easy on me! Here's the situation. There must be a better way than what I'm doing here.
I have a simple 2 column HTML table:
<table id="contacts">
<tbody>
<tr>
<th class="column-1">
Contact Id
</th>
<th class="column-2">
Applications assigned count
</th>
</tr>
<tr class="odd" id="contacts_tr_1">
<td class="column-1">
1
</td>
<td class="column-2">
247
</tr>
<tr class="even last" id="contacts_tr_2">
<td class="column-1">
2
</td>
<td class="column-2">
0
</td>
</tr>
<tr class="even last" id="contacts_tr_2">
<td class="column-1">
3
</td>
<td class="column-2">
44
</td>
</tr>
<tr class="even last" id="contacts_tr_2">
<td class="column-1">
.........
</td>
<td class="column-2">
.........
</td>
</tr>
<tr class="even last" id="contacts_tr_2">
<td class="column-1">
10
</td>
<td class="column-2">
76
</td>
</tr>
</tbody>
</table>
I want to update the "Applications assigned count" column (but only for certain rows), as determined by the result of an AJAX call. So assuming the table has 10 rows, the AJAX call might say that the value of the "Applications assigned count" column of rows 1, 4 and 7 need to be updated, to e.g. 247, 80 and 356 respectively. I'm thinking of using a JSON object like this as my Vue data object, because the AJAX response will look like this.
data: {
num_of_applications_assigned: [
{
"party_id": "1",
"num": "247"},
{
"party_id": "4",
"num": "80"},
{
"party_id": "7",
"num": "356"}
]
},
I thought there might be a way to bind the "Applications assigned count" column to the Vue data object that gets updated by the AJAX call, but I don't see a way to do this other than adding a unique v-text to each individual <TD> cell e.g.
<div v-text="num_of_applications_assigned_1"></div>
<div v-text="num_of_applications_assigned_2"></div>
etc
However, this has lead me to writing some very convoluted code when updating those v-texts with the results of the AJAX response, as I have to dynamically construct the references:
let vm = this;
jQuery.ajax({
url: myurl
}).then(function(response) {
for (var i = 0, len = vm.num_of_applications_assigned.length; i < len; i++) {
var party_id = vm.num_of_applications_assigned[i].party_id;
var dref = 'vm.num_of_applications_assigned_'+party_id;
var dnum = vm.num_of_applications_assigned[i].num;
eval(dref + ' = ' + dnum + ';');
}
});
Yes, I know eval is bad - that's why I'm here asking for help! What is a better way of doing this, or is Vue not a good match for this situation?

I can't use v-for, as the table and its rows are all generated server side
If you cannot use v-for you can still use Vue to render your data, if you decide to do some additional work on the server-side, and you mould your data a little differently. It's less elegant than v-for but it should be straightforward.
For example, if you wanted to create a two-column table where Vue would render/update the second column's cell values, you could generate something like this on the server side:
<table id="app">
<tr>
<td>1</td>
<td>{{ applications.party_1.num }}</td>
</tr>
<tr>
<td>2</td>
<td>{{ applications.party_2.num }}</td>
</tr>
</table>
Where you use your favourite server-side language to generate values party_1, party_2, party_3 dynamically.
This implies that an underlying data structure like so:
applications: {
party_1: { num: 1 },
party_2: { num: 2 }
}
This should be straightforward to create that structure dynamically on the server-side. Then, just create a Vue instance and populate its initial data object with that data structure:
new Vue({
el: '#app',
data: {
applications: {
party_1: { num: 1 },
party_2: { num: 2 }
}
}
});
When the HTML is rendered in a browser, the Vue instance mounts and it will update the bound cell values from its data object. These values are reactive, so any subsequent changes to the Vue instance's underlying data will be rendered automatically.
Hope this helps :)

The idea in Vue is to have all your data ready, and let Vue.JS do the rendering. So, your data should probably look like this:
data: {
assignedApplications: [
{ party_id: 1, num: 247 },
{ party_id: 2, num: 0 },
{ party_id: 3, num: 44 },
{ party_id: 4, num: 76 },
{ party_id: 5, num: 9 },
]
},
}
Then, you can let Vue render it:
<table>
<tr>
<th class="column-1">Contact Id</th>
<th class="column-2">Applications assigned count</th>
</tr>
<tr v-for="a in assignedApplications">
<td>{{a.party_id}}</td>
<td>{{a.num}}</td>
</tr>
</table>
Remains the problem, how to update it. After getting the new data, you have to modify the array this.assignedApplications, and then Vue will re-render the table correctly. If your rows have a unique id, you could make assignedApplications instead of an array a map-like data structure, so you can easy access specific rows by their id and change the value. If not, you have to search through the whole array for every change and adapt it:
mergeInNewData(newData) {
for (let i = 0; i < newData.length; i++) {
let changedData = newData[i];
for (let j = 0; j < this.assignedApplications.length; j++) {
if (this.assignedApplications[j].party_id === changedData.party_id) {
this.assignedApplications[j].num = changedData.num;
}
}
}
}
All together, an example could look like this:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<table>
<tr>
<th class="column-1">Contact Id</th>
<th class="column-2">Applications assigned count</th>
</tr>
<tr v-for="a in assignedApplications">
<td>{{a.party_id}}</td>
<td>{{a.num}}</td>
</tr>
</table>
Update
</div>
<script>
var app = new Vue({
el: '#app',
data: {
assignedApplications: [
{ party_id: 1, num: 247 },
{ party_id: 2, num: 0 },
{ party_id: 3, num: 44 },
{ party_id: 4, num: 76 },
{ party_id: 5, num: 9 },
]
},
methods: {
update: function() {
let newData = [
{ party_id: 1, num: 243},
{ party_id: 2, num: 80},
{ party_id: 4, num: 0},
];
this.mergeInNewData(newData);
},
mergeInNewData(newData) {
for (let i = 0; i < newData.length; i++) {
let changedData = newData[i];
for (let j = 0; j < this.assignedApplications.length; j++) {
if (this.assignedApplications[j].party_id === changedData.party_id) {
this.assignedApplications[j].num = changedData.num;
}
}
}
}
}
})
</script>
</body>
</html>

Related

Update Export-Data on DataTable [duplicate]

I've been struggling with this code. I need to avoid specific columns from being exported. I have done that but I don't know how to export the text inside any input element.
Here is the code:
$("#campaignMaterialsTable table").DataTable({
dom: 'Bfrtip',
buttons: [
{
extend: 'excel',
className: 'export-button',
text: 'Export as Excel',
columns: ':not(.notexport)',
//exportOptions: {
// format: {
// body: function (data, row, column, node) {
// //
// //check if type is input using jquery
// return $(data).is("input") ?
// $(data).val() :
// data;
// },
// columns: ':not(.notexport)'
// }
//},
title: 'Campaign Materials'
}]
});
I don't know here to put the code at the right place.. while initializing..
I referred to this links, but still not getting what I want:
https://datatables.net/forums/discussion/50724/export-values-typed-in-input-box-excelhtml5
https://datatables.net/forums/discussion/42205/export-data-with-text-box-in-td-of-data-table#latest
https://datatables.net/extensions/buttons/examples/html5/outputFormat-function.html
https://datatables.net/forums/discussion/50724/export-values-typed-in-input-box-excelhtml5
I assume you have a DataTable containing one or more input fields, something like this:
The user can type values into the input fields.
You want to export data to Excel, so that the result looks like this:
Here we can see the user-provided data as well as the standard table data.
To achieve this, I used the following table data:
<table id="example" class="display nowrap dataTable cell-border" style="width:100%">
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Adélaïde Nixon</td>
<td>System Architect</td>
<td><input type="text" id="office" name="office"></td>
<td>432434</td>
<td>2011/04/25</td>
<td>$320,800</td>
</tr>
<tr>
<td>John Smith</td>
<td>Maager</td>
<td><input type="text" id="office" name="office"></td>
<td>6123</td>
<td>2011/04/25</td>
<td>$320,800</td>
</tr>
<tr>
<td>John Smith 2</td>
<td>Director</td>
<td><input type="text" id="office" name="office"></td>
<td>6123</td>
<td>2011/04/25</td>
<td>$320,800</td>
</tr>
</tbody>
</table>
And I used the following DataTable initialization code:
<script type="text/javascript">
$(document).ready(function() {
$('#example').DataTable({
dom: 'Bfrtip',
buttons: [
{
extend: 'excel',
exportOptions: {
format: {
body: function ( inner, rowidx, colidx, node ) {
if ($(node).children("input").length > 0) {
return $(node).children("input").first().val();
} else {
return inner;
}
}
}
}
}
]
});
});
</script>
The body function assumes that, for input fields, there is only an input field in the cell (i.e. the input field is not contained in a form or any other element).
It uses the node parameter to check for input data, to avoid data formatting issues that could occur (e.g. with dates). I recommend doing that rather than using $(data).is("input").
If there is no input field found in the cell, then the cell contents (inner) are returned.
If the structure of your table does not match these assumptions, then you may need to adjust the above code, of course.
Edit
Regarding the follow-up question about choosing which columns to export, there are various ways.
You can hard-code the column indexes you want to export (where index 0 means the first column). For example:
exportOptions: {
columns: [ 0, 1, 2, 3, 5 ],
format: {
body: function ( inner, rowidx, colidx, node ) {
if ($(node).children("input").length > 0) {
return $(node).children("input").first().val();
} else {
return inner;
}
}
}
}
This exports all columns apart from the Start Date column (index 4).
Or you can use the approach in your question, based on a class, I believe. I have not actually tried that one.
Take note of where the columns: [ 0, 1, 2, 3, 5 ] directive is placed - it is inside the exportOptions section.
you can use
exportOptions:{
columns: ':visible'
}
in below buttons
buttons: [
{
extend: 'pdf',
footer: true,
exportOptions: {
columns: ':visible'
}
},
it will export only visible columns

Sorting a vue 3 v-for table composition api

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

Creating a button dynamically in vuejs table

I am trying to create a table in vuejs for a personal project (I dont want to use a existing table) and I am facing probably a newbie problem.
I am trying to insert on my last column some buttons, but I dont know why, the grid is rendering my element tag instead of the element himself.
May someone explain to me why this does not work ? And, how can I create this feature ?
Fiddle: https://jsfiddle.net/y7830cvd/1/
<div id="app">
<div>
<vue-grid :rows="gridData" :title="nTitle"></vue-grid>
</div>
</div>
Vue.component('vue-grid', {
props: ['rows', 'title'],
template: `<div>
<h2>{{title}}</h2>
<div class="table-wrapper">
<table class="fl-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.id" v-on:click="sortTable(col)">{{col}}</th>
</tr>
</thead>
<tbody v-if="rows.length > 0">
<tr v-for="row in rows" :key="row.id">
<td v-for="col in columns" :key="col.id">{{row[col]}}</td>
</tr>
</tbody>
</table>
</div>
</div>`,
computed: {
columns: function columns() {
if (this.rows.length == 0) {
return []
}
return Object.keys(this.rows[0])
}
},
sortTable(col) {
this.rows.sort(function(a, b) {
if (a[col] > b[col]) {
return 1
} else if (a[col] < b[col]) {
return -1
}
return 0
})
},
methods: {
formatter(row, column) {
return row.address
},
filterTag(value, row) {
return row.tag === value
},
filterHandler(value, row, column) {
const property = column['property']
return row[property] === value
}
}
});
var app = new Vue({
el: '#app',
data(){
return {
gridData: [
{"id" : 1, "name": "firstValue", "something": "wha the fox say?","options" : "<button>Add</button>" },
{"id" : 1, "name": "firstValue", "something": "uauu uauu uauu?"},
{"id" : 1, "name": "firstValue", "something": "The cow goes mu?"}
],
nTitle: "Hello There!"
}},
})
Try v-html:
<td v-for="col in columns" :key="col.id">
<span v-if="col == 'options'" v-html="row[col]"></span>
<span v-else>{{row[col]}}</span>
</td>
Something you should consider (source documentation - link above):
Updates the element’s innerHTML. Note that the contents are inserted
as plain HTML - they will not be compiled as Vue templates. If you
find yourself trying to compose templates using v-html, try to rethink
the solution by using components instead.

DataTables row.add() based on columns

I have a table:
<table class="table table-bordered table-hover" id="user-table">
<thead>
<tr>
<th data-field="id">Id</th>
<th data-field="name">Name</th>
<th data-field="email">Email</th>
<th data-field="company">Company</th>
<th data-field="actions">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>John Doe</td>
<td>john#doe.com</td>
<td>Doe Inc.</td>
<td>
<div class="btn-group">
<i class="fa fa-pencil"></i>
</div>
</td>
</tr>
</tbody>
</table>
My current way to add row:
var columns = $.map(data,function(v,k){
return v;
});
var rowNode = table.row.add(columns).order([0, 'desc']).draw(false).node();
$(rowNode).addClass('selected');
setTimeout(function(){$(rowNode).removeClass('selected');}, 2000);
My data looks like this:
{
"data":[ {
"id": 2,
"name": "Jane Doe",
"email": "jane#doe.com",
"company": "Doe Inc."
}
]
}
Is there a way to add row based on columns? Instead of returning v on a mapped data, I can directly insert data[0] using row.add(). I've been searching but can't get the exact scenario.
Apparently, the purpose is for shuffled data. In a table that have 15 columns, I don't have to worry about the proper column arrangements.
I found this https://datatables.net/reference/api/row.add() example #1 but obviously the documentation says:
Data to use for the new row. This may be an array, object, Javascript object instance or a tr element. If a data structure is used (i.e. array or object) it must be in the same format as the other data in the table (i.e. if your table uses objects, pass in an object with the same properties here!).
If you specify columns (or columnDefs) with data attributes, you can add row or rows in JSON / object literal format:
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'email' },
{ data: 'company' }
]
You must also specify the last column, even it is not targeting any data. You can use render(), createdCell() or defaultContent:
const actions = `
<div class="btn-group">
<i class="fa fa-pencil"></i>
</div>
`;
var table = $('#user-table').DataTable({
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'email' },
{ data: 'company' },
{ data: null, defaultContent: actions }
]
...
})
Now you can insert data from above:
var rowNode = table.row.add(data.data[0]).order([0, 'desc']).draw(false).node();
or all rows :
table.rows.add(data.data).draw()

How to reload dataTables from VueJS after adding or editing?

I'd like to know how I can use this same approach when loading data from database through an api. In the first time the dataTables loads fine. However, when I add a new record, then I need to load my dataTables again with the new record. Here's my html code:
<table class="table table-striped table-bordered table-hover" id="dataTables-eventos">
<thead>
<tr class="tbheader">
<th class="text-center">#</th>
<th>Nome do Evento</th>
<th class="text-center">Editar</th>
<th class="text-center">Deletar</th>
</tr>
</thead>
<tbody>
<tr v-for="ev in eventos" :key="ev.id" track-by="id">
<td class="text-center">{{ ev.id }}</td>
<td>{{ ev.name }}</td>
<td class="text-center"><a class="cursorpointer" v-on:click="showMessage()"><span class="glyphicon glyphicon-edit"></span></a></td>
<td class="text-center"><span class="glyphicon glyphicon-trash"></span></td>
</tr>
</tbody>
</table>
And here's my VueJS code:
mounted() {
this.getEventos();
},
methods: {
getEventos() {
axios.get('/api/eventos')
.then((response) => {
this.eventos = response.data})
.then((response) => {
$('#dataTables-eventos').DataTable({
responsive: true,
"aaSorting": [[ 1, "asc" ]],
"aoColumnDefs": [
{ "bSortable" : false, "aTargets": [0,2,3] },
{ "searchable": false, "aTargets": [2,3] }
],
language: {
url: '/js/dataTables/localization/pt_BR.json'
}
});
});
},
addNewRecord() {
axios.post('/api/eventos', { nomeEvento: this.nomeEvento });
}
So after adding (or editing) a new record on my DB how can I reload my dataTables so I can see the changes?
I just ran into a situation where I needed to use vue.js with datatables.net and had to use customized table html. I created a component that allowed me to format the table as I needed using template/v-for and refresh the datatable while preserving datatables.net functionality. I hope this helps someone down the road...
export default {
data() {
return {
dataTable: null
}
},
props: {
data: Array
},
watch: {
data() {
if (this.dataTable) {
this.dataTable.destroy();
}
this.$nextTick(() => {
this.dataTable = $("#table").DataTable({
language: {
emptyTable: "No Results Found"
}
});
});
}
}
}
You can do it without reloading all the data.
Just add this.nomeEvento to array eventos after posting to server.