What is a suspensible component in Vue 3? - vue.js

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

Related

Vue js simple component for table not working

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>

What is the best way to use Vuetify skeleton on Nuxt

I'm kinda new to Vue, Nuxt, and Vuetify and their aspects. I'm working on a Nuxt project with Vuetify and wanna use its skeleton loader but it's kinda messy. right now I use this pattern
template:
<v-skeleton-loader :loading="isLoading" type"card">
<mycomponent />
</v-skeleton-loader>
script
import skeleton from '#plugins/mixins/skeleton.js
export default {
mixins:[skeleton]
}
skeleton.js
export default{
data(){
return{
loading: null
}
},
computed:{
isLoading(){
return this.loading
}
},
created(){
this.loading = true
},
mounted(){
this.loading = false
}
}
when I first used it it was working perfectly. i had a static page and each of its components had their own skeleton and every time i loaded the page it would show their skeleton until they were loaded.
BUT.... as I started using this pattern on different pages i found out that it has many flaws!!
it only shows the skeleton when the page is refreshed!
won't show when I add components or data to the page! for example, an Axios call to get the product
it won't work when changing between routes
and so on ...
So, my question is, What's the best and most practical way to use the skeleton loader! i had a page with a v-for loop through a component and the component had its own skeleton in its template. it only show skeleton on refresh!
like this:
<div v-for="i in 10" :key="i">
<mycomp />
</div>
mycomp:
<v-skeleton-loader :loading="isLoading" type"card">
// components html codes
</v-skeleton-loader>
I would you suggest to create skeleton component. And in main component most of apps do this stuff, where the amount of skeleton is fixed or limited by pagination:
<template v-if="loading">
<skeleton v-for="i in 10" :key="i" />
</template>
<template v-else>
<div v-for="item in items" :key="item.id">
{{ item }}
</div>
</template>

Are Vue component slots not considered children?

Lets say that I have a parent component, Tabs, which takes in child Tab Components as slots.
So the setup is like this:
Tabs (Parent)
<div>
<slot name="tabChild">
</div>
Tab (Child)
<div>
Tab {{name}}
</div>
MyApp
<Tabs>
<Tab slot="tabChild" name="1" ></Tab>
<Tab slot="tabChild" name="1" ></Tab>
</Tabs>
However, in the Tabs (Parent) component, when I try to programmatically access its children, like this:
Tabs Component
mounted(){
let childTabs = this.$children //this is empty??
childTabs = this.$slots //this is correctly the child Tab Components
}
Moreover, in the Tab (Child) component, when I try to access its parent, which I thought was the Tabs component (since they are slotted within it), it is not:
Tab Component
mounted(){
let parentTab = this.$parent //this is MyApp (grandfather), NOT Tabs
}
Why are the tab child components slotted within the greater Tabs component not its children?
Well, $parent will always refer to the "direct" parent/wrapper a particular component is nested inside, so it's not quite reliable when the need to refer to the "root" parent arises.
Here are some good excerpts from the official docs:
In most cases, reaching into the parent makes your application more difficult to debug and understand, especially if you mutate data in the parent. When looking at that component later, it will be very difficult to figure out where that mutation came from. [1]
Use $parent and $children sparingly as they mostly serve as an escape-hatch. Prefer using props and events for parent-child communication. [2]
Implicit parent-child communication: Props and events should be preferred for parent-child component communication, instead of this.$parent or mutating props.
An ideal Vue application is props down, events up. Sticking to this convention makes your components much easier to understand. [3]
Unfortunately, using the $parent property didn’t scale well to more deeply nested components. That’s where dependency injection can be useful, using two new instance options: provide and inject. [4]
Here's a quick example that sort of demonstrates the above recommendations:
const Parent = Vue.extend({
template: `
<ul :style="{ columnCount: colCount }">
<slot></slot>
</ul>
`,
provide() {
return {
biologicalParent: this
}
},
props: ['value', 'colCount', 'message'],
methods: {
sayIt() {
const
num = this.$children.indexOf(this.value) + 1,
message = [
`I am child no. ${num}`,
`among ${this.$children.length} of my siblings`,
`in ${this.colCount} different lineages.`
]
.join(' ');
this.$emit('update:message', message);
}
},
watch: {
value: 'sayIt'
}
});
const Child = Vue.extend({
template: `<li v-text="name" #click="setChild"></li>`,
props: ['name'],
inject: ['biologicalParent'],
methods: {
setChild() {
this.biologicalParent.$emit('input', this);
}
}
});
new Vue({
el: '#demo',
data: () => ({
lineage: 3,
childCount: 10,
message: 'Click on a child for the associated message.',
lastChild: undefined
}),
components: {
Parent,
Child
}
});
input {
width: 4em;
}
li {
cursor: pointer;
}
.message {
background-color: beige;
border: 1px solid orange;
padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<table>
<tr>
<td>Child count:</td>
<td><input type="number" v-model="childCount" /></td>
</tr>
<tr>
<td>Lineage:</td>
<td><input type="number" v-model="lineage" /></td>
</tr>
</table>
<parent
v-model="lastChild"
:col-count="lineage"
:message.sync="message">
<child
v-for="(n, i) of Array.apply(null, {length: childCount})"
:key="i"
:name="`child #${i+1}`">
</child>
</parent>
<p class="message">{{ message }}</p>
</div>
The provide options allows us to specify the data or methods we want to provide to descendant components. In the above example, that's the Parent instance.
Notice how the Child index/number is evaluated at runtime (when clicked) instead of compile time (rendering phase).

Getting ref of component in async v-for

I have a list of items that don't get created until after an async call happens. I need to be able to get the getBoundingClientRect() of the first (or any) of the created items.
Take this code for instance:
<template>
<div v-if="loaded">
<div ref="myItems">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
<div v-else>
Loading...
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
items: []
}
},
created() {
axios.get('/url/with/some/data.json').then((response) => {
this.items = response.data;
this.loaded = true;
}, (error) => {
console.log('unable to load items');
});
},
mounted() {
// $refs is empty here
console.log(this.$refs);
// this.$refs.myItems is undefined
}
};
</script>
So, I'm trying to access the myItems ref in the mounted() method, but the this.$refs is empty {} at this point. So, therefore, I tried using a component watch, and various other methods to determine when I can read the ref value, but have been unsuccessful.
Anyone able to lead me in the right direction?
As always, thanks again!!
UPDATES
Added a this.$watch in the mounted() method and the $refs still come back as {}. I then added the updated() method to the code, then was able to access $refs there and it seemed to work. But, I don't know if this is the correct solution?
How does vuejs normally handle something like dynamically moving a div to an on-screen position based on async data? This is similar to what I'm trying to do, grab an element on screen once it has been rendered first (if it even should be rendered at all based on the async data), then access it to do something with it (move it to a position)?
Instead of doing on this.$refs.myItems during mounted, you can do it after the axios promise returns the the response.
you also update items and loaded, sou if you want to use watch, you can use those
A little late, maybe it helps someone.
The problem is, you're using v-if, which means the element with ref="myItems" doesn't exist yet. In your code this only happens when Axios resolves i.e. this.loaded.
A better approach would be to use a v-show.
<template>
<div>
<div v-show="loaded">
<div ref="myItems">
<div v-if="loaded">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div v-show="!loaded">
Loading...
</div>
</div>
</template>
The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.
https://v2.vuejs.org/v2/guide/conditional.html#v-show

Undefined props in Vue js child component. But only in its script

I have just started using Vue and experienced some unexpected behavior. On passing props from a parent to child component, I was able to access the prop in the child's template, but not the child's script. However, when I used the v-if directive in the parents template (master div), I was able to access the prop in both the child script and child template. I would be grateful for some explanation here, is there a better was of structuring this code? See below code. Thanks.
Parent Component:
<template>
<div v-if="message">
<p>
{{ message.body }}
</p>
<answers :message="message" ></answers>
</div>
</template>
<script>
import Answers from './Answers';
export default {
components: {
answers: Answers
},
data(){
return {
message:""
}
},
created() {
axios.get('/message/'+this.$route.params.id)
.then(response => this.message = response.data.message);
}
}
</script>
Child Component
<template>
<div class="">
<h1>{{ message.id }}</h1> // works in both cases
<ul>
<li v-for="answer in answers" :key="answer.id">
<span>{{ answer.body }}</span>
</li>
</ul>
</div>
</template>
<script>
export default{
props:['message'],
data(){
return {
answers:[]
}
},
created(){
axios.get('/answers/'+this.message.id) //only worls with v-if in parent template wrapper
.then(response => this.answers = response.data.answers);
}
}
</script>
this.message.id only works with v-if because sometimes message is not an object.
The call that you are making in your parent component that retrieves the message object is asynchronous. That means the call is not finished before your child component loads. So when your child component loads, message="". That is not an object with an id property. When message="" and you try to execute this.message.id you get an error because there is no id property of string.
You could continue to use v-if, which is probably best, or prevent the ajax call in your child component from executing when message is not an object while moving it to updated.