2
votes

I can't get how to properly use computed properties when displaying data organized as columns with table. The full example of my code is available at jsfiddle, here is the short version and description. I want to render this data as a table:

var vueData = {
  objects: [
    {
      name: "objectName",
      objectData: [
        {prop1: "val1", prop2: "val2"}
      ]
    }
  ]
}

In this vueData each element of objectData array should be displayed as a column (that column represents data of one month or day). The props in objectData's elements should be displayed not as is, but as computed values. And that displayed values should reflect changes of vueData.

So I came to this vue template:

<table>
<tr>
  <th>Object Name</th>
  <th>Data Piece 1</th>
  <th>Data Piece 2</th>
</tr>
<template v-for="obj in objects">
<tr> 
  <th rowspan="2">{{ obj.name }}</th>
  <td v-for="dataPiece in obj.objectData">{{ compute(dataPiece.prop1) }}</td>
</tr>
<tr><td v-for="dataPiece in obj.objectData">{{ compute(dataPiece.prop2) }}</td></tr>
</template>
</table>

All works fine except that I have used methods, not vue's computed properties. The problem with methods is that their results do not cache and after a single prop change ALL cells' values are recomputed.

I could use a vue component for each cell with computed property instead of tds , but it looks like overkill, as table can be big.

Is there any another solution from the vue's point of view? (I mean without changing the data structire or appearance of the table, etc).

3
This is one of very few shortcomings in Vue: no way to create dynamic computeds. Best idea I have is to implement caching in your compute function.Roy J
try the second answer from this post, it should help: stackoverflow.com/questions/40322404/…V. Sambor

3 Answers

1
votes

I had wanted to do this with a directive, but had no good way of passing the function and the argument, until I remembered I can use vnode.context to get context, where I can lookup the function by name rather than passing it as an actual function.

So here's a directive that updates its element's textContent with the result of a function call.

var vueData = {
  objects: [{
      name: 'Object 1',
      objectData: [{
          prop1: '1-1-1',
          prop2: '1-1-2'
        },
        {
          prop1: '1-2-1',
          prop2: '1-2-2'
        },
        {
          prop1: '1-3-1',
          prop2: '1-3-2'
        }
      ]
    },
    {
      name: 'Object 2',
      objectData: [{
          prop1: '2-1-1',
          prop2: '2-1-2'
        },
        {
          prop1: '2-2-1',
          prop2: '2-2-2'
        },
        {
          prop1: '2-3-1',
          prop2: '2-3-2'
        }
      ]
    },
    {
      name: 'Object 3',
      objectData: [{
          prop1: '3-1-1',
          prop2: '3-1-2'
        },
        {
          prop1: '3-2-1',
          prop2: '3-2-2'
        },
        {
          prop1: '3-3-1',
          prop2: '3-3-2'
        }
      ]
    },
  ]
};

var vue = new Vue({
  el: document.getElementById("vue"),
  data: vueData,
  methods: {
    compute: function(prop) {
      console.log('computing ' + prop);
      return 'computed(' + prop + ')';
    }
  },
  directives: {
    cache(el, binding, vnode) {
      if (binding.value !== binding.oldValue) {
        el.textContent = vnode.context[binding.arg](binding.value);
      }
    }
  },
  computed: {
    firstProp: function() {
      return this.objects[0].objectData[0].prop1;
    }
  }
});

setTimeout(function() {
  vueData.objects[0].objectData[0].prop1 = 'changed on timeout';
}, 3000);
th,
td {
  border: 1px solid black;
}
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<div id="vue">
  <table>
    <tr>
      <th>Object Name</th>
      <th>Data Piece 1</th>
      <th>Data Piece 2</th>
      <th>Data Piece 3</th>
    </tr>
    <template v-for="obj in objects">
      <tr>
        <th rowspan="2">{{ obj.name }}</th>
        <td v-for="dataPiece in obj.objectData"
          v-cache:compute="dataPiece.prop1"
          >
        </td>
      </tr>
      <tr>
        <td v-for="dataPiece in obj.objectData"
          v-cache:compute="dataPiece.prop2">
        </td>
      </tr>
    </template>
  </table>
  <span>Computed prop1 value = {{ firstProp }}</span>
</div>
1
votes

I came across this post by LinusBorg Generating computed properties on the fly where he shows a function for mapping properties to computed's.

I adapted the function for your objects variable, as the original was more focused on flat form data (and also a little intimidating).

function mapObjectToComputed({objects}) {
  console.log(objects)
  let res = {};
  objects.forEach((obj,i) => {
    obj.objectData.forEach((dat,j) => {
      ['prop1', 'prop2'].forEach(prop => {
        const propModel = `objects_${i}_${j}_${prop}`;
        const computedProp = {
          get() {
            console.log(`Getting ${propModel}`)
            const val = this.objects[i].objectData[j][prop];
            return val;
          }
        }
        res[propModel] = computedProp;
      })
    })
  })
  return res;
}

Here is the inner part of the template. I've changed prop1 to the new syntax and left prop2 as you had it.

<template v-for="(obj, i) in objects">
  <tr> 
    <th rowspan="2">{{ obj.name }}</th>
    <td v-for="(dataPiece, j) in obj.objectData">
      {{ fetch(i,j,'prop1') }}
    </td>
  </tr>
  <tr><td v-for="dataPiece in obj.objectData">
    {{ compute(dataPiece.prop2) }}
  </td></tr>
</template>

The component is

var vue = new Vue({
    el: document.getElementById("vue"),
  data: vueData,
  methods: {
    fetch: function(i,j,prop) {
      const propModel = `objects_${i}_${j}_${prop}`
      return this[propModel];
    },
    compute: function(prop) {
        console.log('computing ' + prop);
        return 'computed(' + prop + ')';
    }
  },
  computed: {
    firstProp: function() {
        return this.objects[0].objectData[0].prop1;
    },
    ...mapObjectToComputed(vueData)
  }
});

The console after timeout is

Getting objects_0_0_prop1  
computing 1-1-2  
computing 1-2-2  
computing 1-3-2  
computing 2-1-2  
computing 2-2-2  
computing 2-3-2  
computing 3-1-2  
computing 3-2-2  
computing 3-3-2  

so only prop2 has recalculated across the board.

Here is the Fiddle

1
votes

As you mention, you can use a component. That is the cleanest solution I've found. The component is quite simple, tuned to this example. It might be worth using if a computation is particularly expensive.

var vueData = {
  objects: [{
      name: 'Object 1',
      objectData: [{
          prop1: '1-1-1',
          prop2: '1-1-2'
        },
        {
          prop1: '1-2-1',
          prop2: '1-2-2'
        },
        {
          prop1: '1-3-1',
          prop2: '1-3-2'
        }
      ]
    },
    {
      name: 'Object 2',
      objectData: [{
          prop1: '2-1-1',
          prop2: '2-1-2'
        },
        {
          prop1: '2-2-1',
          prop2: '2-2-2'
        },
        {
          prop1: '2-3-1',
          prop2: '2-3-2'
        }
      ]
    },
    {
      name: 'Object 3',
      objectData: [{
          prop1: '3-1-1',
          prop2: '3-1-2'
        },
        {
          prop1: '3-2-1',
          prop2: '3-2-2'
        },
        {
          prop1: '3-3-1',
          prop2: '3-3-2'
        }
      ]
    },
  ]
};

var vue = new Vue({
  el: document.getElementById("vue"),
  data: vueData,
  methods: {
    compute: function(prop) {
      console.log('computing ' + prop);
      return 'computed(' + prop + ')';
    }
  },
  components: {
    cacheResult: {
      props: {
        fn: Function,
        arg: String
      },
      template: '<td>{{fn(arg)}}</td>'
    }
  },
  computed: {
    firstProp: function() {
      return this.objects[0].objectData[0].prop1;
    }
  }
});

setTimeout(function() {
  vueData.objects[0].objectData[0].prop1 = 'changed on timeout';
}, 3000);
th,
td {
  border: 1px solid black;
}
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<div id="vue">
  <table>
    <tr>
      <th>Object Name</th>
      <th>Data Piece 1</th>
      <th>Data Piece 2</th>
      <th>Data Piece 3</th>
    </tr>
    <template v-for="obj in objects">
      <tr>
        <th rowspan="2">{{ obj.name }}</th>
        <td v-for="dataPiece in obj.objectData"
          is="cacheResult"
          :fn="compute"
          :arg="dataPiece.prop1"
          >
        </td>
      </tr>
      <tr>
        <td v-for="dataPiece in obj.objectData"
          is="cacheResult"
          :fn="compute"
          :arg="dataPiece.prop2">
        </td>
      </tr>
    </template>
  </table>
  <span>Computed prop1 value = {{ firstProp }}</span>
</div>