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>
tableelement) which would each contain onetrif this would work. Aren't you intending to loop thetrelement? But even looping on thetrelement causes the error...