Search code examples
vue.jsdata-bindingv-model

changing a single value using v-model / full table is redrawn


I was building an editable table, which began to crawl to a halt when the number of rows started to run in the 100's. This led me to investigate what was going on.

In the example below, when changing the value in the input, the whole table is redrawn, and the ifFunction() function is trigged 4 times.

Why is this happening? Shouldn't Vue be capable of just redrawing the respective cell? Have I done something wrong with the key-binding?

<template>
    <div id="app">

        <table border="1" cellpadding="10">
            <tr v-for="(row, rowKey) in locations" :key="`row_+${rowKey}`">
                <td v-for="(column, columnKey) in row" :key="`row_+${rowKey}+column_+${columnKey}`">
                    <span v-if="ifFunction()">{{ column }}</span>
                </td>
            </tr>
        </table>

        <input v-model="locations[0][1]">

    </div>
</template>

<script>

    export default {

        data() {
            return {
                locations: [
                    ["1","John"],
                    ["2","Jake"]
                ], // TODO : locations is not generic enough.
            }
        },

        methods: {
            ifFunction() {
                console.log('ifFunction');
                return true;
            },
        }
    }
</script>

Solution

  • The data property defines reactive elements - if you change one part of it, everything that's depending on that piece of data will be recalculated.

    You can use computed properties to "cache" values, and only update those that really need updating.

    I rebuilt your component so computed properties can be used throughout: created a cRow and a cCell component ("custom row" and "custom cell") and built back the table from these components. The row and the cell components each have a computed property that "proxies" the prop to the template - thus also caching it.

    On first render you see the ifFunction() four times (this is the number of cells you have based on the data property in Vue instance), but if you change the value with the input field, you only see it once (for every update; you may have to click "Full page" to be able to update the value).

    Vue.component('cCell', {
      props: {
        celldata: {
          type: String,
          required: true
        },
        isInput: {
          type: Boolean,
          required: true
        },
        coords: {
          type: Array,
          required: true
        }
      },
      data() {
        return {
          normalCellData: ''
        }
      },
      watch: {
        normalCellData: {
          handler: function(value) {
            this.$emit('cellinput', {
              coords: this.coords,
              value
            })
          },
          immediate: false
        }
      },
      template: `<td v-if="ifFunction()"><span v-if="!isInput">{{normalCellData}}</span> <input v-else type="text" v-model="normalCellData" /></td>`,
      methods: {
        ifFunction() {
          console.log('ifFunction');
          return true;
        },
      },
      mounted() {
        this.normalCellData = this.celldata
      }
    })
    Vue.component('cRow', {
      props: {
        rowdata: {
          type: Array,
          required: true
        },
        rownum: {
          type: Number,
          required: true
        }
      },
      template: `
      <tr>
        <td
          is="c-cell"
          v-for="(item, i) in rowdata"
          :celldata="item"
          :is-input="!!(i % 2)"
          :coords="[i, rownum]"
          @cellinput="reemit"
         ></td>
      </tr>`,
      methods: {
        reemit(data) {
          this.$emit('cellinput', data)
        }
      }
    })
    new Vue({
      el: "#app",
      data: {
        locations: [
          ["1", "John"],
          ["2", "Jake"]
        ], // TODO : locations is not generic enough.
      },
      methods: {
        updateLocations({
          coords,
          value
        }) {
          // creating a copy of the locations data attribute
          const loc = JSON.parse(JSON.stringify(this.locations))
          loc[coords[1]][coords[0]] = value
          // changing the whole locations data attribute to preserve
          // reactivity
          this.locations = loc
        }
      }
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
    
    <div id="app">
      <table border="1" cellpadding="10">
        <tbody>
          <tr v-for="(row, i) in locations" is="c-row" :rowdata="row" :rownum="i" @cellinput="updateLocations"></tr>
        </tbody>
      </table>
      <!-- <input v-model="locations[0][1]">
      <input v-model="locations[1][1]">-->
      {{locations}}
    </div>