Walt Stoneburner's Ramblings

Currating Chaos

VueJS: Tables, Lists, and Tags

While working on a VueJS project, I noted that not all of the elements were appearing.

In fact, you can see the problem by trying this code:

<html>
<head><script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script></head>
<body>

  <div id="app">
    <ul>
      <li>TOP</li>
      <test/>
      <li>BOTTOM</li>
    </ul>
  </div>

  <script>

    const Test = {
      template: '<li>MIDDLE</li>'
    }

    const app = new Vue({
      el: '#app',
      components: { Test },
    });

  </script>
</body>
</html>

You're expecting:

  • TOP
  • MIDDLE
  • BOTTOM

But all you actually get is:

  • TOP
  • MIDDLE

Which doesn't feel right, as isn't the static text supposed to be left alone?

Well, the problem is with the <test/> element, as it should really be:

<test></test>

Self-closing components are problematic, because only official "void" elements can be self-closing.

But things can still get weird. For instance, the following code does not work as expected:

<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>

What happens is that the middle row of the table appears before the table itself, both visually and in the DOM.

By comparison, this code works just fine — note it, too, has a container with three elements within it. So what gives?

<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>

The answer has to DOM template parsing.

In short, just as HTML forbids some items from having a self-closing tag, it also insists that certain elements can only exist within other elements.

Let's look at the code again:

<table border="1">
    <tr><td>A</td><td>B</td></tr>
    <test></test>
    <tr><td>Y</td><td>Z</td></tr>
</table>

In this case, <test> is not a "legal" element for a <table>, so HTML hoists this "custom element" up outside of the place it's not allowed to be, putting it before the table.

The solution is to use the expected element, and then VueJS's is="..." trickery to treat it like a known component. Turning:

<test></test>

Into:

<tr is="test"></tr>

This makes the browser's HTML parser happy, and the element is treated as if it was a VueJS component named test.

You can find more details at this StackOverflow.


UPDATE: In VueJS 3 the v-is tag now evaluates as a Javascript expression! This means you need to pass it a string, as in <tr v-is="'test'"></tr>. See workarounds.