Should I be using a computed property here? - vue.js

My apologies if this is a simple question.
I have a table that's being built in vue.js
Column A is for numerical input, column B has a preset value and Column C calculates the difference between them.
Currently I'm using a computed property that loops through the rows, calculates the difference and stores that in my data array, then I'm calling the value {{row.difference}} in the table cells.
I've called my computed property difference, however it only works if I include {{difference}} within the element div.
Is that bad usage? Should I be calling a method on each row instead and returning the calculated value?

Computed values never should mutate data.
I would suggest that your computed value return a new array with the difference in it.
Vue.config.devtools = false;
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data: {
items: [{
a: 10,
b: 4
},
{
a: 443,
b: 23
}
]
},
computed: {
items_with_difference() {
return this.items.map((i) => ({
...i,
difference: i.a - i.b
}));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table>
<thead>
<th>
A
</th>
<th>
B
</th>
<th>
Difference
</th>
</thead>
<tbody>
<tr v-for="(item, idx) in items_with_difference" :key="idx">
<td>
{{item.a}}
</td>
<td>
{{item.b}}
</td>
<td>
{{item.difference}}
</td>
</tr>
</tbody>
</table>
</div>

Related

nested v-for loop not rendering

I am trying to display a table full of Orders and the corresponding Order details in a row-like fashion. However, I've run into a bit of a conundrum. Any geniuses out there? I've tried all sorts of variations in approaching this problem but I'm stuck. Someone mentioned that perhaps using a 'computed' property would do the trick but I haven't been able to get it to work >=/. A bit of help, maybe a lot of help, would be greatly appreciated!
<div id="MyOrdersApp" class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr style="color:black;font-size:2em;">
<th style="text-align:center;">Order Id</th>
<th style="text-align:center;">ProductIdsAndQuantity</th>
<th style="text-align:center;">Total Cost</th>
<th style="text-align:center;">Order Status</th>
<th style="text-align:center;">Shipment Tracking URL</th>
<th style="text-align:center;">Created Date</th>
</tr>
</thead>
<tbody>
<tr v-for="(order, i) in parsedOrders" style="background-color:snow;text-align:center;" class="align-content-center;">
<td>{{order.OrderId}}</td>
<td>
<ul>
<li v-if="order.CartLineItems != null" v-for="item in order.CartLineItems;" :key="uniqueKey">
<img :src="item.ProductImageUrl" />
<span>{{item.ProductName}} | </span>
<span>Quantity: {{item.Quantity}} | </span>
</li>
</ul>
</td>
<td style="color:black;font-size:1.5em;font-weight:bold;">{{order.TotalRevenue | currency }}</td>
<td>{{order.OrderStatus}}</td>
<td>{{order.ShipmentTrackingURL}}</td>
<td>{{order.CreatedDate | formatDate }}</td>
</tr>
</tbody>
</table>
</div>
<script>
var MyOrdersApp = new Vue({
el: '#MyOrdersApp',
data: {
parsedOrders: [],
theOrders: [],
uniqueKey: 0
},
computed: {
cartLineItems: function () {
return this.parsedOrders[0].CartLineItems;
}
},
methods: {
getMyOrders: function () {
var scope = this;
this.$http.get("https://" + location.host + "/Home/GetMyOrders").then(resp => {
if (resp.status == 200) {
this.theOrders = resp.body;
scope.setCartLineItems();
}
});
},
setCartLineItems: function () {
var scope = this;
var cartLineItems = [];
var cartDict = {};
for (var i = 0; i < this.theOrders.length; i++) {
cartDict = {};
cartLineItems = [];
cartDict = JSON.parse(this.theOrders[i].ProductIdsAndQuantity).CartLineItems;
for (var key in cartDict) {
var lineItem = JSON.parse(cartDict[key]);
cartLineItems.push(lineItem);
}
//this.allorders[i].CartLineItems = cartLineItems;
//scope.$set(this.allorders[i], 'CartLineItems', cartLineItems);
//this.theOrders[i].CartLineItems = cartLineItems;
scope.$set(this.theOrders[i], 'CartLineItems', cartLineItems);
}
this.parsedOrders = this.theOrders;
console.log("~~ Parsed Cart! ~~ ");
console.log(this.parsedOrders);
console.log(this.parsedOrders[1].CartLineItems);
}
},
mounted: function () {
var scope = this;
this.getMyOrders();
setTimeout(x => { scope.$forceUpdate(); scope.uniqueKey++; }, 1000);
}
});
</script>
How should I modify this to properly display the values in 'CartLineItems'??
Btw, parsedOrders looks like this:
If I comment out the inner v-for, the other columns display just fine and the table looks like this:
Some more background... when I fetch the orders via the $http call and the server returns the array of JSON, the ProductIdsAndQuantity property needs to be parsed as a JSON object and then set as it's own property on the order in question. The tricky part here is that the Vue component seems to not be reacting to the change in the order object array data. Hence, the need for a parsedOrders property and/or the use of scope.$forceUpdate(); or scope.uniqueKey++;. These were proposed solutions to the issue of the Vue component not re-rendering. However, these solutions are not working. So I'm stuck scratching my head...
You can't have both v-for and v-if on the same statement that's what is causing the issue. So instead try using computed in this case
<tbody>
<tr v-for="(order, i) in parsedOrders" style="background-color:snow;text-align:center;" class="align-content-center;">
<td>{{order.OrderId}}</td>
<td>
<ul>
<li v-for="(item, index) in getCartLineItems(order)" :key="index">
<img :src="item.ProductImageUrl" />
<span>{{item.ProductName}} | </span>
<span>Quantity: {{item.Quantity}} | </span>
</li>
</ul>
</td>
<td style="color:black;font-size:1.5em;font-weight:bold;">{{order.TotalRevenue | currency }}</td>
<td>{{order.OrderStatus}}</td>
<td>{{order.ShipmentTrackingURL}}</td>
<td>{{order.CreatedDate | formatDate }}</td>
</tr>
</tbody>
and in computed
computed: {
getCartLineItems() {
return order => {
if(order?.CartLineItems && order.CartLineItems.length) return order.CartLineItems;
return [];
}
}
}

Using v-for for fixed numbers and not objects

I'm trying to render a simple table with 20 rows and 5 columns with v-for but I'm having problems. My code:
<tr v-for="row in totalRows" :key="row">
<td v-for="col in totalColumns" :key="col">
{{ getTableNum() }}
</td>
</tr>
In data:
totalColumns: 5,
totalRows: 20,
numberCount: 0,
Method:
getTableNum() {
return ++this.numberCount;
},
It is throwing a warning...
[Vue warn]: You may have an infinite update loop in a component render function.
... and rendering like 20k rows.
I can't find an example on how use v-for for fixed numbers(only using objects) anywhere.
I imagine that those loops above would reproduce a result like:
for (let row = 0; row < totalRows; row++) {
for (let col = 0; col < totalCols; col++) {
getTableNum();
}
}
But I'm wrong for some reason.
UPDATE: Ended up using no variables at all:
<tr v-for="row in 20" :key="row">
<td v-for="col in 5" :key="col">
{{ col + (row - 1) * 5 }}
</td>
</tr>
I wish official docs could have examples like that. If I knew that fixed numbers could be used in the for loop it would spare me some time.
Running methods inside the template leads to some infinite rendering loops since the variable is also used in template, to avoid this create a two-dimensional array as a computed property and then render it :
computed:{
arr(){
let n=0;
return [...Array(20)].map((_,i)=>[...Array(5)].map((_,j)=>{
++n;
return n;
}))
}
}
in template :
<tr v-for="(row,i) in arr" :key="i">
<td v-for="col in row" :key="col">
{{ col }}
</td>
</tr>
The getTableNum function execution changes the numberCount in data and it triggers Vue to re-render the template, which causes the function execution, and so on.
In this case, you should try to avoid altering the numberCount value.
If I didn't get you wrong, you wish to have 1-2-3-4-5 in the first row, 6-7-8-9-10 in the second, and so on.
If so, you can try to rewrite your function as such:
getTableNum(row, col) {
// row and col are 1-based
return 1 + ((row - 1) * this.totalColumns) + (col - 1);
},
You are changing numberCount (which is a reactive data property) directly in the template. That triggers a re-render (and thus an infinite loop). You can simply do this :
var app = new Vue({
el: '#app',
data: {
totalColumns: 5,
totalRows: 20
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border=1>
<tr v-for="(row, rowIndex) in totalRows" :key="row">
<td v-for="(col, columnIndex) in totalColumns" :key="col">
{{ rowIndex * totalColumns + col}}
</td>
</tr>
</table>
</div>
Another alternative ONLY for static tables (where the table content is not modified after initial rendering) is the use of v-once directive. Each table row will be rendered only once, and every subsequent call to getTableNum function will not trigger the rerendering of previous rows:
var app = new Vue({
el: '#app',
data: {
totalColumns: 5,
totalRows: 20,
numberCount: 0
},
methods: {
getTableNum() {
return ++this.numberCount
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border=1>
<tr v-once v-for="(row, rowIndex) in totalRows" :key="row">
<td v-for="(col, columnIndex) in totalColumns" :key="col">
{{ getTableNum() }}
</td>
</tr>
</table>
</div>

iterating over multiple table rows with v-for

In a Vue app, I want to render multiple table rows for each item in a collection. Currently the (simplified) markup that renders the table body is
<tbody>
<template v-for="item in collection">
<tr>
<td>{{item.foo}}</td>
<td>{{item.bar}}</td>
</tr>
<tr>
<td>{{item.foo2}}</td>
<td>{{item.bar2}}</td>
</tr>
</template>
<tbody>
However, the problem with this is that there's no key defined, if I try to add one with
<template v-for="item in collection" :key="item.id">
Then I get an eslint error informing me that keys are only allowed on real elements. I can't replace <template> with a real element such as
<tbody>
<div v-for="item in collection" :key="item.id">
<tr>
<td>{{item.foo}}</td>
<td>{{item.bar}}</td>
</tr>
<tr>
<td>{{item.foo2}}</td>
<td>{{item.bar2}}</td>
</tr>
</div>
<tbody>
Because the only element that can be nested inside <tbody> is a <tr>. How can I add a key without violating either the HTML nesting rules or eslint rules?
Rather than trying to reshape the template to fit the data, you may be able to reshape the data to fit the template. Here's an example where the collection is split into an array of rows so that a simple v-for can be used with <td> elements:
<template>
<tbody>
<tr v-for="(item, index) in rows" :key="index">
<td>{{ item.column1 }}</td>
<td>{{ item.column2 }}</td>
</tr>
</tbody>
</template>
const ITEMS = [
{ foo: 'a1', bar: 'a2', foo2: 'b1', bar2: 'b2' },
{ foo: 'c1', bar: 'c1', foo2: 'd2', bar2: 'd2' },
];
export default {
data() {
return { items: ITEMS };
},
computed: {
rows() {
const result = [];
this.items.forEach(({ foo, bar, foo2, bar2 }) => {
result.push({ column1: foo, column2: bar });
result.push({ column1: foo2, column2: bar2 });
});
return result;
},
},
};

How can I iterate over array data and create new row for every new data in vue?

I have a stream data that I call through axios.get and the resposne.data is a json with one sensor's data like ,{"Temperature":"56"}, with this I use to create a row in vue template.The resposne.data is assigned to array but its not appending rows ,the data in table are getting changed.
This is the template part
<template>
<div class="container">
<h1>Tabular Data</h1>
<table class="table table-striped table-bordered" >
<thead>
<tr>
<th class="text-center">Time</th>
<th class="center">Device Parameter</th>
<th class="center">Magnitude</th>
<th class="right">Unit</th>
</tr>
</thead>
<tbody>
<tr v-for="data in Tabular" :key="data">
<td> {{new Date().toString()}} </td>
<td class="test-left">Temperature</td>
<td class="text-center"> {{data.toString()}} </td>
<td class="test-left">C</td>
</tr>
<!-- <td>{{data_alias.Humidity}}</td>
<td>{{data_alias.Pressure}}</td> -->
</tbody>
</table>
</div>
</template>
<script>
/* eslint-disable */
import axios from 'axios';
export default {
name: 'tabular',
data() {
return {
Tabular:[ ],
}
},
mounted() {
setInterval(()=>{
axios.get('http://localhost:3000/data')
.then((response)=>{
console.log(response.data);
this.Tabular=response.data;
})
.catch((error)=>{
console.log(error);
});
},2000);//I used set interval to get data from stream constantly
},
}
</script>
are you sure Tabular fill at one even?
I think no, because this.Tabular=response.data; ;
this.Tabular is undefined ;
try this
var that = this;
axios.get('http://localhost:3000/data')
.then((response)=>{
console.log(response.data);
that.Tabular=response.data;
})
.catch((error)=>{
console.log(error);
});
That is because you are replacing the content of this.Tabular. You should loop through your response data and, considering response.data is an array of rows, use
response.data.forEach((row) => { this.Tabular.push(row) })

Removing a row from a table with VueJS

In Vue how do you remove a row from a table when the item is deleted?
Below is how I am rendering the table
<tbody>
<tr v-for="item in items">
<td v-text="item.name"></td>
<td v-text="item.phone_number"></td>
<td v-text="item.email"></td>
<td><button #click="fireDelete(item.id)">Delete</button></td>
</tr>
</tbody>
Below is an excerpt from my Vue component.
data() {
return {
items: []
}
},
methods: {
fireDelete(id) {
axios.delete('/item/'+id).then();
}
},
mounted() {
axios.get('/item').then(response => this.items = response.data);
}
The axios.get work and so does the axios.delete, but the fronend doesn't react so the item is only removed from the table after a page refresh. How do I get it to remove the relevant <tr>?
I've managed to work out a nice way. I pass the index to the fireDelete method and use the splice function. Works exactly how I wanted.
<tbody>
<tr v-for="(item, index) in items" v-bind:index="index">
<td v-text="item.name"></td>
<td v-text="item.phone_number"></td>
<td v-text="item.email"></td>
<td><button #click="fireDelete(item.id, index)">Delete</button></td>
</tr>
</tbody>
fireDelete(id, index) {
axios.delete('/item/'+id).then(response => this.organisations.splice(index, 1));
}
I had the same trouble as this question. So maybe someone will find this usefull.
For the button use:
v-if="items.length > 1" v-on:click="fireDelete(index)"
And the fireDelete function:
fireDelete: function (index) {
this.photos.splice(index, 1);
}
You can try to modify your #click="fireDelete(item.id)" part to a custom method #click='deleteData(items, item.id)'
and do something like:
methods: {
deleteData (items, id) {
this.items = null // These parts may not
this.fireDelete(id) // match your exact code, but I hope
} // you got the idea.
}
and your template can do just:
<tbody>
<tr v-for="item in items" v-if='items'>
<td v-text="item.name"></td>
<td v-text="item.phone_number"></td>
<td v-text="item.email"></td>
<td><button #click="deleteData(item, item.id)">Delete</button></td>
</tr>
</tbody>