Walt Stoneburner
Walt Stoneburner

Reputation: 2612

Why is this simple VueJS component template rendering in the incorrect place within the DOM, above a table?

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:

The VueJS Component appears before the Table

Examined DOM:

DOM showing Component is above the root elements

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

Answers (1)

palaѕн
palaѕн

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

Related Questions