Watchduck
Watchduck

Reputation: 1156

How to make a Vue.js component of some (but not all) table cells in a table row?

I have some related and neighboring table columns that I would like to group in the same component. But the Vue.js template system has this limitation that only one tag can be directly in <template>. Often this meaningless wrapper is a <div>. But what can I use in a table?

I can abuse tags like <span> for that, but the consequences are not acceptable. The cells in the same column do not have the same size, and the table borders do not collapse. Is there a way to have a wrapper tag that ideally will not show up in the HTML at all, or will at least be as neutral as a <div>?

Table row:

<template>
    <tr v-for="thing in things">
        <td>{{thing.name}}</td>
        <size-component :size="thing.size"></size-component>
        <time-component :time="thing.time"></time-component>
    </tr>
</template>

Size columns:

<template>
    <wrap>
        <td>{{size.x}}</td>
        <td>{{size.y}}</td>
        <td>{{size.z}}</td>
    </wrap>
</template>

Time columns:

<template>
    <wrap>
        <td>{{time.h}}</td>
        <td>{{time.m}}</td>
        <td>{{time.s}}</td>
    </wrap>
</template>

Edit:

To me this boils down to the problem, that there is no tag to group <td>s in a <tr> (like <tr>s can be grouped in <table> with multiple <tbody> tags). Compare Is there a tag for grouping "td" or "th" tags? Semantically <colgroup> is intended for this purpose, but that does not help.

For me using vue-fragment turned out to be the right solution:

<template>
    <fragment>
        <td>{{size.x}}</td>
        <td>{{size.y}}</td>
        <td>{{size.z}}</td>
        <td>{{volume}}</td>
    </fragment>
</template>

<script>
    import { Fragment } from 'vue-fragment';

    export default {
        computed: {
            volume() { return this.size.x * this.size.y * this.size.z },
        },
        components: { Fragment },
        props: ['size']
    }
</script>

Upvotes: 2

Views: 1008

Answers (2)

Richard Matsen
Richard Matsen

Reputation: 23533

To add computed to @Andreas solution, can reference a parent property.

Demo CodeSandbox

functional component

export default {
  functional: true,
  props: {
    size: {
      type: Object,
      default: () => ({
        x: 1,
        y: 2,
        z: 3
      })
    }
  },
  render: (createElement, context) => [
    createElement("td", context.props.size.x),
    createElement("td", context.props.size.y),
    createElement("td", context.props.size.z),
    createElement("td", context.parent.volume(context.props.size))
  ]
};

parent component

export default {
  components: {
    SizeComponent
  },
  data() {
    return {
      things: [
        {
          name: 'aName',
          size: { x: 1, y: 2, z: 3 }
        }
      ]
    };
  },
  mounted() {
    // test it is reactive
    setTimeout(()=> { this.things[0].size.x = 2 }, 3000) 
  },
  computed: {
    volume() {
      return (size) => size.x * size.y * size.z;
    }
  },
}

Upvotes: 1

Andreas
Andreas

Reputation: 1099

You are right, this is a limitation with Vue and has something to do with the diffing algorithm. You can, however, use a functional component to render multiple root elements.

<script>
export default {
  functional: true,
  props: {
    size: {
      type: Object,
      default: () => ({
        x: 1,
        y: 2,
        z: 3
      })
    }
  },
  render: (createElement, context) => [
    createElement('td', context.props.size.x),
    createElement('td', context.props.size.y),
    createElement('td', context.props.size.z)
  ]
}
</script>

Another option is to use the plugin vue-fragments

Upvotes: 4

Related Questions