Vue 2 - Calculate total of rows input - vuejs2

I have a dynamic table with a quantity & price input, and I use a computed property to calculate each row's total.
Now I need to find a way to calculate the grandtotal (sum of all subtotals).
HTML:
<tr v-for="(item, index) in items">
<td><input v-model.number="item.qty" size="10"></td>
<td><input v-model.number="item.price" size="10"></td>
<td><input v-model.number="subtotalRow[index]" readonly size="10"></td>
<td><button #click="addRow(index)">+</button></td>
<td><button #click="removeRow(index)">-</button></td>
</tr>
<tr>
<td>Total: {{total}}</td>
</tr>
Javascript:
computed: {
subtotalRow() {
return this.items.map((item) => {
return Number(item.qty * item.price)
});
},
// the index part is confusing me
//
// total() {
// return this.items.reduce((total, ?) => {
// return total + ?;
// }, 0);
//}
},
I provided a small fiddle to make things clear.
https://jsfiddle.net/h5swdfv5/
I hope that some guidance can help me.
Thank you in advance

total() {
return this.items.reduce((total, item) => {
return total + item.qty * item.price;
}, 0);
}
Working Fiddle: https://jsfiddle.net/h5swdfv5/1/

Related

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

Binding input parameters to URL params in VueJS

Is there an easy way to bind the input parameters, to allow people to bookmark the Javascript calculation?
var app = new Vue({
el: '#app',
data: {
// dummy data
disksize: 100,
cost: 0.05,
items: [{
freq: "daily",
qty: 3,
ratio: 5
},
{
freq: "weekly",
qty: 0,
ratio: 10
},
{
freq: "yearly",
qty: 0,
ratio: 20
}
],
},
computed: {
initialCost() {
return Number(this.disksize * this.cost)
},
subtotalSize() {
return this.items.map((item) => {
return Number(item.qty * item.ratio / 100 * this.disksize)
});
},
subtotalCost() {
return this.items.map((item) => {
return Number(item.qty * item.ratio / 100 * this.disksize * this.cost)
});
},
subTotals() {
return this.items.reduce((subTotals, item) => {
return Number(subTotals + item.qty * item.ratio / 100 * this.disksize * this.cost)
}, 0);
},
total() {
return Number(this.initialCost + this.subTotals)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p><label>Size of disk: <input type=number v-model.number="disksize">GB</label></p>
<p><label>EBS cost: <input type=number step="0.001" v-model.number="cost">per GB-month of data stored</label></p>
<p><label>Cost of initial EBS disk: <input readonly :value="initialCost.toFixed(2)">USD per month</label></p>
<h3>
EBS snapshots
</h3>
<p>
EBS snapshots are incremental, so if you created a snapshot hourly each snapshot would only be backing up the changes that had been written to the volume in the last hour.
</p>
<table title="Retention">
<thead align="left">
<th>Frequency</th>
<th>Quantity</th>
<th>Ratio</th>
<th>Size</th>
<th>Cost per month</th>
</thead>
<tbody>
<tr v-for="(item, index) in items">
<td><input readonly :value="item.freq.charAt(0).toUpperCase() + item.freq.slice(1)" size="10"></td>
<td><input type="number" min="0" max="100" v-model.number="item.qty" size="10"></td>
<td><input type="number" min="0" max="100" v-model.number="item.ratio" size="3">%</td>
<td><input type="number" step="0.01" :value="subtotalSize[index].toFixed(2)" readonly size="10">GB</td>
<td><input type="number" step="0.01" :value="subtotalCost[index].toFixed(2)" readonly size="10">USD</td>
</tr>
</tbody>
</table>
<p>
<label>Total
{{initialCost.toFixed(2)}} initial cost + {{subTotals.toFixed(2)}} snapshots = <strong>{{total.toFixed(2)}} USD per month</strong>
</label>
</p>
</div>
I don’t want to use npm et al. Just prepackaged URLs like https://unpkg.com/vue-router/dist/vue-router.js ... if that's the solution. I'm not sure.
https://example.com/?disk=100&quantity=3&ratio=5
Quantity/Ratio can actually repeat, not sure what the at looks like in URL params. Any hints?
If I understand your question correctly, you want to do that allow an url like:
https://example.com/?disk=100&quantity=3&ratio=5
will passdisk,quantity and ratio props to your component.
This is achievable using vue router. One possible way is to use it in function mode:
You can create a function that returns props. This allows you to cast
parameters into other types, combine static values with route-based
values, etc.
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
If you simply want to get the values of a URL parameter and pass them along to a component, you can make use of Javascript's URLSearchParams() method (this won't work in IE 11, but a polyfill might be available).
function getURLParam (param) {
const queryString = window.location.search // query string from URL
const params = new URLSearchParams(queryString) // parse query string into object
return params.get(param) // get the value of the param name passed into function
}
Experiment with the code above. I can't be sure of the exact implementation you need, but this should suffice as a good starting point.

Sorting table doesnt working Vue

I found this pen https://codepen.io/cfjedimaster/pen/BYpJgj But it doesn't work in my implementation. I can't understand what i am doing wrong
<table>
<thead>
<tr class="header">
<th #click="sort('campaign')">Campaign</th> // click heandler
<th #click="sort('time')">Time</th>
<th #click="sort('views')">Views</th>
<th #click="sort('visitors')">Visitors</th>
<th #click="sort('ctr')">CTR</th>
<th #click="sort('cpc')">CPC</th>
<th #click="sort('cpv')">CPV</th>
<th #click="sort('cpm')">CPM</th>
<th #click="sort('status')">Status</th>
</tr>
</thead>
<tbody> //target table
<tr v-for="item in data" :key="item.id"> // loop
<td>{{item.name}}</td> //row
<td>{{item.time}}</td>
<td>{{item.views}}</td>
<td>{{item.visitors}}</td>
<td>{{item.ctr}}</td>
<td>{{item.cpc}}</td>
<td>{{item.cpv}}</td>
<td>{{item.cpm}}</td>
<td>{{item.status}}</td>
</tr>
</tbody>
</table>
data(){
return{
data: [], // data
currentSort:'campaign', // dynamic property
currentSortDir:'asc'
}
},
methods:{ // methods
getData(){
this.data = this.$store.state.graphArr // get data from store
},
sort(s) { // sort function
//if s == current sort, reverse
if(s === this.currentSort) { // if statement
this.currentSortDir = this.currentSortDir==='asc'?'desc':'asc'; }
this.currentSort = s; // setting currentSortDir
}
},
computed:{ // computed
sortedCats() { // where this function should be called
return this.data.sort((a,b) => { // ES Lint highlits "this". Unexpetced side effect
let modifier = 1;
if(this.currentSortDir === 'desc') modifier = -1;
if(a[this.currentSort] < b[this.currentSort]) return -1 * modifier;
if(a[this.currentSort] > b[this.currentSort]) return 1 * modifier;
return 0;
});
}
}
Maybe i should somewhere call sortedCats function? And the role of computed property at all in this case?
You need to loop through sortedCats array to get the desired result instead of data as sortedCats returns new value every time sort is clicked through computed property.
<tr v-for="item in sortedCats" :key="item.id">

v-model in a nested v-for of a multidimensional array

Hi I want to create a table with the days of the selected month where you can a add an employee and mark meals you want to assign to the employee.
I almost there, I can add a row to the table and mark the meals by day but when a second row is added the same meals are marked, all the meals are binded by day if i mark a meal in a row it marks for all the rows.
Here is the code and a jsfiddle
Html
<div id="app">
<span class="demonstration">Pick a month</span>
<input type="month" v-model="month">{{month}}<br><br>
<button #click="addEmployee()">Add a employee</button><br>
Mark meals for the employee<br>
<table border="1">
<thead>
<tr>
<th rowspan="3">Name</th>
<th :colspan="calendar.length*3">days of the month</th>
</tr>
<tr>
<th colspan="3" v-for="day in calendar">{{day.date}}</th>
</tr>
<tr>
<template v-for="c in calendar">
<th>b</th>
<th>l</th>
<th>d</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="(item, indexItem) in list" :key="indexItem">
<td>
<input type="text" v-model="item.name">
</td>
<template v-for="(day, indexDay) in item.days">
<td>
<input type="checkbox" v-model="item.days[indexDay].breakfast">
</td>
<td>
<input type="checkbox" v-model="item.days[indexDay].lunch">
</td>
<td>
<input type="checkbox" v-model="item.days[indexDay].dinner">
</td>
</template>
</tr>
</tbody>
</table>
</div>
Vue
new Vue({
el: "#app",
data: {
month: '',
list: [
]
},
computed: {
calendar () {
let selected = new Date(this.month)
let daysOfMonth = new Date(selected.getFullYear(), selected.getMonth() + 1, 0)
let days = [{}]
for (var i = 0; i < daysOfMonth.getDate(); i++) {
days[i] = {
date: selected.getFullYear().toString() + '-' + (selected.getMonth() + 1).toString() + '-' + (i + 1).toString(),
breakfast: false,
lunch: false,
dinner: false
}
}
return days
}
},
methods: {
addEmployee () {
let cal = []
cal = this.calendar
this.list.push(
{
name: '',
days: cal
}
)
}
}
})
https://jsfiddle.net/patogalarzar/v8h0knt7/
You are sharing the same object on every row, which means when one row is update, all the rest get updated as well.
Computed method is not the right tool here. I suggest you create a method to generate the calendar object.
methods: {
createCalander (month) {
let selected = new Date(month)
let daysOfMonth = new Date(selected.getFullYear(), selected.getMonth() + 1, 0)
let days = [{}]
for (var i = 0; i < daysOfMonth.getDate(); i++) {
days[i] = {
date: selected.getFullYear().toString() + '-' + (selected.getMonth() + 1).toString() + '-' + (i + 1).toString(),
breakfast: false,
lunch: false,
dinner: false
}
}
return days
}
}
}
Now you can create the computed property using this method, passing this.month.
On the add employees you would be using the new method to generate the list.
addEmployee () {
let cal = []
cal = this.getCalander(this.month)
this.list.push(
{
name: '',
days: cal
}
)
}
Now that you are not using the same object, the rows will not update together.
Your mistake was to use the same object on every row.
I've updated the jsfiddle
Change your addEmployee method to avoid point to same object:
addEmployee () {
let cal = []
cal = JSON.parse(JSON.stringify(this.calendar))
this.list.push(
{
name: '',
days: cal
}
)
}
More proper way to create a method call getCalendar and let cal = this.getCalendar()
this is because all employees reference the same object calendar, you can deep copy the object, or try this way

How to calculate the total in vue component ? Vue.JS 2

My vue component, you can see below :
<template>
<div>
<div class="panel-group" v-for="item in list">
...
{{ total = 0 }}
<tr v-for="product in item.products">
...
<td>
<b>Price</b><br>
<span>{{ product.quantity * product.price }}</span>
</td>
</tr>
{{ total += (product.quantity * product.price) }}
<tr>
<td colspan="3" class="text-right">
<b>Total: {{ total }} </b>
</td>
</tr>
</div>
</div>
</template>
<script>
export default {
...
computed: {
list: function() {
return this.$store.state.transaction.list
},
...
}
}
</script>
I try like above code
But, seems it still wrong
How can I solve it correctly?
I'm still newbie in vue.js 2
Since, TypeError: this.$store.state.transaction.list.reduce is not a function is an error marked in Frank's answer I presume this.$store.state.transaction.list is not an Array but an object as v-for iterates through both.
total: function() {
var list = this.$store.state.transaction.list
var sum = 0
for(var listProps in list) {
list[listProps].products.forEach(function (product) {
sum += product.pivot.quantity * product.pivot.price
})
}
return sum;
}
Use another computed property
<script>
export default {
...
computed: {
list: function() {
return this.$store.state.transaction.list
},
total: function() {
return this.$store.state.transaction.list.reduce(function(sum, item) {
sum += item.products.reduce(function(tmp, product) { tmp += product.quantity * product.price; return tmp; }, 0);
return sum;
}, 0);
}
...
}
}
</script>
Use a nested Array.reduce to get the total of your structure where the list has many items and an item has many products