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

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.

Related

Paginated async Component doesn't trigger setup() on route change

I have a paginated component. The async setup() method is pulling data from an API to populate the page. It works fine when the route is directly loaded, but when I change the route to a different page slug (eg. clicking a router-link), the component is not reloaded and setup is not executed again to fetch the new data.
I guess I somehow want to force reloading the component?
This is my MainApp component it has the router view and fallback.
<router-view v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
loading...
</template>
</Suspense>
</router-view>
The router looks kinda like that. You see the page component takes a page_slug:
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "",
component: MainApp,
children: [
{
name: "page",
path: "page/:page_slug",
component: Page,
props: true,
},
// [...]
]
}
And this is how my Page component looks like. It uses the page_slug to load data from an API which is then used in the template:
<template>
<div> {{ pageData }} </div>
</template>
export default defineComponent({
name: "Page",
props: {
page_slug: {
type: String,
required: true,
},
},
async setup(props) {
const pageData = await store.dispatch("getPageData", {
page_slug: props.page_slug
});
return { pageData }
}
}
When I directly open the route, the fallback "loading..." is nicely shown until the data is returned and the component is rendered.
But when I do a route change to another page, then async setup() is not executed again. In that case the url in the browser updates, but the data just remains the same.
How can I solve this case? Do I have to force reload the component somehow? Or have an entirely different architecture to the data loading?
The answer is simple, when trying to create Vue 3 Single File Components (SFCs) in Composition API way as shown below:
<template>
<!-- Your HTML code-->
</template>
<script>
export default {
name: 'ComponentName',
async setup():{
// Your code
}
};
</script>
<style>
/*Your Style Code*/
</style>
<script>, will only executes once when the component is first imported. So, when the data have changed by other component, the component above will not updated or in other words not re-created.
To make your component re-created whenever it about to mount, you have to use <script setup> which will make sure the code inside will execute every time an instance of the component is created, but you need to re-write your script code with few changes in comparison when using setup() method, and also you are able to use both of scripts like this:
<script>
// normal <script>, executed in module scope (only once)
runSideEffectOnce()
// declare additional options
export default {
name: "ComponentName",
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// executed in setup() scope (for each instance)
</script>
Read this documentation carefully to have full idea.

How do I disconnect Vuetify v-intersect observer in component beforeDestroy hook?

I'm using v-intersect to modify the NavBar content based on the scrolling event. The data is then recorded to Vuex Store. Problem is when navigate to a different route, v-intersect fire the event and modify the Store which changed the NavBar content unexpectedly. v-intersect.once is not applicable to my use case, but I'd like to know how to disconnect v-intersect observer from the beforeDestroy hook?
<template>
...
<div v-intersect="intersect" />
...
</template>
<script>
import { mapFields } from 'vuex-map-fields'
export default {
computed: {
...mapFields(['isIntersecting']),
},
methods: {
intersect(entries, observer) {
this.isIntersecting = !!entries[0].intersectionRatio
},
},
}
</script>
Directive instance is supposed to be destroyed and so disconnected when a parent is destroyed, this is the only proper way for a directive to work.
To stop receiving events, v-if can be used, or events can be conditionally processed. The second option is applicable if there's race condition on destroy, so the behaviour needs to be applied immediately:
beforeDestroy() {
this.isIntersectionInactive = false;
}
...
intersect(entries, observer) {
if (this.isIntersectionInactive)
return;
this.isIntersecting = !!entries[0].intersectionRatio
},

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

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>

How to dynamically mount vue component with props

Scenario / context
I have an overview component which contains a table and an add button. The add button opens a modal component. When i fill in some text fields in the modal and click the save button, a callback (given as prop) is called so the parent component (the overview) is updated. The save button also triggers the model toggle function so the model closes.
So far works everything like expected but when i want to add a second entry, the modal is "pre-filled" with the data of the recently added item.
Its clear to me that this happens because the model component keeps mounted in the background (so its just hidden). I could solve this by "reset" the modals data when the toggle function is triggered but i think there should be a better way.
I have a similar issue when i want to fetch data in a modal. Currently i call the fetch function in the mounted hook of the modal. So in this case the fetch happens when the parent component mounts the modal. This does not make sense as it should only (and each time) fetch when the modal is opened.
I think the nicest way to solve this is to mount the modal component dynamically when i click the "add" (open modal) button but i can't find how i can achieve this. This also avoids that a lot of components are mounted in the background which are possibly not used.
Screenshot
Example code
Overview:
<template>
<div>
// mount of my modal component
<example-modal
:toggleConstant = modalToggleUuid
:submitHandler = submitHandler />
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
someList: [],
}
},
mounted() {
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Modal:
<template>
<div>
<input v-model="item.type">
<input v-model="item.name">
<input v-model="item.location">
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
item: {},
}
},
mounted() {
// in some cases i fetch something here. The data should be fetched each time the modal is opened
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Question
What is the best practive to deal with the above described scenario?
Should i mount the modal component dynamically?
Do i mount the component correctly and should i reset the content all the time?
You are on the right way and in order to achieve what you want, you can approach this issue with v-if solution like this - then mounted() hook will run every time when you toggle modal and it also will not be present in DOM when you are not using it.
<template>
<div>
// mount of my modal component
<example-modal
v-if="isShowModal"
:toggleConstant="modalToggleUuid"
:submitHandler="submitHandler"
/>
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
isShowModal: false,
modalToggleUuid: someUuid,
someList: []
};
},
mounted() {},
methods: {
showModal: function() {
this.isShowModal = true;
},
submitHandler: function(item) {
this.someList.push(item);
this.isShowModal = false;
}
}
};
</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).