What is the best way to use Vuetify skeleton on Nuxt - vue.js

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>

Related

Vue: Reuse loading spinner template and logic

I've multiple (20+) pages where I need the following code:
<template>
<template v-if="isLoading">
<Spinner full size="medium" />
</template>
<template v-else>
<p>Page data</p>
</template>
</template>
<script>
export default {
computed: {
isLoading() {
return this.$store.getters['loader/isLoading'];
},
},
};
</script>
I don't want to rewrite this part everytime I need it so is there a way to create something like a higher order component with access to the computed method and a way to add the hoc to the script tag of the different vue files? Or another way to archive this?
I could recommend extending the spinner component where you want to show the spinner. I've created a boilerplate setup that show a very simple implementation of this approach here.
The main idea is to expose a default slot for you spinner component, and wrap the page component in that slot.
<template>
<div>
<Spinner v-if="isLoading" full size="medium" />
<!-- Slot for component data -->
<slot v-else></slot>
</div>
</template>
<script>
export default {
computed: {
isLoading() {
return this.$store.getters['loader/isLoading'];
},
},
};
</script>
Then in any component that you want to show the spinner:
<template>
<spinner>
<!-- Pass here the component content to be shown after the spinner is hidden -->
</spinner>
</template>
<script>
import Spinner from "./spinner";
export default {
name: "Page1",
extends: Spinner,
components: { Spinner }
};
</script>

vue instance with child vue instance possible or alternative approach?

I would like to develop a vuejs multitouch app for a 4K display. It’s about 3-4 cards that are on a background and actually show the same content. For each of the cards a different entry page is visible.
Is it possible to pack several other instances (with the same content) of vuejs in divs within a Vue instance?
Somehow I would like to integrate an instance with store and router multiple times, but I can’t figure it out.
It would be helpful if someone can help me here, maybe provide a link or an approach.
I am looking for an approach how I can display the same content 3 times at the same time, at best with routes and nested routes. Each User can navigate separately, everyone has their own history via GUI.
when I try to use 2 instance inside the main vue instance 3 different routers, it’s always renders the content of main route.
I found this example where to instances are side by side, works great: https://jsfiddle.net/m91e7s2v/
but not inside a parent instance? why?
inside app.vue
<div id="app">
<VueToolMultitouch class="schatten" :startX="100" :startY="100" :startColor='"#00FF00"' id="id1" :idName="'id1'" :startZ="2">
<div id="subapp1">
<router-link to="/">/home</router-link>
<router-link to="/foo">/foo</router-link>
<p>Route path: {{ $route.path }}</p>
<router-view></router-view>
</div>
<h2>Passing Text 1</h2>
</VueToolMultitouch>
<VueToolMultitouch class="schatten" :startX="200" :startY="600" :startColor='"#FF0000"' id="id2" :idName="'id2'" :startZ="3">
<div id="subapp2">
<router-link to="/">/home</router-link>
<router-link to="/foo">/foo</router-link>
<p>Route path: {{ $route.path }}</p>
<router-view></router-view>
</div>
<h2>Passing Text 2</h2>
</VueToolMultitouch>
</div>
inside main.js
import router1 from "./router/router";
import router1 from "./router/router-1";
import router2 from "./router/router-2";
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
new Vue({
router: router1,
}).$mount("#subapp1");
new Vue({
router: router2,
}).$mount("#subapp2");
An alternative would be if everything is implemented with a single vue instance, but each of the cards gets its own "router".
maybe someone has an idea what that might look like.
The problem is that every child gets bound to the parent vue app and its prototype, this overrides the router of the children. I think that you'll need either to use iframes for the children or make the parent app handle with state the children views.
Edit:
I just learned about v-pre, this directive prevents Vue from "compiling" an HTML node and it's children.
You can basically have as many Vue instances even if they're nested as long as you put v-pre on the tag you use to mount the child Vue app.
Here's a working fiddle https://jsfiddle.net/dja36s7x/18/
I found an alternative way in the VueJS forum.
<div id="app">
<div class="row">
<my-child1></my-child1>
<my-child2></my-child2>
</div>
<div class="row">
<my-child3></my-child3>
<my-child4></my-child4>
</div>
</div>
const routes = [
{
path: '/page1',
component: { template: '<p>Page 1</p>' }
}, {
path: '/page2',
component: { template: '<p>Page 2</p>' }
}, {
path: '/page3',
component: { template: '<p>Page 3</p>' }
}
]
const MyChild = {
template: `
<div>
<router-link to="/page1">Page 1</router-link>
<router-link to="/page2">Page 2</router-link>
<router-link to="/page3">Page 3</router-link>
<button #click="$router.back()">Back</button>
<div>{{ $route.path }}</div>
<router-view />
</div>
`
}
function getChild() {
return {
extends: MyChild,
router: new VueRouter({
mode: 'abstract',
routes
})
}
}
new Vue({
components: {
MyChild1: getChild(),
MyChild2: getChild(),
MyChild3: getChild(),
MyChild4: getChild()
}
}).$mount('#app')
JSFiddle Example
Here, the components are expanded with their own router.
I currently no longer need the route via nested instances. but i will test the v-pre on everyone.
It seems this might be achieved using a hierarchy of components. If you're sure you need different Vue app instances, then it's worth going with Vue 3 as it's abandoned the idea of a shared global config, allowing you to create many Vue instances with createApp. All with different configurations.
You could do something like this (JS Fiddle here):
Vue.createApp({
name: 'App',
template: `
<h1>Primary App</h1>
<div id="subAppOne"></div>
<div id="subAppTwo"></div>
<div id="subAppThree"></div>
`
}).mount('#app');
Vue.createApp({
name: 'AppOne',
template: `<h2>App One</h2>`,
}).mount('#subAppOne');
Vue.createApp({
name: 'AppTwo',
template: `<h2>App Two</h2>`,
}).mount('#subAppTwo');
Vue.createApp({
name: 'App Three',
template: `<h2>App Three</h2>`,
}).mount('#subAppThree');
You can specify different routers with .use() on each app instance, just before calling mount().
const routerOne = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [/* … */],
});
Vue.createApp({/* … */}).use(routerOne).mount('#appOne');

Is there a way, to "emit" a built in event, by hard coding it in Vue instance?

I would like to link two components with each other in my Vue project.
I use two-way binding for that, so I have a parent, and two child components.
The concept:
We see a carousel in the left side of the screen, and we see an accordion in the right side. I built the carousel and the accordions with v-for from a database file.
When I click in some of the accordion it drops down, and I need a reaction from a carousel component, to slide exactly there, where I clicked in the accordion.
Like:
carousel: banana, apple, house
accordion: banana, apple house
So when im clicking in the apple accordion button, I need the slider to go to the where are the apple is displayed, and reverse.
As I said, I already bind the two components to each other, so when I'm clicking one of the accordion buttons like #click="onShowStart(index)", I get that index in the another child too, and it's changing dynamically vica-versa by sliding or clicking. So the indexes are already linked and its dynamic.
My problem is I don't know how to trigger an event, like #sliding-start from vue instance in the watch field. So I watch the "actualPosition" prop in my component, and when its changed (from 3 to 1 for example), I would like to start a sliding event to the new value of the actualPosition.
So i need something like:
this.$emit('sliding-start', actualPosition);
I've been sitting at this problem for days, but I think my whole thinking is wrong. But before i believe this, im asking you first.
Here is my code for the Parent component:
<div class="row">
<carousel :actualPosition="actualPosition" class="col bg-dark" #sendTheCarouselPosition="updateAccordion($event)"></carousel>
<accordion :actualPosition="actualPosition" class="col bg-dark" #sendTheAccordionlPosition="updateCarousel($event)"></accordion>
</div>
<script>
export default {
data() {
return {
actualPosition: null,
}
},
methods:{
updateAccordion: function (updatedAccordion){
this.actualPosition = updatedAccordion;
},
updateCarousel: function(updatedSlider){
this.actualPosition = updatedSlider
}
},
}
</script>
My Accordion component:
<template>
<div role="tablist">
<b-card no-body class="mb-1" v-for="(item, index) in dataForProject">
<b-card-header header-tag="header" class="p-1" role="tab">
<b-button block href="#" v-b-toggle="'accordion-' + index" variant="info" #click="onShowStart(index)" >{{ item.title }}</b-button>
</b-card-header>
<b-collapse :id="'accordion-' + index" visible accordion="my-accordion" role="tabpanel">
<b-card-body>
<div>
<h1>data from Carousel sibling: {{ actualPosition }}</h1>
</div>
<b-card-text>{{ item.content }}</b-card-text>
</b-card-body>
</b-collapse>
</b-card>
</div>
</template>
<script>
import myDataBase from '../data2'
export default {
props:['actualPosition'],
watch:{
actualPosition: function () {
},
},
data() {
return {
dataForProject: myDataBase,
}
},
methods:{
onShowStart: function (accordionIndex) {
this.$emit('sendTheAccordionlPosition', accordionIndex);
},
},
}
</script>
And my Carousel component:
<template>
<div>
<p class="mt-4 text-white">
data from Accordion sibling: {{ actualPosition }}
</p>
<b-carousel
id="carousel-1"
:interval="0"
controls
indicators
background="#ababab"
img-width="1024"
img-height="480"
style="text-shadow: 1px 1px 2px #333;"
ref="slider"
#sliding-start="onSlideStart"
#sliding-end="onSlideEnd"
>
<b-carousel-slide v-for="(item, index) in dataForProject" :id="index" >
<img
slot="img"
class="d-block img-fluid w-100"
width="1024"
height="480"
:src="item.image_url"
alt="image slot"
>
</b-carousel-slide>
</b-carousel>
</div>
</template>
<script>
import myDataBase from '../data2'
export default {
props:['actualPosition'],
watch: {
actualPosition: function () {
},
},
data() {
return {
//slide: 0,
dataForProject: myDataBase,
}
},
methods: {
onSlideStart(slide) {
this.$emit('sendTheCarouselPosition', slide);
},
onSlideEnd(slide) {
},
}
}
</script>
I can get this done by two ways.
1 - Global EventBus
I will create an eventBus and register events on it from any file and listen it anywhere -
import { EventBus } from '#/eventBus'
// simply import it to component which need listen the event
//Register Event where you have your methods - like In your COMP_B_TWO
EventBus.$on('changeValue', () => { this.doSomething() })
// Emit event from another component
EventBus.$emit('changeValue')// Like directly from your COMP_A_TWO
To know how to create a eventBus follow this - Global Event Bus Vue
2 - Use state management - Vuex Follow this link - Vuex
Basically, it will have centralized store for all the components in an application. Whenever you wish to update state you will update to store. And all the other component using that state will react accordingly
Okay, so my whole concept was wrong, and i was wrong about this.
I should have used the v-model for this whole thing. And nothing more.
I added the v-model to the target tags, and my problem is solved.

Modal component in VueJS

I am building a web app. I have few components that are modals (that show data about customers, lessons, ...).
I search a way to show one of the components easily.
And if possible doing lazy loading.
What's the best way to perform this?
Check out conditional rendering, specifically v-if. This would only load the modal if the button is clicked for example.
https://v2.vuejs.org/v2/guide/conditional.html#v-if
Single page component:
<template>
<div>
<div
v-if="showModal"
class="modal">
Stuff
</div>
<button #click="toggleModal">
Toggle Modal
</button>
</div>
</template>
<script>
export default {
data () {
return {
showModal: false
}
},
methods: {
toggleModal() {
this.showModal = !this.showModal
}
},
}
</script>

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