Vue js simple component for table not working - vue.js

I am beginner to vue js. I am trying learn step by step things from official vue documentation. I tried to understand component functionality and created following code:
<div id="app">
<table class="table">
<tr>
<td><strong>Name</strong></td>
<td><strong>Email Address</strong></td>
</tr>
<contact-item v-for="item in contacts" v-bind:contact="item" v-bind:key="item.id"></contact-item>
</table>
</div>
and here is the javascript code for displaying row data from component template.
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script>
Vue.component('contact-item', {
props: ['contact'],
template: '<tr><td>{{ contact.name }}</td><td>{{ contact.email }}</td></tr>'
})
var app = new Vue({
el: '#app',
data: {
contacts: [
{id: 1, name:'John Doe', email:'John#Doe.com'},
{id: 2, name:'Peter Drunket', email:'Peter#Drunket.com'},
{id: 3, name:'Mike Benjamin', email:'Mike#Benjamin.com'},
]
}
});
</script>
The problem is data is displaying but not in table. It is displaying right after "app" div.
Output screenshot attached.

There're certain caveats with parsing DOM templates if those mix Vue components and native elements.
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 will be hoisted out as invalid
content, causing errors in the eventual rendered output.
In your case, it caused your table to be rendered 'above' the header. Actually, browser has created two tables in this case: one for <tr>s replacing the hoisted component, another for 'native' table that was there in the template from the beginning.
Fortunately, the is special attribute offers a workaround. You need to specify the name of component that you're going to use to replace the specific native element. It's not quite convenient to specify the name of that element twice (first in HTML, then in component itself), but, like it's been said, it's a workaround.
<table>
<tr is="blog-post-row"></tr>
</table>
Here how it might look in your case:
Vue.component('contact-item', {
props: ['contact'],
template: '<tr><td>{{ contact.name }}</td><td>{{ contact.email }}</td></tr>'
})
var app = new Vue({
el: '#app',
data: {
contacts: [{
id: 1,
name: 'John Doe',
email: 'John#Doe.com'
},
{
id: 2,
name: 'Peter Drunket',
email: 'Peter#Drunket.com'
},
{
id: 3,
name: 'Mike Benjamin',
email: 'Mike#Benjamin.com'
},
]
}
});
<div id="app">
<table class="table">
<tr>
<td><strong>Name</strong></td>
<td><strong>Email Address</strong></td>
</tr>
<tr is="contact-item" v-for="item in contacts" v-bind:contact="item" v-bind:key="item.id"></tr>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>

Related

What is a suspensible component in Vue 3?

I'm reading this article: https://v3.vuejs.org/guide/component-dynamic-async.html#using-with-suspense
It's referring a concept called "suspensible" component.
I have researched, but I can't find any information about what is a so called "suspensible" component.
Can anyone explain what it is? Thanks!
"Suspensible" means replaceable by fallback content while parent <Suspense> resolves async child components found in its <template #default>.
The concept is borrowed from React's Suspense API.
In more detail, <Suspense> is a built-in Vue 3 component which renders a <template #fallback> instead of the <template #default>, until all async child components in default template are resolved.
In order to be suspensible, a component's rendering needs to depend on a promise:
be loaded using () => import('some/path')
or use an async/await (or any other form of Promise syntax) in its setup function
A suspensible component is suspensed when included in a <Suspense>'s default template, while its parent <Suspense> has not resolved all its suspensible components, even if the suspensed component itself has already resolved.
Obviously, <Suspense> components themselves are suspensible and suspensing can be nested.
Here's a more detailed explanation on <Suspense> in Vue 3.
Among other usages, <Suspence> provides an elegant and intuitive way to resolve the common problem of having to wrap child components and templates in v-if guarding against non-existent properties on data which has not yet been loaded.
A typical Vue 2 example:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('render-items', {
props: ['items'],
template: `<table>
<tr>
<th>Id</th>
<th>User Id</th>
<th>Title</th>
</tr>
<tr v-for="(item, key) in items" :key="key">
<td v-text="item.id"></td>
<td v-text="item.userId"></td>
<td v-text="item.title"></td>
</tr>
</table>`
});
new Vue({
el: '#app',
data: () => ({
items: []
}),
computed: {
hasData() {
return this.items.length;
}
},
async created() {
const items = await fetch('https://jsonplaceholder.typicode.com/posts')
.then(r => r.json());
this.items = items;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.js"></script>
<div id="app">
<render-items :items="items" v-if="hasData"></render-items>
<template v-else>loading...</template>
</div>
Same example (more or less) in Vue 3, using <Suspense> and async setup:
const RenderItems = Vue.defineComponent({
async setup() {
const items = await fetch('https://jsonplaceholder.typicode.com/posts')
.then(r => r.json());
return Vue.reactive({ items });
},
template: `<table>
<tr>
<th>Id</th>
<th>User Id</th>
<th>Title</th>
</tr>
<tr v-for="(item, key) in items" :key="key">
<td v-text="item.id"></td>
<td v-text="item.userId"></td>
<td v-text="item.title"></td>
</tr>
</table>`
});
const App = { components: { RenderItems }};
Vue.createApp(App).mount('#app');
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<Suspense>
<template #default>
<render-items></render-items>
</template>
<template #fallback>
loading...
</template>
</Suspense>
</div>
One major advantage is in the Vue 3 example we can contain the data fetcher (and the data) in the child component. This is not possible in Vue 2, because:
the sub-component is only created after data has loaded
the parent needs to know when the condition changed (so it needs access to the actual condition) in order to switch between rendering fallback content or the child component.
The simplest way to do it in Vue 2 is actually to load the data in parent and pass the result to the child component, via props. If you have a lot of sub-components, this pattern can get messy.
In Vue 3, the responsibility for loading the data and checking the condition can rest entirely with the child-component. Parent doesn't need access to the actual condition.
Just like <template>, <Suspense> does not create a DOM element.
In the above Vue 3 example, <RenderItems /> is suspensible.
A suspensible component would be one that is capable of using the new item in Vue 3. A suspense item it something that loads and may take a longer time to load, like an API call. Generally you would be using async/await inside of items that are inside of the suspense item.
A lot of good info here: https://v3.vuejs.org/guide/component-dynamic-async.html#async-components
You would use a suspense item to say while items inside of the suspense item are being awaited show something else (like a skeleton loader).

Is it possible to DRY html in vuejs components without creating more components?

Lets say you have:
<template>
<div>
<!-- html for buttons -->
<!-- your form -->
<!-- html for buttons -->
</div>
</template>
<!-- rest of your component -->
Is it possible to DRY up the html for the html for buttons without using a separate component? It seems a lot of work to keep adding components just to save repeating 3-4 lines of html?
I don't know any Vue api that allows to do that properly, however there is a way.
There is v-html which would serve you for DRY html, but it would get rendered as plain HTML, so you cannot use Vue events from there -which I guess your buttons do-.
For instance:
//template
<div id="app">
<div v-html="dryContent"></div>
<p>{{content}}</p>
<div v-html="dryContent"></div>
<div v-html="computedString"></div>
</div>
//script
new Vue({
el: '#app',
data: {
content: 'some sentence',
dryContent: `<div>
<p>Hello world!</p>
</div>`
},
computed: {
computedString() {
return `<p>${this.content}</p>`
}
}
});
Will render the HTML properly. But you cannot setup vue event listeners in the rendered HTML.
You can still, however, setup native listeners:
dryContent: `<div>
<p onclick="console.log('foo')">Hello world!</p>
</div>`
And it will work.
And, well, there is this really obscure pattern which I totally don't suggest but that actually will fit your needs:
new Vue({
el: '#app',
data: {
content: 'some sentence',
dryContent: `<div>
<p onclick="modifyContent()">Hello world!</p>
</div>`
},
computed: {
computedString() {
return `<p>${this.content}</p>`
}
},
created() {
window.modifyContent = function() {
this.content = 'modified!!';
}.bind(this);
}
});
You export the component method to a window property, so you can call it from native code.
Don't know your use case, but I'm pretty sure I would just duplicate the HTML code or setup a new component instead of doing this.

Vue JS loop with different components

I am using Vue JS to make a list that has one generic list item component. If there exists a none generic component that meets the correct type a custom component will be used.
<email-queue-item v-for="item in queue"
:key="item.id"
:item="item"
v-if="type == 'EmailMessage'"></email-queue-item>
<queue-item v-for="item in queue"
:key="item.id"
:item="item"
v-else></queue-item>
The code above better illustrates what I am trying to do. The problem I seem to have is due loops first creating two list and then checks the conditional. Is there a way in due to pick the right component vase on the type and then loop through the list?
The data Used to display these components is like this:
{
name: Email,
type: EmailMessage,
data:[
{...},
{...},
...
]
}
Dynamic components make this pretty easy in the template:
<component
:is="type == 'EmailMessage' ? 'email-queue-item' : 'queue-item'"
v-for="item in queue"
:key="item.id"
:item="item"
/>
If I undersstand correctly, you'd like v-for with dynamic component.
so check Vue Official Guide: dynamic component, then the demo will be like below which uses v-bind:is:
Vue.config.productionTip = false
Vue.component('email-queue-item', {
template: `<div><h3 :style="{'background-color':color}">Email: {{color}}</h3></div>`,
props: ['color']
})
Vue.component('message-queue-item', {
template: `<div><h1 :style="{'background-color':color}">Message: {{color}}</h1></div>`,
props: ['color']
})
new Vue({
el: '#app',
data() {
return {
items: [
{'component':'email-queue-item', 'color':'red'},
{'component':'message-queue-item', 'color':'blue'},
{'component':'email-queue-item', 'color':'green'}
]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div v-for="(item, index) in items" :key="index" :color="item.color" :is="item.component"></div>
</div>

Vue - How do you render sub-rows in a table when you are only allowed one root component?

I'm trying to figure out the best way to render sub rows in a table.
I'm developing what's essentially a table+accordion. That is, the table adds more rows when you click on it to show more details for that entry.
My table is located in the Researcher Groups component, which has a sub-component: "app-researcher-group-entry (ResearcherGroupEntry).
I'm using Vue-Material, but the problem would be the same if I was using a plain .
Anyway, my structure is kind of like:
// ResearcherGroups.vue
<template>
<md-table>
<md-table-row>
<md-table-head></md-table-head>
<md-table-head>Researcher</md-table-head>
<md-table-head>Type</md-table-head>
<md-table-head></md-table-head>
<md-table-head></md-table-head>
</md-table-row>
<app-researcher-group-entry v-for="(group, index) in researcherGroups" :key="group.label" :groupData="group" :indexValue="index"></app-researcher-group-entry>
</md-table>
</template>
And in ResearcherGroupEntry.vue:
<template>
<md-table-row>
<md-table-cell>
<md-button class="md-icon-button md-primary" #click="toggleExpansion">
<md-icon v-if="expanded">keyboard_arrow_down</md-icon>
<md-icon v-else>keyboard_arrow_right</md-icon>
</md-button>
<md-button #click="toggleExpansion" class="index-icon md-icon-button md-raised md-primary">{{indexValue + 1}}</md-button>
</md-table-cell>
<md-table-cell>{{groupData.label}}</md-table-cell>
<md-table-cell>Group</md-table-cell>
<md-table-cell>
<md-button #click="openTab" class="md-primary">
<md-icon>add_box</md-icon>
Add Client / Client Type
</md-button>
</md-table-cell>
<md-table-cell>
<md-button class="md-primary">
<md-icon>settings</md-icon>
Settings
</md-button>
</md-table-cell>
<app-add-client-to-group-tab :indexValue="indexValue" :groupData="groupData" :closeTab="closeTab" :addClientToGroupTab="addClientToGroupTab"></app-add-client-to-group-tab>
</md-table-row>
</template>
Here's where the problem comes in. I want to be able to expand the clients like this from our mockup:
This would be trivially easy if I could just add another into the ResearcherGroupEntry component. Unfortunately, wrapping with a div or span tag won't work here (it messes up the table).
If I wasnt using Vue Material, I might have considered using JSX for this component, because then I could simply use .map in the parent to create an array of components based on the two variables. I might still be able to do that with v-for directives, but I'm not sure. What I may have to do is create, from props, a crazy pseudo array merging in parents and children, but that's UUUUUGLY code.
The key here is that the row component doesn't render its own sub-rows. You can use <template> tags to provide entity-less v-for and v-if wrappers. Do your v-for in a template, so that you can output the row tr and also some optional sub-row trs. Simple example:
new Vue({
el: '#app',
data: {
rows: [{
id: 1,
name: 'one',
subrows: ['a', 'b', 'c']
},
{
id: 2,
name: 'two',
subrows: ['d', 'e', 'f']
}
],
expanded: {}
},
methods: {
expand(id) {
this.$set(this.expanded, id, true);
}
},
components: {
aRow: {
template: '<tr><td>{{name}}</td><td><button #click="expand">Expand</button></td></tr>',
props: ['name', 'id'],
methods: {
expand() {
this.$emit('expand', this.id);
}
}
},
subRow: {
template: '<tr><td colspan=2>{{value}}</td></tr>',
props: ['value']
}
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<table id="app" border=1>
<tbody>
<template v-for="r in rows">
<a-row :id="r.id" :name="r.name" #expand="expand"></a-row>
<template v-if="r.id in expanded">
<sub-row v-for="s in r.subrows" :value="s"></sub-row>
</template>
</template>
</tbody>
</table>

Nested Vue instances

Is there anyway to have nested Vue instances? I know about Vue components and I use them extensively in my applications but in this use case I have different applications (I mean different projects) that are loading inside each other in a page.
For example when I do something like the following:
<div id="parent">
{{msg}}
<div id="child">
{{msg}}
</div>
</div>
...
new Vue({
el: '#parent',
data: { msg: 'sth' },
})
new Vue({
el: '#child',
data: { msg: 'sth else' },
})
But both msg's uses msg data of parent Vue instance. Here I just want to show an example but in my use case these instances are not next to each other and just somehow relate to each other through Django framework (which is not important to notice here).
Update
It's not a duplicate question. Person who asked that question doesn't need nested Vue instances and just needs components. But I explicitly said that I know about components but need nested Vue instances.
Issue
According to this issue, they are not going to implement such behavior.
If you really need to have nested instances, use VueJS v-pre directive. You can add v-pre to the child app. More details about it here.
<div id="parent">
{{msg}}
<div id="child" v-pre>
{{msg}}
</div>
</div>
...
new Vue({
el: '#parent',
data: { msg: 'sth' },
})
new Vue({
el: '#child',
data: { msg: 'sth else' },
})
{{ msg }} for child instance will be "sth else". Note that the nested instance (#child element) is not compiled by vue parent instance because of the v-pre directive.