Walt Stoneburner
Walt Stoneburner

Reputation: 2612

Why is this simple VueJS 2.x component being hoisted out of the nested HTML table elements?

I'm using the development version of VueJS 2.6.12.

In my HTML, I have a VueJS template that looks like this (I've stripped it down for the sake of small examples):

<div>
  <table class="table">
    <thead>
      <tr><th>AAA</th><th>BBB</th></tr>
    </thead>

    <tbody id="main">
       <my-item v-for="item in dataset" :d="item" :key="item.key"/>       <!-- (this part works) -->
    </tbody>
  </table>
</div>

In my JavaScript, I defined a global Vue Component like so (and this works, showing my data):

Vue.component('my-item', {
  props: ["d"],
  template: `<tr><td>{{this.d.AAA}}</td><td>{{this.d.BBB}}</td></tr>`
});

When I load the page, I see my data correctly rendered, but the table headers are at the bottom. The tr elements (containing my data) are present, though outside the table.

Viewing the DOM in the browser inspector shows VueJS built this structure, hoisting the elements outside the tbody and even the table itself:

<div>
  <tr><td>aaa</td><td>bbb</td></tr>               <!-- Why are these two rows outside the table? -->
  <tr><td>yyy</td><td>zzz</td></tr>
  <table class="table">
    <thead>
      <tr><th>AAA</th><th>BBB</th></tr>
    </thead>
    <tbody id="main"></tbody>
  </table>
</div>

Why is this happening and how can I fix it? I'm expecting my <tr>...</tr> elements to be injected within the tbody that's under the thead header rows.


Even slightly stranger, if I manually add in some rows above and below:

    <tbody id="main">
       <tr><td>111</td><td>222</td></tr>
       <my-item v-for="item in dataset" :d="item" :key="item.key"/>
       <tr><td>888</td><td>999</td></tr>
    </tbody>

Those do get preserved:

<div>
  <tr><td>aaa</td><td>bbb</td></tr>         <!-- ...but these still got hoisted out if the table -->
  <tr><td>yyy</td><td>zzz</td></tr>
  <table class="table">
    <thead>
      <tr><th>AAA</th><th>BBB</th></tr>
    </thead>
    <tbody id="main">
      <tr><td>111</td><td>222</td></tr>
      <tr><td>888</td><td>999</td></tr>
    </tbody>
  </table>
</div>

Upvotes: 3

Views: 278

Answers (1)

Walt Stoneburner
Walt Stoneburner

Reputation: 2612

Ugh, got bit by the same problem, in a slightly different form.

In a nutshell, there are DOM Parsing Caveats, and elements like table have restrictions on what elements can appear inside them. (FYI: ul, ol, select have this issue as well).

The solution is to use VueJS's is attribute:

<div>
  <table class="table">
    <thead>
      <tr><th>AAA</th><th>BBB</th></tr>
    </thead>

    <tbody id="main">
       <!-- Specify the element, but then use  is="component-name" -->
       <tr is="my-item" v-for="item in dataset" :d="item" :key="item.key"/>
    </tbody>
  </table>
</div>

This will cause the template parsing to be happy, as table (and tbody) will be getting a "tr" element, but its content will be rendered by the named component.


IMPORTANT: Breaking change from VueJS 2 (shown above) to VueJS 3.

In VueJS 2 the is="..." has become `v-is="..." in VueJS 3.

Also, VueJS 2 took a string, where VueJS now takes a Javascript expression, meaning you must pass it a string, e.g. <tr v-is="'my-item'"> with quotes.

See https://v3.vuejs.org/guide/component-basics.html#dom-template-parsing-caveats and https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html#v-is-for-in-dom-template-parsing-workarounds for details.

Upvotes: 3

Related Questions