vue js error:" For recursive components, make sure to provide the "name" option." when recursively create table - vue.js

I create a dynamic HTML table by vue js which get data from server. The data includes 'columns', which is a Array with server objects including the table header(key is title), 'q', is the value type of each column generated from database table column name, 'content", the value display method. e.g content='content":'', the cell will display like ''.
HTML Part
<div id="app3">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" v-for="c in columns" v-text="c.title"></th>
</tr>
</thead>
<tbody>
<vue-cell:rows="rows" :columns="columns">
</vue-row>
</tbody>
</table>
</div>
Vue js part
<script>
var columns =[
{'q': "id",
'title': 'id',
'content":'<input type="checkbox">'
},
{'q': "type",
'title': 'Type',
'content":''
},
]
var rows=[
{'id': 1,
'type':"Server",
},
{'id': 2,
'type':"PC",
},
]
create component to loop rows and column
Vue.component('vue-cell', {
name:'inputBox',
props: {
rows: Array,
columns: Array,
},
render: function (createElement) {
var self = this
return createElement('tr', self.rows.map(function(row) {
return createElement('td', self.columns.map(function(column) {
// comlum.q = id or type ...
// column['content'] = <input type="checkbox">
//assign row.id as defalut value
if (column['content'].includes('checkbox')) {
return createElement('input', {
attrs: {
type: "checkbox",
},
domProps: {
// column.q may not be 'id' maybe type
// get the value of type or id in a row.
value: row[column.q],
}
})
} else {
return row[column.q];}
}))
}))
},
})
Create a new Vue
new Vue ({
data: function() {
return {
columns: [],
rows:[],
}
},
created: function() {
var self = this;
axios.("/web/asset-json.html").then(function (response) {
self.columns = [...response.data.columns];
self.rows = [...response.data.rows]
})
}
})
</script>

Related

Standard "check-all" functionality in table

Here's a part of my grid (CRUD) component:
<template>
<table class="MyComponent table">
<thead>
<tr>
<th width="30px">
<b-form-checkbox v-model="allChecked" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in records" :key="index">
<td width="30px">
<b-form-checkbox :value="record['id']" v-model="checkedRows" />
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: "MyComponent",
components: {
},
props: ['config'],
data() {
return {
records: [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}, {
id: 6
}],
checkedRows: []
}
},
computed: {
allChecked: {
get() {
return this.records.length == this.checkedRows.length
},
set(v) {
if(v) {
this.checkedRows = [];
for(var i in this.records) {
this.checkedRows.push(this.records[i]['id'])
}
}
else {
this.checkedRows = [];
}
}
}
}
};
</script>
As you can see, I would like to achive a standard, widely used functionality: The user can check multiple rows and do some operation with the selected rows. The problem is with the "check all" checkbox on the top of the table. When I check all, then I remove the tick from only one checkbox below, it unchecks all the checkboxes on page.
I understand why its happening: When I remove a tick from on of the checkboxes below, the "this.records.length == this.checkedRows.length" condition will no longer be true, so the "allChecked" computed variable will be set to false, therefore the top checkbox will set to unchecked. The problem is: when the top checkbox will be unchecked, then all of the checkboxes will be unchecked as well, because of the "set" part of the computed variable.
Is there a clean way to solve this problem in Vue?
I'm not sure what you want to do with the checked rows, but maybe this will be better:
<b-form-checkbox :value="record['id']" v-model="record.checked" />
Then add to your objects in records a checked property.
records: [
{
id: 1,
checked: false
},
...
]
and if you need a list of checked records you might do a computed property:
computed: {
checkedRecords() {
return this.records.filter(record => record.checked);
}
}
and for checking-unchecking all you just iterate over all records:
<b-form-checkbox #change="clickedAll" />
methods: {
clickedAll(value) {
this.records = this.records.map(record => {
record.checked = value
return record
}
}
}
OK, meanwhile I solved the problem. Here's my solution. Thanks #Eggon for your help, you gave the idea to use the #change method.
<template>
<table class="MyComponent table">
<thead>
<tr>
<th width="30px">
<b-form-checkbox v-model="allChecked" #change="checkAll" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="(record, index) in records" :key="index">
<td width="30px">
<b-form-checkbox :value="record['id']" v-model="checkedRows" />
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: "MyComponent",
components: {
},
props: ['config'],
data() {
return {
records: [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}, {
id: 6
}],
checkedRows: []
}
},
methods: {
checkAll(value) {
if(!value) {
this.checkedRows = [];
return ;
}
var newCheckedRows = [];
for(var i in this.records) {
newCheckedRows.push(this.records[i].id)
}
this.checkedRows = newCheckedRows;
}
},
computed: {
allChecked: {
get() {
return this.records.length == this.checkedRows.length
},
set() {
}
}
}
};
</script>

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>

Mutating a value in vue when the key didn't previously exist does not update the view

I have a table and a select box for each row. I want the check box to model a value in the data that doesn't actually exist, yet.
<tr v-for="item in someData">
<input type="checkbox" v-model="item.selected"></td>
<input type="checkbox" v-model="item.name"></td>
<tr>
My data when loaded from the DB looks like this:
someData: [
{'name': 'john'},
{'name': 'kate'},
{'name': 'aaron'},
]
When the user presses a Select All button it should update the selected key even if it doesn't exist (well thats the idea)
toggleSelect: function () {
this.someData.forEach(element => {
element.selected = !element.selected;
});
}
However the checkboxes don't react even though the values have been updated. To make this work I need to get the data and add the key/value manually prior to loading it into view and rendering
getDatabaseData: function () {
// some code omitted
response['data'].forEach(element => {
element["selected"] = false;
});
app.someData = response['data']
}
Am I doing it correctly? Am I right in thinking Vue won't be reactive to values that didn't exist prior to rendering?
Try this idea,
in vue component.
<input type="checkbox" v-model="selectAll"> Select All
<tr v-for="item in someData" :key="item.name">
<td>
<input type="checkbox" v-model="selected" :value="item.name">
</td>
{{ item.name }}
</tr>
script:
data() {
return {
selectAll: false,
selected: [],
someData: [{ name: "john" }, { name: "kate" }, { name: "aaron" }]
};
},
watch: {
selectAll(value) {
// validate if value is true
if (value) {
this.someData.forEach(item => {
// push unique value
if(this.items.indexOf(item.name) === -1) {
this.selected.push(item.name);
}
});
} else {
// Unselect all
this.selected = [];
}
}
}
You have a selected variable where the selected Items are located. selectAll variable to select all items and push to selected variable.
You should be using Vue.set to update the value of the selected property on your objects in order to be reactive, like this:
import Vue from 'vue';
...
toggleSelect: function () {
this.someData.forEach(element => {
Vue.set(element, 'selected', !element.selected);
});
}

Bootstrap-vue b-table with filter in header

I have a table generated with bootstrap-vue that shows the results of a system search.
The Results Table shows the records to the user, and the user can sort them and filter them.
How can I add the search field underneath the table header <th> generated with the bootstrap-vue <b-table> element?
Screenshot of the current table:
Mockup of the wanted table:
You can use the top-row slot to customise your own first-row. See below for a bare-bones example.
new Vue({
el: '#app',
data: {
filters: {
id: '',
issuedBy: '',
issuedTo: ''
},
items: [{id:1234,issuedBy:'Operator',issuedTo:'abcd-efgh'},{id:5678,issuedBy:'User',issuedTo:'ijkl-mnop'}]
},
computed: {
filtered () {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
String(item[key]).includes(this.filters[key]))
})
return filtered.length > 0 ? filtered : [{
id: '',
issuedBy: '',
issuedTo: ''
}]
}
}
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css"/><link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.css"/><script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.min.js"></script><script src="//unpkg.com/babel-polyfill#latest/dist/polyfill.min.js"></script><script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.js"></script>
<div id="app">
<b-table striped show-empty :items="filtered">
<template slot="top-row" slot-scope="{ fields }">
<td v-for="field in fields" :key="field.key">
<input v-model="filters[field.key]" :placeholder="field.label">
</td>
</template>
</b-table>
</div>
Note: I've used a computed property to filter the items instead of the :filter prop in b-table because it doesn't render rows if all the items are filtered out, including your custom first-row. This way I can provide a dummy data row if the result is empty.
Have upvoted phil's answer, just making it more generic
filtered() {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
String(item[key]).includes(this.filters[key])
);
});
return filtered.length > 0
? filtered
: [
Object.keys(this.items[0]).reduce(function(obj, value) {
obj[value] = '';
return obj;
}, {})
];
}
Thanks to you for these useful answers. It saved some of my time today.
However, in case items are given asynchronously i had to add a test on items size like this
filtered() {
if (this.items.length > 0) {
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key => String(item[key]).includes(this.filters[key])
);
});
return filtered.length > 0
? filtered
: [
Object.keys(this.items[0]).reduce(function (obj, value) {
obj[value] = '';
return obj;
}, {})
];
}
},
On another hand if needed to have column with no filter, i added this test below
In the template
<td v-for="field in fields" :key="field.key">
<input v-if="fieldIsFiltered(field)" v-model="filters[field.key]" :placeholder="field.label">
</td>
and within component methods
fieldIsFiltered(field) {
return Object.keys(this.filters).includes(field.key)
}
mistake
const filtered = this.items.filter(item => {
return Object.keys(this.filters).every(key =>
// String(item[key]).includes(this.filters[key]))
return String(item[key]).includes(this.filters[key]))
})

datatable only working for the first time

function SearchUser(url, pageIndex) {
var table;
table = $('#UsersTable').DataTable({
serverSide: true,
retrieve:true,
ajax: {
url: url,
type: "POST",
data: { username: $('#Username').val(), email: $('#Email').val(), companyID: $('#LifeCompanies').val(), page: pageIndex, isLocked: $('#Locked').is(':checked') }
},
columns: [
{ "data": "UserId" },
{ "data": "Username" },
{ "data": "Email" },
{ "data": "IsLockedOut" },
{
"render": function (data, type, full, meta) {
return '<span>wahoo</span>';
}
}
]
});
}
<div id="search">
<h3>Search:</h3>
#using (Html.BeginForm("Search","Admin"))
{
<div>
<table width="500px">
<thead>
<tr>
<td>Username:</td>
<td>#Html.TextBox("Username")</td>
</tr>
<tr>
<td>Email:</td>
<td>#Html.TextBox("Email")</td>
</tr>
<tr>
<td>Locked:</td>
<td>#Html.CheckBox("Locked")</td>
</tr>
<tr>
<td colspan="2">
<div id="searchForm">
#Html.Partial("SearchBuild")
</div>
</td>
</tr>
<tr>
<td colspan="2"><input type="button" value="Search" onclick="SearchUser('/UserManagement/admin/Search',0)"/></td>
</tr>
</thead>
</table>
</div>
}
</div>
Hi the code above uses the datatables jquery plugin.
It seems to only work for the first time I use that function. The second time, it appears to hit the javascript but doesn't ever retrive any data from my mvc controller.
However if I use destroy instead of retrieve it works perfectly fine.
If I don't use retrieve or destroy, I get the "can't reinitialise table" error.
That function is just called by a button I click.
You need to modify your code to initialize your table and reload it separately.
Also it's not possible to modify initialization options via API unless you destroy and recreate the table.
You need another way to pass pageIndex variable. For example, you can create a hidden input with id PageIndex and set and retrieve value from there.
For example:
function initTable(url) {
var table;
table = $('#UsersTable').DataTable({
serverSide: true,
retrieve:true,
ajax: {
url: url,
type: "POST",
data: {
username: $('#Username').val(),
email: $('#Email').val(),
companyID: $('#LifeCompanies').val(),
page: $('#PageIndex').val(),
isLocked: $('#Locked').is(':checked')
}
},
columns: [
{ "data": "UserId" },
{ "data": "Username" },
{ "data": "Email" },
{ "data": "IsLockedOut" },
{
"render": function (data, type, full, meta) {
return '<span>wahoo</span>';
}
}
]
});
}
To search again you just need to call $('#UsersTable').DataTable().ajax.reload() API method.
If URL will change between searches use $('#UsersTable').DataTable().ajax.url(newurl).load() API method instead.