Show spinner (preloader/loading indicator) whenever page changes and hide when all assets are loaded in Vue Gridsome - vue.js

I am using Gridsome (Vue static site generator with Vue Router) and I've created a preloader in index.html, its a simple div that covers everything. In index.html I also added this JS code to hide the preloader when everything loads
window.onload = function() {
document.getElementById('preloader').style.display = 'none';
};
This works only for the initial load, but when changing pages I am having trouble showing it and hiding it again.
I've tried to add this to my Layout component's beforeDestroy() hook to show the preloader again
beforeDestroy() {
this.preloader.style.display = 'block';
}
which shows it successfully when the route is changed, but then if I add the hiding logic in mounted() like this
mounted() {
this.preloader.style.display = 'none';
}
the preloader is never showed in the first place.
I was unable to find any resources about this kind of loading indicators, all I can find are one's for async calls like axios or fetch. I've created preloaders before in static HTML files, but never in SPAs. Can someone please push me in the right direction? Even googling keywords will help

you can use vuex with this case.
first, add your state src/main.js
import DefaultLayout from "~/layouts/Default.vue";
import Vuex from "vuex";
export default function(Vue, { appOptions }) {
Vue.component("Layout", DefaultLayout);
Vue.use(Vuex);
appOptions.store = new Vuex.Store({
state: {
loading: false,
},
mutations: {
on(state) {
state.loading = true;
},
off(state) {
state.loading = false;
},
},
});
}
second, add spinner to ./src/layouts/Default.vue
<template>
<div class="layout">
// add your spinner here or another
<div v-if="$store.state.loading">loading</div>
<slot />
</div>
</template>
finally, add commit code pages, templete, or components. like below.
<script>
export default {
created() {
// commit("on") first
this.$store.commit("on");
// commit("off") last, after fetch data or more.
this.$store.commit("off");
},
};
</script>

Related

Vue3 - router push to another page only renders top part of the page - not scrollable

I am experiencing an issue that I actually already have had for quite some time.
My setup is Vue3 and Vuetify 3.
Whenever I change the page after some kind of calculation:
router.push({ name: 'AnotherPage', params: { id: index, variable: x} });
The page is redirected to 'AnotherPage' but the page is not scrollable, so only the part of the page that fits on the page is rendered.
After doing an F5 refresh, the complete page is rendered and scrollable.
I only noticed this behavior when I was looking into redirecting to a certain section on a page, using anchors, and found that it was not working.
scrollToElement() {
if (this.$route.hash) {
const targetId = ref(this.$route.hash.replace('#', ''));
const eal = document.getElementById(targetId.value);
if (eal != null) {
eal.scrollIntoView();
}
}
},
This works when I load the page from scratch, but it doesn't when I use the aforementioned router.push method. There is no error though, so the component is able to find the element linked to the requested anchor tag.
Another thing is that when I perform a hardcoded router.push from a button click, it works!
In vuejs there is a parent-child dependency between elements. It is not clear from the question if views are used, but I assume so, because that's a best practice.
router.push is for history manipulation: so if you do that from the parent main-view it will work, and inform automatically a child-view to re-render (because of the change itself)
but if you do the calculation deeper in the page, in a child, and want to update the entire page, you have to use the emit component event (the child informs the parents about the change of values and for a re-render)
See this example for a demo: https://learnvue.co/tutorials/vue-emit-guide
To put it together: having a update function in the mainView, which wants to be called from a RouterLink in a childView. This is started by updateParent - so there a emit event prev-next-click is defined.
<script setup>
import { RouterLink } from 'vue-router'
</script>
<script>
export default {
emits: ['prev-next-click'],
methods: {
updateParent: function(c, d) {
// on button click update from fixture
// emit a call from subView (child) to run the update in main App (parent)
this.$emit("prev-next-click", c, d);
},
to: function (c=this.category, d=this.today) {
return { name: 'quote', params: { category: c, day: d }}
},
},
created() {
this.updateParent(this.$route.params.category, this.$route.params.day);
}
};
</script>
<template>
<RouterLink class="button prev" :to="to(category,prev)" #click="updateParent(category,prev)">click</RouterLink>
</template>
And the mainView glue it together with RouterView (not the name of the view!)
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<script>
export default {
methods: {
update: function (c, d) {
console.log("update c", c, "d", d);
},
},
}
</script>
<template>
<RouterView #prev-next-click="update"/>
</template>
Update to the comment:
A problem I had to solve with the code above, was that the childView wasn't rendered properly when initialized. That's why childView always call updateParent once directly during creation by the created hook.
See vuejs lifecycle for all other hooks - maybe the updated fits in your case.

how to re-mount a child component in vuejs

im searching how to re-mount a child component. Not just re-render it, but totally re-mount (so mounted() function of my child component will be called.
I know there already is a similar question (How to re-mount a component in VueJS?), but the author was searching how to re-rendre (and the solution match this case), when i follow these instructions, my mounted() function is not called.
here is what i have:
I have a page with 2 component:
an upload component
a list component
here is what i want:
I use my upload component to upload a .zip file to my api. Then, the API parse the .zip, and store its information in database.
My list il a table of every .zip, displaying basic informations (name, description...), taking data from the store, and if empty, using axios to fetch from API (everything into the mounted() hook).
the only solution i have to update the list when i upload a new .zip is to fetch the list from my API.
here is the problem: i cant find a way to update my list. Knowing data are fetched when my list component is mounted, the solution would be to re-mount my component, But I can't find how, so I'm asking you guys.
here is my page code:
<template>
<div>
<div class="col-md-4 text-center mr-auto">
<card>
<Upload #upload="upload" ></Upload>
</card>
</div>
<div>
<List :ProjID="selectedProject.id" :key="childKey"/>
</div>
</div>
</template>
<script>
import upload from "./upload.vue";
import API from "./services/API";
import list from "./list.vue";
export default {
name: "ProjectCard",
components: {
Upload,
List,
},
props: {
selectedProject: null
},
data() {
return {
childKey: 0,
};
},
methods: {
upload(file) {
API.postJob(file, this.selectedProject.id)// <-- function calling axios, everything is working here. It also delete stored file in my store, so the list component will fetch from API to get new data
.then(response => {
this.childKey += 1;
console.log("success!")
})
.catch(response => {
console.log("not succes :c")
});
}
}
};
</script>
here is my list mounted() hook:
async mounted() {
const fetch = await API.getAll(); <-- this function fetch from my store, and if nothing comes out, fetch from API.
if (fetch == 401) {
console.log("not logged in");
}
if (fetch == 500) {
console.log("error");
return();
}
this.fetchedFiles = fetch;
this.dataReady = true;
}
I have to re-call the mounted() hook in order to update my list (or any better solution)

How does Vue lifecycle works? and use it for skeleton and loading

this question my look silly but i got stuck in this!
i'm using Vuetify skeleton and created a data isLoading like the codes bellow. on page refresh every thing is fine but on route change (go back and forward in my pages) it's not working.
in my codes, when page is refreshed btn becomes disabled and mycomponent show skeleton using the same isLoading in its file. but when i go forward and back to it, my btn is loaded, not disabled, mycomponent load after a time without showing skeleton!
what's the problem! i guest it's about using the lifecycle!
<template>
<div>
<v-btn :disabled="isLoading">Button</v-btn>
<mycomponent />
</div>
</template>
<script>
import mycomponent from '~/components/mycomponent'
export default {
components:{
'mycomponent': mycomponent
},
data(){
return{
isLoading: true
}
},
created(){
this.isLoading = true
},
mounted(){
this.isLoading = false
}
}
</script>
mycomponent:
<template>
<v-skeleton-loader
:loading="isLoading"
type="button"
width="100%"
>
<v-btn>Button</v-btn>
</v-skeleton-loader>
</template>
<script>
export default {
data(){
return{
isLoading: true
}
},
created(){
this.isLoading = true
},
mounted(){
this.isLoading = false
}
}
</script>
So The Problem: It only works when i land on page for first time or refresh browser. by going forward and back to this page neither disable button nor skeleton on component works.
Update
I'm on NuxtJs v2.13
The created hook is called when the vue instance is created and the mounted hook is called when the vue instance has been mounted in the DOM. These hooks are called when a component is routed to for the first time or when the page is refreshed. That explains why it only works when you land on the page for the first time or refresh the browser.
When a component has been mounted, pressing the back button on the browser won't call the created and mounted hook.
To solve your problem, you can watch the $route object, by doing
App.vue
watch: {
'$route' () {
// this will be called any time the route changes
this.isLoading = true // you can think of a way to make isLoading false
}
},
For more on lifecycle hooks, check out this article.

Lazy loading a specific component in Vue.js

I just make it quick:
In normal loading of a component (for example "Picker" component from emoji-mart-vue package) this syntax should be used:
import {Picker} from "./emoji-mart-vue";
Vue.component("picker", Picker);
And it works just fine.
But when I try to lazy load this component I'm not sure exactly what code to write. Note that the following syntax which is written in the documentation doesn't work in this case as expected:
let Picker = ()=>import("./emoji-mart-vue");
The problem, I'm assuming, is that you're using
let Picker = ()=>import("./emoji-mart-vue");
Vue.component("picker", Picker);
to be clear, you're defining the component directly before the promise is resolved, so the component is assigned a promise, rather than a resolved component.
The solution is not clear and depends on "what are you trying to accomplish"
One possible solution:
import("./emoji-mart-vue")
.then(Picker=> {
Vue.component("picker", Picker);
// other vue stuff
});
This will (block) wait until the component is loaded before loading rest of the application. IMHO, this defeats the purpose of code-spliting, since the application overall load time is likely worse.
Another option
is to load it on the component that needs it.
so you could put this into the .vue sfc that uses it:
export default {
components: {
Picker: () => import("./emoji-mart-vue")
}
};
But this would make it so that all components that use it need to have this added, however, this may have benefits in code-splitting, since it will load only when needed the 1st time, so if user lands on a route that doesn't require it, the load time will be faster.
A witty way to solve it
can be done by using a placeholder component while the other one loads
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: SomeLoadingComponent
});
Vue.component("picker", Picker);
or if you don't want to load another component (SomeLoadingComponent), you can pass a template like this
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: {template:`<h1>LOADING</h1>`},
});
Vue.component("picker", Picker);
In PluginPicker.vue you do this:
<template>
<picker />
</template>
<script>
import { Picker } from "./emoji-mart-vue";
export default {
components: { Picker }
}
</script>
And in comp where you like to lazy load do this:
The component will not be loaded until it is required in the DOM, which is as soon as the v-if value changes to true.
<template>
<div>
<plugin-picker v-if="compLoaded" />
</div>
</template>
<script>
const PluginPicker = () => import('./PluginPicker.vue')
export default {
data() = { return { compLoaded: false }}
components: { PluginPicker }
}
// Another syntax
export default {
components: {
PluginPicker: () => import('./PluginPicker.vue')
}
}
</script>

Nuxt.js global events emitted from page inside iframe are not available to parent page

I'm trying to create a pattern library app that displays components inside iframe elements, alongside their HTML. Whenever the contents of an iframe changes, I want the page containing the iframe to respond by re-fetching the iframe's HTML and printing it to the page. Unfortunately, the page has no way of knowing when components inside its iframe change. Here's a simplified example of how things are setup:
I have an "accordion" component that emits a global event on update:
components/Accordion.vue
<template>
<div class="accordion"></div>
</template>
<script>
export default {
updated() {
console.log("accordion-updated event emitted");
this.$root.$emit("accordion-updated");
}
}
</script>
I then pull that component into a page:
pages/components/accordion.vue
<template>
<accordion/>
</template>
<script>
import Accordion from "~/components/Accordion.vue";
export default {
components: { Accordion }
}
</script>
I then display that page inside an iframe on another page:
pages/documentation/accordion.vue
<template>
<div>
<p>Here's a live demo of the Accordion component:</p>
<iframe src="/components/accordion"></iframe>
</div>
</template>
<script>
export default {
created() {
this.$root.$on("accordion-updated", () => {
console.log("accordion-updated callback executed");
});
},
beforeDestroy() {
this.$root.$off("accordion-updated");
}
}
</script>
When I edit the "accordion" component, the "event emitted" log appears in my browser's console, so it seems like the accordion-updated event is being emitted. Unfortunately, I never see the "callback executed" console log from the event handler in the documentation/accordion page. I've tried using both this.$root.$emit/this.$root.$on and this.$nuxt.$emit/this.$nuxt.$on and neither seem to be working.
Is it possible that each page contains a separate Vue instance, so the iframe page's this.$root object is not the same as the documentation/accordion page's this.$root object? If so, then how can I solve this problem?
It sounds like I was correct and there are indeed two separate Vue instances in my iframe page and its parent page: https://forum.vuejs.org/t/eventbus-from-iframe-to-parent/31299
So I ended up attaching a MutationObserver to the iframe, like this:
<template>
<iframe ref="iframe" :src="src" #load="onIframeLoaded"></iframe>
</template>
<script>
export default {
data() {
return { iframeObserver: null }
},
props: {
src: { type: String, required: true }
},
methods: {
onIframeLoaded() {
this.getIframeContent();
this.iframeObserver = new MutationObserver(() => {
window.setTimeout(() => {
this.getIframeContent();
}, 100);
});
this.iframeObserver.observe(this.$refs.iframe.contentDocument, {
attributes: true, childList: true, subtree: true
});
},
getIframeContent() {
const iframe = this.$refs.iframe;
const html = iframe.contentDocument.querySelector("#__layout").innerHTML;
// Print HTML to page
}
},
beforeDestroy() {
if (this.iframeObserver) {
this.iframeObserver.disconnect();
}
}
}
</script>
Attaching the observer directly to the contentDocument means that my event handler will fire when elements in the document's <head> change, in addition to the <body>. This allows me to react when Vue injects new CSS or JavaScript blocks into the <head> (via hot module replacement).