Reputation: 2612
The examples presented below are not the actual problematic code, but as close to a simplified case as I can make it for the purposes of discussion here.
Broken Example
The following code renders a table, but puts the Component values, unexpectedly, above the table element, both visually and when I inspect it in the DOM:
<html>
<head><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script></head>
<body>
<div id="app">
<table border="1">
<tr><td>A</td><td>B</td></tr>
<test></test>
<tr><td>Y</td><td>Z</td></tr>
</table>
</div>
<script>
const Test = {
template: '<tr><td>ONE</td><td>TWO</td></tr>'
}
const app = new Vue({
el: '#app',
components: { Test },
});
</script>
</body>
</html>
Rendered output:
Examined DOM:
A Similar Example, But Works
However, VueJS doesn't seem to have a problem doing it with other nested objects. The following works just fine. Here's the same thing, but with a list:
<html>
<head><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script></head>
<body>
<div id="app">
<ul>
<li>TOP</li>
<test></test>
<li>BOTTOM</li>
</ul>
</div>
<script>
const Test = {
template: '<li>MIDDLE</li>'
}
const app = new Vue({
el: '#app',
components: { Test },
});
</script>
</body>
</html>
At a high level, these appear to be equivalent in structure -- one container containing three items. In the first case it's a table holding rows (broken), in the second it's a list holding items (works).
The Question
Can someone explain the behavior, what's going on under the hood, and ideally how to fix?
Background of the Real Problem
The actual code involves the table having a table header (thead
) section with a title row (tr
), while the component data appears inside of a table body (tbody
), and for various reasons I do not want to pollute the page with additional components, nor alter existing component code.
Useful Aside
And while unrelated, for readers in a similar boat, I discovered by accident <test/>
and <test></test>
are not the same thing. In fact, if you take the working case above and try it, you'll discover the last element goes missing. Pop on over to VueJS and read about self-closing-components, as only official "void" elements can be self-closing.
Upvotes: 1
Views: 285
Reputation: 73896
This issue is related to DOM template parsing in Vue.js. As mentioned in the docs:
Some HTML elements, such as
<ul>
,<ol>
,<table>
and<select>
have restrictions on what elements can appear inside them, and some elements such as<li>
,<tr>
, and<option>
can only appear inside certain other elements.
This will lead to issues when using components with elements that have such restrictions. For example:
<table>
<blog-post-row></blog-post-row>
</table>
The custom component
<blog-post-row>
will be hoisted out as invalid content, causing errors in the eventual rendered output.
This is the exact same issue that you are facing, as <Test>
is rendered outside the table
in your demo. Fortunately, the is
special attribute offers a workaround for this issue. You will simply need to call <Test>
inside parent component like:
<table border="1">
<tr><td>A</td><td>B</td></tr>
<tr is="test"></tr>
<tr><td>Y</td><td>Z</td></tr>
</table>
Working Demo:
const Test = {
template: '<tr><td>ONE</td><td>TWO</td></tr>'
}
const app = new Vue({
el: '#app',
components: {
Test
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
<div id="app">
<table class="table">
<tr>
<td>A</td>
<td>B</td>
</tr>
<tr is="test"></tr>
<tr>
<td>Y</td>
<td>Z</td>
</tr>
</table>
</div>
Upvotes: 3