2
votes

My application is a cut-down spreadsheet built in Vue with Vuex. The key components are TableCollection, Table and Row. The TableCollection has an array with multiple Table objects. Each Table has an array with multiple Row objects.

The Row component has a property named calculatedField. This simply combines two fields in the row to produce a third field. My options are to implement calculatedField as a computed property, local to the Row component, or as a getter in the Vuex store.

The Table component requires a subTotal value, which is calculated by adding the calculatedField for all rows in the table. As you can see, the subTotal calculation is dependent on the calculatedField calculation.

If I implement calculatedField as a local computed property of Row, it is cached. The problem is however that I can't seem to access the calculated field from a Table parent. I tried the following in Table:

computed : {
    subTotal : function () {
        let total = 0;
        this.table.rows.forEach(function (row) {
           total += row.calculatedField;
        });
        return total;
    }
}

The result was NaN.

One solution would be to duplicate the logic from calculatedField in the computed property of Table, but that's not DRY.

The other alternative would be to implement both subTotal and calculatedField as getters in the store, however this would mean passing arguments to the gettter (tableId, rowId, or both), and so the results would not be cached. This seems really inefficient.

The final possibility is that I implement my calculatedField logic in a global helper or mixin. This would avoid code duplication and getter inefficiency, but doesn't feel quite right - the code relates specifically to Table and Row, and would ideally be kept there.

Are there other solutions I have overlooked? What is the ideal 'Vue-way'?

1

1 Answers

2
votes

If performance is an issue and caching is important right now, you might want to implement caching on the Table component.

In the Row component, emit the new value so the parent component can cache it.

  computed: {
    calculatedField() {
      const result = this.data.field + this.data.other;
      this.$emit('change', this.data.id, result);
      return result;
    }
  },

In the Table component, handle the event and cache the new values.

  data() {
    return { cache: {} };
  },
  computed: {
    subTotal() {
      return Object.values(this.cache).reduce((total, value) => total + value, 0);
    }
  },
  methods: {
    onChange(rowId, val) {
      // important for reactivity
      this.$set(this.cache, rowId, val);
    }
  },

When a Row's data is updated, it triggers a change event with the new calculated value and the parent Table keeps track of the calculated values, using this cache to get the subtotal.

You can see in the following example that the computed properties are hit once, then on row changes (click the Rand button), only the relevant computed properties are refreshed.

const MyRow = {
  props: {
    data: {
      type: Object
    }
  },
  computed: {
    calculatedField() {
      console.log("row computed for", this.data.id);
      const result = this.data.field + this.data.other;
      this.$emit('change', this.data.id, result);
      return result;
    }
  },
  methods: {
    onClick() {
      this.data.other = Math.floor(Math.random() * 10);
    }
  },
  template: `
    <tr>
        <td>{{ data.field }}</td>
        <td>{{ data.other }}</td>
        <td>{{ calculatedField }}</td>
        <td><button type="button" @click="onClick">Rand</button></td>
    </tr>
  `
};

const MyTable = {
  props: {
    rows: {
      type: Array
    }
  },
  components: {
    MyRow
  },
  data() {
    return {
      cache: {}
    }
  },
  computed: {
    subTotal() {
      console.log("Table subTotal");
      return Object.values(this.cache).reduce((total, value) => total + value, 0);
    }
  },
  methods: {
    onChange(rowId, val) {
      console.log("onChange", rowId, val);
      this.$set(this.cache, rowId, val);
    }
  },
  template: `
    <div>
        <table border="1">
            <tr><th>field</th><th>other</th><th>calculated</th><th></th></tr>
            <my-row v-for="row in rows" @change="onChange" :key="row.id" :data="row"></my-row>
        </table>
        Subtotal: {{ subTotal }}
    </div>
  `
};

var app = new Vue({
  el: '#app',
  components: {
    MyTable
  },
  data: {
    rows: [{
        id: 1,
        field: 1,
        other: 1
      },
      {
        id: 2,
        field: 2,
        other: 2
      },
      {
        id: 3,
        field: 3,
        other: 3
      },
    ]
  },
  template: `<my-table :rows="rows"></my-table>`
});
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>