Delete row from Vue Js dynamic array - vue.js

I am trying to create a dynamic table in Vue. I am able to add row properly, but when I try to remove the row, every time it removes the last row by default.
Even if I hard coded the index number, still same result.
Its been 2 days and I am stuck in this problem. Appreciate any help.
My Template:
<div class="box-body">
<table id="myTable" class="table">
<thead>
<tr class="timesheetTableHeader">
<th style="width: 50rem">Project</th>
<th style="width: 40rem">Activity</th>
<th style="width: 20rem">Charge Back</th>
<th style="width: 10rem">Hours</th>
</tr>
</thead>
<div class="mt-2"></div>
<tbody>
<tr v-for="(row, index) in rows">
<td>
<mwselect :options="Project"
v-model="row.projectName"
></mwselect>
</td>
<td>
<mwselect :options="Activity"
v-model="row.activity"
></mwselect>
</td>
<td>
<mwselect :options="CostCenter"
v-model="row.chargeBackCC"
></mwselect>
</td>
<td>
<b-form-input type="number"
class="pb-1 pt-1"
required
v-model="row.hours"
name="Hours">
</b-form-input>
</td>
<td>
<a #click="removeElement(index)" style="cursor: pointer">
<i class="fa fa-trash-o pt-1" style="color:red; text-shadow: 1px 1px 1px #ccc;
font-size: 1.5em;" title="Delete Line"
></i></a>
</td>
</tr>
</tbody>
</table>
</div>
My Script:
<script>
var period_name = ''
var start_date = ''
var end_date = ''
var counter = 0
export default {
components: {
mwselect
},
data() {
return {
Project: projects,
CostCenter: costcenter,
Activity: activities,
rows: []
} // return end
},
methods: {
createLine(index) {
var elem = document.createElement('tr');
this.rows.push({
projectName: "",
activity: "",
chargeBackCC: "",
hours: ""
})
},
removeElement: function (index) {
alert(index)
this.rows.splice(index, 1);
}
}
}
</script>

Ordering is not guaranteed unless you put key attribute to the for loop. So this change to your tag must solve the problem:
<tr v-for="(row, index) in rows" :key="index">
Here is more information about the attribute key: https://v2.vuejs.org/v2/guide/list.html#key

Thanks Cmertayak,
Adding :key="index" worked and it solved my problem.

Related

Sorting table by project

I have an object that looks like this
projects: Object
Admin: Object
Project 1: Array[1]
0: Object
name: Admin Project 1
description: Admin Project 1 Description
Project 2: Array[2]
0: Object
name: Admin Project 2 part 1
description: Admin Project 2 part 1 Description
1: Object
name: Admin Project 2 part 2
description: Admin Project 2 part 2 Description
And what I'm trying to do is to display this information on a table.
The issue I'm having is that because of the way I'm creating the table I'm not able to group them by their sub category (EG: Project 2).
So my table would look like this.
Codepen of how I want it to look like
This is how it is looking like at the moment.
Codepen of the wrong way the table looks like
Here is my code
<table class="table table-bordered table-sm" style="width: 100%" v-for="(value, key) in projects">
<tbody>
<tr style="font-size: 90%; background: #a0a0a0; color: #fafafa; font-weight: normal">
<th colspan="8" style="text-indent: 8px;">{{key}}</th>
</tr>
<tr style="font-size: 90%; background: #d0d0d0; color: #606060;" v-for="(v, k) in value">
<th colspan="7" style="text-indent: 32px;">{{ k }}</th>
</tr>
</tbody>
<tbody>
<tr v-for="(p, kk) in value">
<td>
<table>
<tr v-for="vp in p">
<td>{{ vp.name }}</td>
<td>{{ vp.description }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
The tbody elements are structurize themselves below each other (see table)
If you add the contents of the projects inside the same tbody element as additional rows it should work:
<div id="app">
<table class="table table-bordered table-sm" style="width: 100%" v-for="(value, key) in projects">
<thead>
<tr style="font-size: 90%; background: #a0a0a0; color: #fafafa; font-weight: normal">
<th colspan="8" style="text-indent: 8px;">{{key}}</th>
</tr>
</thead>
<tbody v-for="(v, k) in value">
<tr style="font-size: 90%; background: #d0d0d0; color: #606060;">
<th colspan="7" style="text-indent: 32px;">{{ k }}</th>
</tr>
<tr v-for="vp in v">
<td>{{ vp.name }}</td>
<td>{{ vp.description }}</td>
</tr>
</tbody>
</table>
</div>
EDIT: I update the code to use multiple tbody elements and a thead for the object root – should be semantically more correct I think.
This happens because all your for loop have to be imbricated inside parent divs
Here is a much simpler example using only div.
new Vue({
el: "#app",
data: {
projects: {
Admin: {
'Project 1': [
{name: 'Admin Project 1', description: 'Admin Project 1 Description'}
],
'Project 2': [
{name: 'Admin Project 2 part 1', description: 'Admin Project 2 part 1 Description'},
{name: 'Admin Project 2 part 2', description: 'Admin Project 2 part 2 Description'}
]
}
}
},
methods: {
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(value, key) in projects">
{{key}}
<div v-for="(value2, key2) in value">
{{key2}}
<div v-for="(value3) in value2">
{{value3.name}} - {{value3.description}}
</div>
</div>
</div>
</div>
So in your case you have to think to another model of data.
Observations :
You forget to add :key attribute for each iteration. That is a good practice and helps in rendering the DOM.
You added one extra iteration in your code which causes the issue in displaying the table properly.
Working Demo :
new Vue({
el: "#app",
data: {
projects: {
Admin: {
'Project 1': [
{name: 'Admin Project 1', description: 'Admin Project 1 Description'}
],
'Project 2': [
{name: 'Admin Project 2 part 1', description: 'Admin Project 2 part 1 Description'},
{name: 'Admin Project 2 part 2', description: 'Admin Project 2 part 2 Description'}
]
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table class="table table-bordered table-sm" style="width: 100%" v-for="(value, key) in projects" :key="key">
<thead>
<tr style="font-size: 90%; background: #a0a0a0; color: #fafafa; font-weight: normal">
<th colspan="8" style="text-indent: 8px;">{{key}}</th>
</tr>
</thead>
<tbody v-for="(v, i) in Object.keys(value)" :key="i">
<tr style="font-size: 90%; background: #d0d0d0; color: #606060;">
<th colspan="7" style="text-indent: 32px;">{{ v }}</th>
</tr>
<tr v-for="(p, ii) in value[v]" :key="ii">
<td>
<table>
<tr>
<td>{{ p.name }}</td>
<td>{{ p.description }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>

Unable to calculate product qty

I'm trying to calculate the quantity of items in my vue. The problem I'm having is that my computed property isn't picking up my object, because my thinking was as you can see with the commented out section is that I was going to loop through it and calculate the quantity, but since I'm not able to grab productItems I'm not able to loop through it.
Here is my code
<template>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<tr v-for="item in products">
<td>
{{ item['name'] }}
</td>
<td>
<input style="width: 100px; margin-left: 0; display: inline"
type="number" class="form-control"
v-model="productItems[item['name']]['unit']"
>
</td>
</tr>
<tr>
<td></td>
<td>
Consumption total: {{ consumptionTotal }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
props: [],
data(){
return {
productItems: {}
},
computed:{
consumptionTotal(){
console.log(this.productItems);
// return Object.keys(this.productItems).reduce((carry, item) => {
// carry += Number(this.productItems[item]['unit'])
// return carry;
// }, Number(0));
},
},
watch: {
},
methods: {
},
mounted() {
}
}
</script>
Try below steps. Hopefully it will helps you.
Step 1: productItems should be an array
Step 2: Calculate functions be like
computed: {
consumptionTotal () {
return this.productItems.reduce((total, item) => {
total += item.unit
return total
}, Number(0))
}
}
Step 3: HTML template will be like
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<tr v-for="item in productItems" :key="item.id">
<td>{{item.id}}</td>
<td>
{{item.name}}
</td>
<td>
<input style="width: 100px; margin-left: 0; display: inline" type="number" class="form-control" :value="item.unit">
</td>
</tr>
<tr>
<td></td>
<td>
Consumption total: {{ consumptionTotal }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
DEMO

Can't add new item using v-model in Vue JS

I am learning Vue.
Now, I am trying to add data with the price and finally, it calculates total price:
Here is the HTML
<div id="app">
<form #submit.prevent="addItem">
<table border="1" cellpadding="10" width="300">
<tr>
<td colspan="2"><strong>Add New Item</strong></td>
</tr>
<tr>
<td>
<input type="text" name="" v-model="newItem" placeholder="Item Name">
</td>
<td>
<input type="number" name="" v-model="newItemPrice" placeholder="Item Price">
</td>
</tr>
</table>
</form>
<br>
<table border="1" cellpadding="10" width="400">
<tr>
<th>Item Name</th>
<th>Item Price</th>
</tr>
<tr v-for="(item, index) in items" :key="index">
<td>{{ item.name }}</td>
<td><input type="number" name="" v-model="item.price"></td>
<td><button #click="removeItem(index)">X</button></td>
</tr>
<tr>
<td>Total</td>
<td><strong>{{ total }}</strong></td>
</tr>
</table>
</div>
Here is the Vue Instance:
new Vue({
el : '#app',
data : {
items: [
{ name: 'Rice', price : 12.60 },
{ name: 'Oil', price : 22.00 },
{ name: 'Mango', price : 32.50 },
{ name: 'Orange', price : 42.00 },
],
newItem : '',
newItemPrice : '',
},
computed: {
total() {
var total = 0;
this.items.forEach( item => {
total += parseFloat( item.price );
})
return total;
}
},
methods: {
addItem() {
this.items.push({
name: this.newItem,
price: 0
});
},
removeItem( index ) {
this.items.splice( index, 1 )
}
}
});
You can see it's by default showing item name and price. I want to add new item using the v-model called newItem But It's not adding the new item to the table
BUT
If I remove the Item Price column I mean this line:
<td>
<input type="number" name="" v-model="newItemPrice" placeholder="Item Price">
</td>
then it's adding the new item perfectly :(
can you tell me what's wrong here?
See two issues with the fiddle:
There is no way to submit the form data
When pushing the price field was not added to the object
After fixing both of them it works well in this fiddle.
This happens because of browser implementation. As mentioned in W3C Specs:
When there is only one single-line text input field in a form, the user agent should accept Enter in that field as a request to submit the form.
But in case of multiple elements, the enter keypress does not trigger the form submit and thus you get this behaviour.
To resolve this you can simply use #keyup.enter.prevent="addItem" to listen to the enter keypress on each input and call the addItem() function like:
new Vue({
el: '#app',
data: {
items: [{name:"Rice",price:12.6},{name:"Oil",price:22},{name:"Mango",price:32.5},{name:"Orange",price:42}],
newItem: '',
newItemPrice: null,
},
computed: {
total() {
var total = 0;
this.items.forEach(item => {
total += parseFloat(item.price);
})
return total;
}
},
methods: {
addItem() {
this.items.push({
name: this.newItem,
price: 0
});
},
removeItem(index) {
this.items.splice(index, 1)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
<div id="app">
<form #submit.prevent="addItem">
<table border="1" cellpadding="10" width="300">
<tr>
<td colspan="2"><strong>Add New Item</strong></td>
</tr>
<tr>
<td>
<input type="text" name="" v-model="newItem" placeholder="Item Name"
#keyup.enter.prevent="addItem">
</td>
<td>
<input type="number" name="" v-model="newItemPrice" placeholder="Item Price"
#keyup.enter.prevent="addItem">
</td>
</tr>
</table>
</form>
<br>
<table border="1" cellpadding="10" width="400">
<tr>
<th>Item Name</th>
<th>Item Price</th>
</tr>
<tr v-for="(item, index) in items" :key="index">
<td>{{ item.name }}</td>
<td><input type="number" name="" v-model="item.price"></td>
<td><button #click="removeItem(index)">X</button></td>
</tr>
<tr>
<td>Total</td>
<td><strong>{{ total }}</strong></td>
</tr>
</table>
</div>
You should put a new line in your form, my suggestion is to put just above the close form tag </form>:
<input type="submit" value="add">
Another fix to do is in your methods addItem()
addItem() {
this.items.push({
name: this.newItem,
price: this.newItemPrice
});
}
Where it is the number 0 you should provide the this.newItemPrice to it work properly.

Passing b-icon to <td> element in VueJS

I want to pass a piece of HTML to a table-data-element using VueJS. The following demonstrates my scenario:
<template>
<div>
<div v-if="someObject.properties" style="margin-top: 20px;" class="table-responsive-md">
<table class="table table-striped">
<thead>
<tr>
<th style="text-align: left" scope="col">Some icons</th>
</tr>
</thead>
<tbody v-for="(property, index) in someObject.properties" :key="index">
<tr>
<td style="text-align: center" v-html="getIconWhenSomeRequirementIsMet(property)"/>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script lang="ts">
...
getIconWhenSomeRequirementIsMet (property: any): string {
if (property.type === 'animal') return '<b-icon-check class="h3 mb-0" style="color:green;"/>'
if (property.type === 'human') return '<b-icon-check class="h3 mb-0" style="color:yellow;"/>'
return '<b-icon-x class="h3 mb-0" style="color:red;"/>'
}
</script>
The code above is a minimal example of my Vue single file component. However, this way, I get empty fields in my table instead of the actual icons. Isn't there a simple and clean approach to achieve this?
The reason it doesn't work is because you can't use v-html to render custom components.
Instead, here's two different ways you can do this.
The first is to pre-define your b-icon-* and use v-if, v-else-if and v-else to match which icon to show.
The second is to dynamically bind properties using v-bind, this way you can use a method to do it, like you are now, but instead return the properties based on the type.
new Vue({
el: "#app",
data() {
return {
items: [
{ type: "animal" },
{ type: "human" },
{ type: "alien" },
],
fields: ['Type', 'Icon 1', 'Icon 2']
}
},
methods: {
getIconWhenSomeRequirementIsMet (type) {
/* Default properties */
const properties = {
icon: 'x',
style: 'color: red',
class: 'h3 mb-0'
};
if (type === 'animal') {
properties.icon = 'check';
properties.style = 'color: green;';
}
else if (type === 'human') {
properties.icon = 'check';
properties.style = 'color: yellow;';
}
return properties;
}
}
})
<link href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="//unpkg.com/bootstrap-vue#2.7.0/dist/bootstrap-vue.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app">
<div class="table-responsive-md">
<table class="table table-striped">
<thead>
<tr>
<th v-for="field in fields" >{{ field }}</th>
</tr>
</thead>
<tbody>
<tr v-for="{ type } in items">
<td>
{{ type }}
</td>
<td>
<b-icon-check v-if="type === 'animal'" variant="success" class="h3 mb-0">
</b-icon-check>
<b-icon-check v-else-if="type === 'human'" variant="warning" class="h3 mb-0">
</b-icon-check>
<b-icon-x v-else variant="danger" class="h3 mb-0">
</b-icon-x>
</td>
<td>
<b-icon v-bind="getIconWhenSomeRequirementIsMet(type)"></b-icon>
</td>
</tr>
</tbody>
</table>
</div>
</div>

vue.js v-for on two table rows

Vue 2, no webpack. I want to render two trs at a time, for main and detail expandable row. This is what I'm trying to achieve:
<table>
<tbody>
<div v-for="item in items">
<tr></tr>
<tr class="detail-row"></tr>
</div>
</tbody>
</table>
The problem is that <div> is an invalid child of tbody. How to render two <tr>s at each for loop iteration?
This is the way you solve it in browsers that support template.
<table>
<tbody>
<template v-for="item in items">
<tr></tr>
<tr class="detail-row"></tr>
</template>
</tbody>
</table>
If you need to support browsers that do not support template, I typically resort to a render function.
Here is a working example of both.
console.clear()
new Vue({
el: "#app",
data: {
items: [{
master: "Master",
detail: "Detail"
},
{
master: "Master",
detail: "Detail"
},
]
}
})
new Vue({
el: "#app2",
data: {
items: [{
master: "Master",
detail: "Detail"
},
{
master: "Master",
detail: "Detail"
},
]
},
render(h){
// build the rows
let rows = []
for (let item of this.items){
rows.push(h("tr", [h("td", [item.master])]))
rows.push(h("tr", {class: "detail-row"}, [h("td", [item.detail])]))
}
// add rows to body
let body = h("tbody", rows)
// return the table
return h("table", [body])
}
})
.detail-row{
background-color: lightgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<h2>Using template</h2>
<div id="app">
<table>
<tbody>
<template v-for="item in items">
<tr><td>{{item.master}}</td></tr>
<tr class="detail-row"><td>{{item.detail}}</td></tr>
</template>
</tbody>
</table>
</div>
<h2>Using render function</h2>
<div id="app2"></div>
In newer version of VueJS, it wants an index. So the solution would look like
<table>
<tbody>
<template v-for="(item, index) in items">
<tr :key="index">
<td>{{item.master}}</td>
</tr>
<tr :key="index" class="detail-row">
<td>{{item.detail}}</td>
</tr>
</template>
</tbody>
</table>
If you want to use in double tag.
Or want to use a separate component in the template div within the table tr tags (as in a new component) you could use style="display: contents" in the first div to keep the table rows inline with each other.
Vue component
<table>
<template v-for="v-for="(price, index) in prices">
<div :key="price.id" style="display: contents">
<tr><td>{{price.id}}</td><td>{{price.name}}</td></tr>
<tr col-span="2">{{price.desc}}</tr>
</div>
</template>
</table>
Or if you want to use a separate component for the rows
Table.vue
<template>
<div>
<table class="table">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
</tr>
</thead>
<tbody>
<template v-for="item in items">
<my-component :item=“item”/>
</template>
</tbody>
</table>
</div>
</template>
my-component.vue
<template>
<div style="display: contents">
<tr>
<td>{{item.firstname}}</td>
<td>{{item.lastname}}</td>
</tr>
<tr>
<td colspan="2" >
{{item.description}}
</td>
</tr>
</div>
</template>
Instead of using display: content in the div and you want the rows linked together you can use display: table-row-group type
<table>
<div v-for="v-for="(price, index) in prices"
v-bind:key="price.id"
style="display: table-row-group">
<tr>
<td>{{price.id}}</td><td>{{price.name}}</td>
</tr>
<tr col-span="2">
{{price.desc}}
</tr>
</div>
</table>