Standard "check-all" functionality in table - vue.js

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>

Related

Change v-model value without changin the actual data

So i've this data
data: () => ({
products: [
{ id: 1, name: "Prod 1", price: 2, stock: 5 },
{ id: 2, name: "Prod 2", price: 3, stock: 6 }
]
})
Template
<table>
<thead>
<tr>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>
<input
type="text"
class="form-control"
v-model="product.price"
#paste.prevent
/>
</td>
<td>
<input
type="text"
class="form-control"
maxlength="999"
v-model="product.stock"
#paste.prevent
#keypress="onlyNumber($event)"
#input="handleInputStock($event.target.value)"
#blur="updateStock($event.target.value, product.id)"
/>
</td>
</tr>
</tbody>
</table>
So what I want is that when the user hit delete/backspace from the stock input field the value cannot be empty (blank) or it must be greater than or equal to zero. but without changing the products.stock value. this is because I need the product.stock value to compare with the changed value (stock input field) before sending to the server. So if stock value is equal to product.stock don't send to server otherwise send and update stock value.
so here's what i've done so far.
prevent the stock value empty but not working
handleInputStock(value) {
return +value.replace(/[^0-9]/g, "");
},
update stock
updateStock(stock, productId) {
const productStock = this.products.find(product => product.id == productId).stock;
if (!(stock == productStock)) {
// do ajax
}
},
onlyNumber
onlyNumber(e) {
const charCode = e.which ? e.which : event.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
e.preventDefault();
}
},
Personally this feels like a higher level question to which your flow of product editing needs tweaking. Here is what I can think of:
User enters all the information.
User hits submit button.
Check whether of not the stock count is empty or 0.
Return an error message if it is.
Submit and update otherwise.
It might be worth looking into vuelidate that handles such validation in JavaScript. Meanwhile, we are also coming up with a tool called CRUDS DS (a WIP) that handles such situation with ease.
The best way is to create a ProductComponent and watch every product separately inside its own component, as shown below:
Product.vue
<ProductComponent
v-for="product in products"
:product="product"
:key="product.id" />
ProductComponent.vue
<template>
<tr>
<td>{{ product.name }}</td>
<td>
<input
type="text"
class="form-control"
v-model="product.price"
#paste.prevent
/>
</td>
<td>
<input
type="text"
class="form-control"
maxlength="999"
v-model="product.stock"
#paste.prevent
#keypress="onlyNumber($event)"
#blur="updateStock($event.target.value, product.id)"
/>
</td>
</tr>
</template>
<script>
export default {
props: {
product: {
type: Object,
default: {},
},
},
data: () => ({ actual_stock: "" })
// this is for handle stock cannot be empty or GTE:0
// also you dont need handleInputStock anymore
watch: {
product: {
handler(val) {
this.actual_stock = val.stock;
},
immediate: true,
},
"product.stock": function (newVal, oldVal) {
this.product.stock = +newVal;
},
},
methods: {
updateStock(stock, productId) {
if (!(stock == this.actual_stock)) {
// do ajax
}
}
}
}
</script>
If you want to handle it on parent side, you may use $emit to send an event upwards.
Can we have two versions of products? One for the server, one for v-models.
var server_products = [
{ id: 1, name: "Prod 1", stock: 5 },
{ id: 2, name: "Prod 2", stock: 6 }
]
//...
data: () => ({
products = server_products
})
updateStock(stock, productId) {
server_products.forEach((product) => {
if(product.id === productId && stock !== product.stock){
product.stock = stock
// do ajax
}
})
},
//...
If not than you can use vue's watch property so vue finds changes to the array for you.
//...
data: () => ({
products: [
{ id: 1, name: "Prod 1", stock: 5 },
{ id: 2, name: "Prod 2", stock: 6 }
]
}),
watch: {
'products': {
handler: function(newValue) {
// do ajax
},
deep: true
}
}
//...

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>

:class not updating when clicked in vuejs v-for

I want to add a css class to a item in v-for when the td in clicked
<template>
<div>
<h1>Forces ()</h1>
<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>
<section v-if="loading">
<p>loading...</p>
</section>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th
#click="orderByName = !orderByName">Forces</th>
</tr>
<th>Delete</th>
</thead>
<tbody>
<tr v-for="(force, index) in forces" #click="completeTask(force)" :class="{completed: force.done}" v-bind:key="index">
<td>
<router-link :to="{ name: 'ListForce', params: { name: force.id } }">Link</router-link>
</td>
<th>{{ force.done }}</th>
<th>{{ force.name }}</th>
<th><button v-on:click="removeElement(index)">remove</button></th>
</tr>
</tbody>
</table>
<div>
</div>
</div>
</template>
<script>
import {APIService} from '../APIService';
const apiService = new APIService();
const _ = require('lodash');
export default {
name: 'ListForces',
components: {
},
data() {
return {
question: '',
forces: [{
done: null,
id: null,
name: null
}],
errored: false,
loading: true,
orderByName: false,
};
},
methods: {
getForces(){
apiService.getForces().then((data, error) => {
this.forces = data;
this.forces.map(function(e){
e.done = false;
});
this.loading= false;
console.log(this.forces)
});
},
completeTask(force) {
force.done = ! force.done;
console.log(force.done);
},
removeElement: function (index) {
this.forces.splice(index, 1);
}
},
computed: {
/* forcesOrdered() {
return this.orderByName ? _.orderBy(this.forces, 'name', 'desc') : this.forces;
}*/
},
mounted() {
this.getForces();
},
}
</script>
<style>
.completed {
text-decoration: line-through;
}
</style>
I want a line to go through the items when clicked, but the dom doesn't update. If I go into the vue tab in chrome I can see the force.done changes for each item but only if I click out of the object and click back into it. I'm not to sure why the state isn't updating as it's done so when I have used an click and a css bind before.
I've tried to make minimal changes to get this working:
// import {APIService} from '../APIService';
// const apiService = new APIService();
// const _ = require('lodash');
const apiService = {
getForces () {
return Promise.resolve([
{ id: 1, name: 'Red' },
{ id: 2, name: 'Green' },
{ id: 3, name: 'Blue' }
])
}
}
new Vue({
el: '#app',
name: 'ListForces',
components: {
},
data() {
return {
question: '',
forces: [{
done: null,
id: null,
name: null
}],
errored: false,
loading: true,
orderByName: false,
};
},
methods: {
getForces(){
apiService.getForces().then((data, error) => {
for (const force of data) {
force.done = false;
}
this.forces = data;
this.loading= false;
console.log(this.forces)
});
},
completeTask(force) {
force.done = ! force.done;
console.log(force.done);
},
removeElement: function (index) {
this.forces.splice(index, 1);
}
},
mounted() {
this.getForces();
}
})
.completed {
text-decoration: line-through;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<div>
<h1>Forces ()</h1>
<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>
<section v-if="loading">
<p>loading...</p>
</section>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>ID</th>
<th
#click="orderByName = !orderByName">Forces</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(force, index) in forces" #click="completeTask(force)" :class="{completed: force.done}" v-bind:key="index">
<th>{{ force.done }}</th>
<th>{{ force.name }}</th>
<th><button v-on:click="removeElement(index)">remove</button></th>
</tr>
</tbody>
</table>
<div>
</div>
</div>
</div>
The key problem was here:
this.forces = data;
this.forces.map(function(e){
e.done = false;
});
The problem here is that the property done is being added to the data after it has been made reactive. The reactivity observers get added as soon as the line this.forces = data runs. Adding done after that counts as adding a new property, which won't work.
It's also a misuse of map so I've switched it to a for/of loop instead.
for (const force of data) {
force.done = false;
}
this.forces = data; // <- data becomes reactive here, including 'done'
On an unrelated note I've tweaked the template to move the Delete header inside the top row.
Make sure force.done is set to false in data before initialization so it is reactive.
data() {
return {
force:{
done: false;
}
}
}
If force exists but has no done member set, you can use Vue.set instead of = to create a reactive value after initialization.
Vue.set(this.force, 'done', true);

dynamically call an object property in a v-for loop

so i ran into a problem again,
i want to make a table component where you can send a array to the component, and it will render a table for you
we set it up like this
<template>
<section class="container">
<Apptable :search="true" :loader="true" title="User data" :data="users"/>
</section>
</template>
<script>
import Apptable from "~/components/table.vue";
export default {
components: {
Apptable
},
data() {
return {
users: [
{
id: 1,
name: "Lars",
Adres: "hondenstraat 21",
phone: "06555965"
},
{
id: 1,
name: "John",
Adres: "verwelstraat 35",
phone: "06555965"
}
]
};
}
};
</script>
i send data to the component and loop it from there like this
<template>
<section class="container">
<h2 v-if="title">{{title}}</h2>
<input v-if="search" class="search" placeholder="Search">
<button v-if="loader" class="update" #click="dialog = true">Update</button>
<table class="table">
<thead>
<tr class="tableheader">
<th v-for="(item, index) in Object.keys(data[0])" :key="index">{{item}}</th>
</tr>
</thead>
<tbody>
<tr class="userdata" v-for="(item, index) in data" :key="index">
<td v-for="(name, index) in Object.keys(data[index])" :key="index">{{//TODO: I WANT TO SELECT THE ITEM.DYNAMIC PROPERTY}}</td>
</tr>
</tbody>
</table>
<loader v-if="loader" :trigger="dialog"/>
</section>
</template>
<script>
import loader from "~/components/loader.vue";
export default {
components: {
loader
},
data() {
return {
dialog: false
};
},
watch: {
dialog(val) {
if (!val) return;
setTimeout(() => (this.dialog = false), 1500);
}
},
props: {
data: {
type: Array,
required: true
},
title: {
type: String,
required: false,
default: false
},
loader: {
type: Boolean,
required: false,
default: false
},
search: {
required: false,
type: Boolean,
default: true
}
}
};
</script>
so if you look at the table. were i left the todo, if i put in the {{item}} in the todo place. i will get this in my column
enter image description here
but i want to select the key of the object dynamically. but if i put {{item.name}} in the todo place it will not select the key dynamically.
so the point is that i want to dynamically call a property from the object in the v-for so the columns will get the data in the cells.
You should use item[name] instead of item.name
<tbody>
<tr class="userdata" v-for="(item, index) in data" :key="index">
<td v-for="(name, nIndex) in Object.keys(data[index])" :key="nIndex">
{{ item[name] }}
</td>
</tr>
</tbody>

V-model does not get updated after checkbox clicked

Any idea how to resolve this problem:
in this example, the author uses vue 2.3.2 which works perfect,
new Vue({
el: '#app',
data: {
users: [{
"id": "Shad",
"name": "Shad"
},
{
"id": "Duane",
"name": "Duane"
},
{
"id": "Myah",
"name": "Myah"
},
{
"id": "Kamron",
"name": "Kamron"
},
{
"id": "Brendon",
"name": "Brendon"
}
],
selected: [],
allSelected: false,
userIds: []
},
methods: {
selectAll: function() {
this.userIds = [];
if (this.allSelected) {
for (user in this.users) {
this.userIds.push(this.users[user].id.toString());
}
}
},
select: function() {
this.allSelected = false;
}
}
})
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
<div id="app">
<h4>User</h4>
<div>
<table>
<tr>
<th>Name</th>
<th>Select All<input type="checkbox" #click="selectAll" v-model="allSelected"></th>
</tr>
<tr v-for="user in users">
<td>{{ user.name }}</td>
<td><input type="checkbox" v-model="userIds" #click="select" :value="user.id"></td>
</tr>
</table>
</div>
<span>Selected Ids: {{ userIds }}</span>
</div>
when I switch it to 2.5.16 ( <script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script> ) , the behavior is wierd:
When click the selectAll checkbox, only that checkbox checked, but when I toggle it to uncheck, all the checkboses below get checked
For consistent browser functionality, I can recommended to not use click/change on checkboxes. Instead, bind the checkbox to a value (which you've already done), and then use a watcher on the value. This way, the real value of the checkbox will always accurately represent it's state. So you'd have something like this:
<input type="checkbox" v-model="allSelected">
Vue.component({..., {
data: function() {
return {
allSelected: false,
}
}
},
watch: {
allSelected: function(val){
//Use your source of truth to trigger events!
this.doThingWithRealValue(val);
}
}
});
You're already using your component data value of allSelected as the source of truth, so you should use this source of truth as the real triggering element value, not a click. Whenever the value of allSelected changes, your code will get ran. This solves the problem without the rendering order weirdness.
As pointed out by rob in the comments and in his answer you cannot rely on #click / #input / #change to have the same behaviour in all browsers in regards to their execution order relative to the actual model change.
There is an issue at the VueJS repository with a bit more context: https://github.com/vuejs/vue/issues/6709
The better solution is to watch the model for changes and then react accordingly.
new Vue({
el: '#app',
data: {
users: [{
"id": "Shad",
"name": "Shad"
},
{
"id": "Duane",
"name": "Duane"
},
{
"id": "Myah",
"name": "Myah"
},
{
"id": "Kamron",
"name": "Kamron"
},
{
"id": "Brendon",
"name": "Brendon"
}
],
selected: [],
allSelected: false,
userIds: []
},
methods: {
selectAll: function() {
this.userIds = [];
if (this.allSelected) {
for (user in this.users) {
this.userIds.push(this.users[user].id.toString());
}
}
},
select: function() {
this.allSelected = false;
}
},
watch: {
allSelected: function () {
this.selectAll()
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<h4>User</h4>
<div>
<table>
<tr>
<th>Name</th>
<th>Select All<input type="checkbox" v-model="allSelected"></th>
</tr>
<tr v-for="user in users">
<td>{{ user.name }}</td>
<td><input type="checkbox" v-model="userIds" #click="select" :value="user.id"></td>
</tr>
</table>
</div>
<span>Selected Ids: {{ userIds }}</span>
</div>