1

I'm wondering how to calculate a total on a vue.js list. My situation is a bit complex, so please allow me to use an example. Let's say that I'm rendering a table of invoice items. Here's my code:

<table>
<template v-for="(invoice_item, index) in invoice_items" v-if="invoice_item.category === 'widgets'">
    <tr>
        <td>@{{ invoice_item.name }}</td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.rate"></td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.quantity"></td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.activation_fee"></td>
        <td class="subtotal">@{{ computeSubTotal(invoice_item) }}</td>
    </tr>
</template>
</table>

For each row, I've computed a subtotal and displayed it in the last column. Here's that code in my vue.js javascript:

computeSubTotal: function(invoice_item) {
    return(this.formatPrice((parseFloat(invoice_item.rate) * parseFloat(invoice_item.quantity) + parseFloat(invoice_item.activation_fee))));
},

This works great. However, now I want to display the total of all of the subtotals. In other words:

enter image description here

How would I pull this off?

Thanks!

Bert
  • 80,741
  • 17
  • 199
  • 164
clone45
  • 8,952
  • 6
  • 35
  • 43
  • Possible duplicate of [Vue.js How to calculate totals?](https://stackoverflow.com/questions/39116903/vue-js-how-to-calculate-totals) – Richard Matsen Feb 05 '18 at 19:06

1 Answers1

2

Use computed properties to do your calculations.

console.clear()

new Vue({
  el: "#app",
  data: {
    invoice_items: [
      {
        name: "Community / Support",
        rate: 5.20,
        quantity: 1,
        activation_fee: 3.00,
        category: "widgets"
      },
      {
        name: "Infrastructure",
        rate: 269.00,
        quantity: 3,
        activation_fee: 1.00,
        category: "widgets"
      },
      {
        name: "Infrastructure",
        rate: 269.00,
        quantity: 3,
        activation_fee: 1.00,
        category: "stuff"
      },
    ]
  },
  computed: {
    // probably need a better name for this, but its just an example
    itemsWithSubTotal() {
      return this.invoice_items.map(item => ({
          item,
          subtotal: this.computeSubTotal(item)
      }))
    },
    // calculate the total of all the subtotalItems grouped by category
    totalByCategory() {
      // group the items by category
      let grouped = this.itemsWithSubTotal
        .reduce((acc, val) => {
          // This line does everything the lines below do, but in a very
          // terse, possibly confusing way.
          //(acc[val.item.category] = acc[val.item.category] || []).push(val)
          
          //if there is not already an array set up for the current
          //category, add one
          if (!acc[val.item.category]) 
            acc[val.item.category] = []
          // push the current value into the collection of values
          // for this category
          acc[val.item.category].push(val)
          // return the accumulator (object)
          return acc
        }, {})
        
      // create an object that has the total for each category
      return Object.keys(grouped).reduce((acc, val) => {
        acc[val] = grouped[val].reduce((total, item) => total += item.subtotal, 0)
        return acc
      }, {})
    }
  },
  methods: {
    computeSubTotal: function(invoice_item) {
      //formatPrice is removed here because its not defined in the question
      return ((parseFloat(invoice_item.rate) * parseFloat(invoice_item.quantity) + parseFloat(invoice_item.activation_fee)));
    },
  }
})
input {
  width: 5em
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
  <table>
    <template v-for="(invoice_item, index) in itemsWithSubTotal" v-if="invoice_item.item.category === 'widgets'">
    <tr>
        <td>{{ invoice_item.name }}</td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.item.rate"></td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.item.quantity"></td>
        <td><input type="number" class="inline-edit" v-model="invoice_item.item.activation_fee"></td>
        <td class="subtotal">{{ invoice_item.subtotal }}</td>
    </tr>
</template>
  </table>
  Total: {{totalByCategory["widgets"]}}
</div>

The itemsWithSubTotal may look a little strange.

itemsWithSubTotal() {
  return this.invoice_items.map(item => ({
      item,
      subtotal: this.computeSubTotal(item)
  }))
},

Basically this returns a new object with one property, item pointing to the original item and a subtotal property. I did this this way so that v-model will work in the template and automatically update the computed properties.

Bert
  • 80,741
  • 17
  • 199
  • 164
  • Great answer! Still trying to understand it all. What does acc[val.item.category] = acc[val.item.category] do? – clone45 Jan 25 '18 at 17:40
  • @clone45 `(acc[val.item.category] = acc[val.item.category] || []).push(val)` this line is slightly tricky and I could probably write it clearer. What I'm doing in that reducer is creating an object that looks like this: `{widgets: [...], stuff: [...]}`. In other words, grouping the objects by category. `acc[val.item.category] = acc[val.item.category] || []` sets the current category equal to itself or an empty array (if it doesn't yet exist). That expression will evaluate to whatever `acc[val.item.category]` ends up as, which means we can then push the latest value into the grouping. – Bert Jan 25 '18 at 18:17
  • Pretty amazing code. I had to make some minor adjustments. My computeSubTotal() function used a helper function called formatPrice which added a dollar sign and commas to the subtotals. This caused total += item.subtotal to do string concatenation instead of addition. Once that was fixed, the code worked beautifully. – clone45 Jan 31 '18 at 18:06