2

I'm struggling to develop a simple component and use it inside a loop:

<template id="measurement">
    <tr class="d-flex">
    </tr>
</template>
Vue.component('measurement', {
    template: '#measurement',
    props: {
        name: String,
        data: Object,
        val: String,
    },
});

This is obviously not functional yet but already fails:

<table v-for="(m, idx) in sortedMeters">
    <measurement v-bind:data="m"></measurement>
</table>

gives ReferenceError: Can't find variable: m inside view. For a strange reason the same thing works, i.e. without error, in a paragraph:

<p v-for="(m, idx) in sortedMeters">
    <measurement v-bind:data="m"></measurement>
</p>

What causes the variable to be not found?

PS.: here's a fiddle: https://jsfiddle.net/andig2/u47gh3w1/. It shows a different error as soon as the table is included.

Update It is intended that the loop produces multiple tables. Rows per table will be created by multiple measurements

3
  • While I don't know the answer to your question (I'm also puzzled by it), checking the resulting HTML shows that you are creating two tables (each for iteration creates one table element) which would each contain one tr if this would work. Aren't you intending to loop the tr element? But even looping on the tr element causes the error... Commented Jan 11, 2020 at 19:53
  • Thats actually intended- in the end I‘ll need multiple tables for the use case. Commented Jan 11, 2020 at 20:22
  • 1
    Then the first half of my answer should answer your question. Well, not answer it, but give you a solution, because I have no idea why yours doesn't work. Commented Jan 11, 2020 at 20:26

3 Answers 3

2

TLDR: Before Vue is passed the DOM template, the browser is hoisting <measurement v-bind:name="i" v-bind:data="m"> outside the <table> (outside v-for context), leading to the errors in Vue. This is a known caveat of DOM template parsing.


The HTML spec requires the <table> contain only specific child elements:

  • <caption>
  • <colgroup>
  • <thead>
  • <tbody>
  • <tr>
  • <tfoot>
  • <script> or <template> intermixed with above

Similarly, the content model of <tr> is:

  • <td>
  • <th>
  • <script> or <template> intermixed with above

The DOM parser of compliant browsers automatically hoists disallowed elements – such as <measurement> – outside the table. This happens before the scripting stage (before Vue even gets to see it).

For instance, this markup:

<table>
  <tr v-for="(m,i) in obj">
    <measurement v-bind:name="i" v-bind:data="m"></measurement>
  </tr>
</table>

...becomes this after DOM parsing (before any scripting):

<measurement v-bind:name="i" v-bind:data="m"></measurement> <!-- hoisted outside v-for -->
<table>
  <tr v-for="(m,i) in obj">
  </tr>
</table>

Notice how i and m are then outside the context of the v-for loop, which results in Vue runtime errors about i and m not defined (unless by chance your component coincidentally declared them already). m was intended to be bound to <measurement>'s data prop, but since that failed, data is simply its initial value (also undefined), causing the rendering of {{data.value}} to fail with Error in render: "TypeError: Cannot read property 'value' of undefined".

To demonstrate hoisting without these runtime errors and without Vue, run the code snippet below:

<table style="border: solid green">
  <tr>
    <div>1. hoisted outside</div>
    <td>3. inside table</td>
    2. also hoisted outside
  </tr>
</table>

...then inspect the result in your browser's DevTools, which should look like this:

<div>1. hoisted outside</div>
2. also hoisted outside
<table style="border: solid green">
  <tr>
    <td>3. inside table</td>
  </tr>
</table>

Solution 1: Use <tr is="measurement">

If you prefer DOM templates, you could use the is attribute on a <tr> to specify measurement as the type (as suggested by the Vue docs and by another answer). This first requires the <measurement> template use <td> or <th> as a container element inside <tr> to be valid HTML:

<template id="measurement">
  <tr>
    <td>{{name}} -> {{data.value}}</td>
  </tr>
</template>

<div id="app">
  <table v-for="(m,i) in sortedMeters">
    <tr is="measurement" v-bind:name="i" v-bind:data="m" v-bind:key="i"></tr>
  </table>
</div>

Vue.component('measurement', {
  template: '#measurement',
  props: {
    name: String,
    data: Object
  }
})

new Vue({
  el: '#app',
  data: {
    sortedMeters: {
      apple: {value: 100},
      banana: {value: 200}
    },
  }
})
<script src="https://unpkg.com/[email protected]"></script>
<template id="measurement">
  <tr>
    <td>{{name}} -> {{data.value}}</td>
  </tr>
</template>

<div id="app">
  <table v-for="(m,i) in sortedMeters">
    <tr is="measurement" v-bind:name="i" v-bind:data="m" v-bind:key="i"></tr>
  </table>
</div>

Solution 2: Wrap <table> in component

If you prefer DOM templates, you could use a wrapper component for <table>, which would be able to contain <measurement> without the hoisting caveat.

Vue.component('my-table', {
  template: `<table><slot/></table>`
})
<div id="app">
  <my-table v-for="(m, i) in sortedMeters">
    <measurement v-bind:name="i" v-bind:data="m"></measurement>
  </my-table>
</div>

Vue.component('measurement', {
  template: '#measurement',
  props: {
    name: String,
    data: Object
  }
})

Vue.component('my-table', {
  template: `<table><slot/></table>`
})

new Vue({
  el: '#app',
  data: {
    sortedMeters: {
      apple: {value: 100},
      banana: {value: 200}
    },
  }
})
<script src="https://unpkg.com/[email protected]"></script>
<template id="measurement">
  <tr>
    <td>{{name}} -> {{data.value}}</td>
  </tr>
</template>

<div id="app">
  <my-table v-for="(m, i) in sortedMeters">
    <measurement v-bind:name="i" v-bind:data="m"></measurement>
  </my-table>
</div>

Solution 3: Move <table> markup into template string

You could move the entire <table> into a component's template string, where the DOM template caveats could be avoided. Similarly, you could move the <table> into a single file component, but I assume you have a significant need for DOM templates instead.

Vue.component('my-table', {
  template: `<div>
    <table v-for="(m, idx) in sortedMeters">
      <measurement v-bind:data="m"></measurement>
    </table>
  </div>`,
  props: {
    sortedMeters: Object
  }
})
<div id="app">
  <my-table v-bind:sorted-meters="sortedMeters"></my-table>
</div>

Vue.component('measurement', {
  template: '#measurement',
  props: {
    name: String,
    data: Object
  }
})

Vue.component('my-table', {
  template: `<div>
    <table v-for="(m,i) in sortedMeters">
      <measurement v-bind:name="i" v-bind:data="m" v-bind:key="i"></measurement>
    </table>
  </div>`,
  props: {
    sortedMeters: Object
  }
})

new Vue({
  el: '#app',
  data: {
    sortedMeters: {
      apple: {value: 100},
      banana: {value: 200}
    },
  }
})
<script src="https://unpkg.com/[email protected]"></script>
<template id="measurement">
  <tr>
    <td>{{name}} -> {{data.value}}</td>
  </tr>
</template>

<div id="app">
  <my-table :sorted-meters="sortedMeters"></my-table>
</div>

Sign up to request clarification or add additional context in comments.

1 Comment

Wow. Excellent answer!
1

If you replace

<table v-for="(m, idx) in sortedMeters">
  <measurement v-bind:data="m"></measurement>
</table>

with

<template v-for="(m, idx) in sortedMeters">
  <table>
    <measurement v-bind:data="m"></measurement>
  </table>
</template>

You'll end up with working code.

But you'll most likely want to use

<table>
  <template v-for="(m, idx) in sortedMeters">
    <measurement v-bind:data="m"></measurement>
  </template>
</table>

or

<table>
  <measurement v-for="(m, idx) in sortedMeters" v-bind:data="m"></measurement>
</table>

2 Comments

I can confirm that wrapping the table with template works. However- i can use the loop on the table just fine as long as I don't add the child component and instead do the tr's html manually. It remains a mystery why that fails.
Wrapping the table in <template> works only because its contents are inert (unparsed by the DOM parser until invoked by JavaScript).
0

It's because you've missing <td> inside <tr>. Without it your component produces invalid html markup and extracts "slot" data outside <tr> causing error.

Your template should looks like:

<template id="measurement">
  <tr>
    <td>{{name}} -> {{data.value}}</td>
  </tr>
</template>

You also need to move v-for to measurement:

<table border="1">
    <tr is="measurement" v-for="(m,index) in obj" :key="index" v-bind:name="index" v-bind:data="m"></measurement>
</table>

You can use is attribute to set component name.

Working fiddle: https://jsfiddle.net/cyaj0ukh/

3 Comments

Are you sure it's an invalid HTML-thing? I've changed his fiddle to have proper HTML code but even that fails. Here's an example: jsfiddle.net/hpc9Lmjr
Reached the same conclusing. I need multiple tables, but even with proper html it fails.
@andig This answer is essentially correct about the question's HTML being invalid (+1), although the explanation is somewhat incomplete IMO.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.