Why are my table rows rendered outside the table? DOM template parsing caveats in Vuejs
Usng the special Vuejs is attribute to fix common DOM parsing caveats.
Published on: 2022-07-01
Written by Schalk Neethling
The problem described here might not be something you run into often, as it mainly applies to situations where you write your HTML template directly in your HTML document. If you use a string template or a single file component, you will not encounter this problem.
When learning or wanting to prototype an idea when using Vuejs quickly, you may opt to place all of your code in a single .html
document. Something like this:
<div id="app">
<h1>WaterBear</h1>
<table>
<caption>
Contributors
</caption>
<thead>
<tr>
<th>Username</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
Vue.createApp({
data() {
return {
usernames: ["schalkneethling"],
};
},
})
.component("github-user-row", {
template: "#github-user-row-template",
props: ["username"],
data() {
return {
user: {},
};
},
async created() {
const userAPI = "https://api.github.com/users";
try {
const response = await axios.get(`${userAPI}/${this.username}`);
this.user = response.data;
} catch (error) {
console.log(error);
}
},
})
.mount("#app");
</script>
Filling in the template
You will notice that the tbody
is currently empty. For our rows, we might want to use an x-template
, for example:
<script id="github-user-row-template" type="text/x-template">
<tr>
<td><img :src="user.avatar_url" height="100" width="100" />
<a :href="user.html_url">{{ user.name }}</a>
</td>
</tr>
</script>
You would then use the template inside the table
:
<github-user-row
v-for="username in usernames"
:username="username"
></github-user-row>
The above will loop over the usernames
array and output a row for each.
The problem
If you open this page in a browser, you will find that the rows are rendered outside the table
element. Why is this? You have just experienced a DOM parsing caveat when using Vue this way.
What is happening is that the browser sees the following when it parses the document.
<table>
<caption>
Contributors
</caption>
<thead>
<tr>
<th>Username</th>
</tr>
</thead>
<tbody>
<github-user-row
v-for="username in usernames"
:username="username"
></github-user-row>
</tbody>
</table>
Later in Vuejs’s lifecycle, it will replace the component with our HTML but right now, the browser does not recognize the component, marks it as invalid, and hoists it outside of the table, producing the following HTML:
<github-user-row
v-for="username in usernames"
:username="username"
></github-user-row>
<table>
<caption>
Contributors
</caption>
<thead>
<tr>
<th>Username</th>
</tr>
</thead>
<tbody></tbody>
</table>
When Vue reaches the stage in its lifecycle where it will replace the component with the HTML, you end up with the following:
<tr>
<td>
<img :src="user.avatar_url" height="100" width="100" />
<a :href="user.html_url">{{ user.name }}</a>
</td>
</tr>
<table>
<caption>
Contributors
</caption>
<thead>
<tr>
<th>Username</th>
</tr>
</thead>
<tbody></tbody>
</table>
The solution
The way to address this problem is to use the special is
attribute inside the table
element as follows:
<tbody>
<tr
is="vue:github-user-row"
v-for="username in usernames"
:username="username"
></tr>
</tbody>
Note the vue:
used in front of the template name. If you open the page in a browser, your table will render as expected.