VueJs Quasar - scroll position stuck using Quasar loading component after page change - vue.js

I have a problem using Quasar with VueJs.
When I use Quasar's Loading.show() method, the scroll is stuck when I go to the new page, and the top of the new page is not shown.
In the router/index.js file i have set
const router = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0}),
...
});
For example:
In MainLayout.vue (which is the parent component of the index and category page), I have set a watch to show the loading screen when the isLoading flag is true in the state.
watch: {
isLoading: {
deep: true,
handler(isLoading) {
if(isLoading == true) {
Loading.show();
} else {
Loading.hide();
}
}
}
}
This is working fine, but when I have clicked a link to a category page after I have scrolled a little bit, The category page will also retain the previous scroll position.
Is working without the scroll issue with commented out Loading.show();
Video from the issue:
https://drive.google.com/file/d/1d-WaxHazNdZ8bp3_pcN14hcayucUYYUb/view

Try to do this, but I'm not sure if this is the right solution:
import { scroll } from 'quasar'
const { getScrollTarget, setVerticalScrollPosition } = scroll
watch: {
isLoading: {
deep: true,
handler(isLoading) {
if(isLoading == true) {
Loading.show();
} else {
Loading.hide();
const el = document.querySelector('.q-page-container')
const target = getScrollTarget(el)
const offset = el.offsetTop
const duration = 0
setTimeout(() => setVerticalScrollPosition(target, offset, duration), 400);
}
}
}
}
Documentation https://quasar.dev/quasar-utils/scrolling-utils#scrolling-to-an-element

Related

Nuxt js scroll to section via hash is covered by the fixed navbar

I'm currently using Nuxt js + Vuetify and can't find a way or any documentation for adding a scroll padding in the router.scrollBehavior.js file in the .nuxt folder.
I have tried to import and use the Vuetify goTo() (which automatically accounts for the fixed navbar) in the router.scrollBehavior.js but it gives a "Cannot use import statement outside a module" uncaught syntax error.
The link
<NuxtLink
:to="{ path: '/about', hash: '#five-principles-intro' }"
class="text-no-wrap"
>Our Principles</NuxtLink>
default router.scrollBehvaior.js
import { getMatchedComponents, setScrollRestoration } from './utils'
if (process.client) {
if ('scrollRestoration' in window.history) {
setScrollRestoration('manual')
// reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
window.addEventListener('beforeunload', () => {
setScrollRestoration('auto')
})
// Setting scrollRestoration to manual again when returning to this page.
window.addEventListener('load', () => {
setScrollRestoration('manual')
})
}
}
function shouldScrollToTop(route) {
const Pages = getMatchedComponents(route)
if (Pages.length === 1) {
const { options = {} } = Pages[0]
return options.scrollToTop !== false
}
return Pages.some(({ options }) => options && options.scrollToTop)
}
export default function (to, from, savedPosition) {
// If the returned position is falsy or an empty object, will retain current scroll position
let position = false
const isRouteChanged = to !== from
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
} else if (isRouteChanged && shouldScrollToTop(to)) {
position = { x: 0, y: 0 }
}
const nuxt = window.$nuxt
if (
// Initial load (vuejs/vue-router#3199)
!isRouteChanged ||
// Route hash changes
(to.path === from.path && to.hash !== from.hash)
) {
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
}
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
return window.scrollTo({})
// // CSS.escape() is not supported with IE and Edge.
// if (
// typeof window.CSS !== 'undefined' &&
// typeof window.CSS.escape !== 'undefined'
// ) {
// hash = '#' + window.CSS.escape(hash.substr(1))
// }
// try {
// if (document.querySelector(hash)) {
// // scroll to anchor by returning the selector
// position = { selector: hash, top: 100 }
// }
// } catch (e) {
// console.warn(
// 'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).'
// )
// }
}
resolve(position)
})
})
}
Any suggestions are appreciated.

How to use Computed Property in Nuxt js Layout Pages? (Composition Api)

I need to use computed property in my Category Layout page. When pages which are under this layout changed, computed must understand page changings. But I can't catch these changes and I can only catch changes if I use them in Watch property.
Layout.vue
v-app
v-main
nuxt
speed-dial-button(:page="page")
....
setup() {
const route = useRoute();
const page = ref('');
const currentPath = computed(()=>route.value.path);
if (['/eserler', '/en/artworks', '/ar/artworks'].includes(currentPath)) {
page.value = i18n.t('general.addNewArtwork');
} else if (['/kisiler', '/en/persons', '/ar/persons'].includes(currentPath)) {
page.value = i18n.t('general.addNewPerson');
} else if (['/etkinlikler', '/en/activities', '/ar/activities'].includes(currentPath)) {
page.value = i18n.t('general.addNewActivity');
}
console.log('page',page)
console.log('route',routePath)
return {
page,
};
}
This will work if I load page first time or if I leave layout and return again .
Your template will react, but not your log statement. Put that in a watcher. Set deep and immediate to your needs.
watch(route, value => {
// your code here
console.log(value.path)
}, {deep: true, immediate: true})

Call function after Vuetify v-bottom-sheet transition completes? Async vuex mutation? async computed property?

I've built a modal using Vuex and Vuetify's v-bottom-sheet. Basically how it works is when the state of sendMessageModal is true it is picked up in a computed property and shows the modal and vice versa for false:
//Vuex mutation
toggleSendMessageModal(state) {
return state.sendMessageModal = ! state.sendMessageModal;
}
//Vuex Action
closeSendMessageModal(context) {
return new Promise((resolve, reject)=>{
context.commit('toggleSendMessageModal');
setTimeout(1000);
const error = false;
if(!error){
resolve();
} else {
reject();
}
});
}
//SendMessageModal Component
computed: {
showSendMessageModal() {
return this.$store.state.sendMessageModal;
}
},
methods: {
closeSendMessageModal(){
this.$store.dispatch('closeSendMessageModal').then(function() {
this.clearValues()
}.bind(this));
},
clearValues: function(){
this.subject = '';
this.body = '';
this.slide = 1;
},
}
What I'm trying to do is clearValues() after the modal is closed. The only way I can currently do this is if I set a time out like such:
//SendMessageModal Component
closeSendMessageModal(){
this.$store.dispatch('closeSendMessageModal').then(function() {
setTimeout(function(){ this.clearValues(); }.bind(this), 1000);
}.bind(this));
},
But I'd like to wait for the modal to close then clearValues(). The problem is if someone wants to reopen the modal immediately again.
Note there is a transition on the modal, but I think what's going on is the toggleSendMessageModal isn't allowing for the modal to close then clear values.
Gif of CSS transition from Vuetify. And here is my full component code.
Edit 2
I don't know if this is a vuetify problem.
I set the time out on the closeSendMessageModal action:
closeSendMessageModal(context) {
return new Promise((resolve, reject)=>{
//context.commit('toggleSendMessageModal');
setTimeout(function(){ context.commit('toggleSendMessageModal'); }.bind(this), 1000);
const error = false;
if(!error){
resolve();
} else {
reject();
}
});
}
The this.clearValues(); method is still hitting before the closeSendMessageModal() action has been resolved.
This is more of a question than an answer but could be both. Cant you use nextTick for this? e.g.:
methods: {
closeSendMessageModal(){
this.$store.dispatch('closeSendMessageModal');
this.$nextTick(() => this.clearValues());
},
clearValues: function(){
this.subject = '';
this.body = '';
this.slide = 1;
},
}

Does the "before" hook trigger a render when leveraging transition hooks in a render function?

I am trying to create a movement transition within a render function using appear hooks. I thought that putting initial state of the transition within the beforeAppear hook and then setting the finalized state in the appear hook would cause an element to be enough for a transition to occur.
However, what seems to be happening is that the style that I set in the beforeAppear either never renders, or is overwritten in the appear hook before the browser knows that it should transition.
Below is an example of what I am trying to accomplish. Using Vue.nextTick() does not give me what I was hoping. However, I do see that using a setTimeout() will give me what I am effectively looking for, but I thought that this was the built-in functionality of the appear hook considering the docs Transition Classes:
v-enter: Starting state for enter. Added before element is inserted, removed one frame after element is inserted.
v-enter-active: Active state for enter. Applied during the entire entering phase. Added before element is inserted, removed when
transition/animation finishes. This class can be used to define the
duration, delay and easing curve for the entering transition.
I understand that these are for the classes that are applied to the element, but don't these map 1:1 with the hooks?
const app = new Vue({
el: "#app",
render(h) {
// Just hooks
const transitionTestJustHooks = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "0em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
el.style.left = "200px";
done();
}
}
}, [h("div", "just-hooks")]);
// Vue.nextTick()
const transitionTestNextTick = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "1em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
Vue.nextTick(() => {
el.style.left = "200px";
done();
});
}
}
}, [h("div", "Vue.nextTick()")]);
// setTimeout()
const transitionTestSetTimeout = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "2em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
setTimeout(() => {
el.style.left = "200px";
done();
});
}
}
}, [h("div", "setTimeout")]);
return h("div", [
transitionTestJustHooks,
transitionTestNextTick,
transitionTestSetTimeout
]);
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.min.js"></script>
<div id="app"></div>

Open a VueJS component on a new window

I have a basic VueJS application with only one page.
It's not a SPA, and I do not use vue-router.
I would like to implement a button that when clicked executes the window.open() function with content from one of my Vue Components.
Looking at the documentation from window.open() I saw the following statement for URL:
URL accepts a path or URL to an HTML page, image file, or any other resource which is supported by the browser.
Is it possible to pass a component as an argument for window.open()?
I was able to use some insights from an article about Portals in React to create a Vue component which is able to mount its children in a new window, while preserving reactivity! It's as simple as:
<window-portal>
I appear in a new window!
</window-portal>
Try it in this codesandbox!
The code for this component is as follows:
<template>
<div v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
// magic!
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeDestroy() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
The key is the line this.windowRef.document.body.appendChild(this.$el); this line effectively removes the DOM element associated with the Vue component (the top-level <div>) from the parent window and inserts it into the body of the child window. Since this element is the same reference as the one Vue would normally update, just in a different place, everything Just Works - Vue continues to update the element in response to databinding changes, despite it being mounted in a new window. I was actually quite surprised at how simple this was!
You cannot pass a Vue component, because window.open doesn't know about Vue. What you can do, however, is to create a route which displays your component and pass this route's URL to window.open, giving you a new window with your component. Communication between the components in different windows might get tricky though.
For example, if your main vue is declared like so
var app = new Vue({...});
If you only need to render a few pieces of data in the new window, you could just reference the data model from the parent window.
var app1 = window.opener.app;
var title = app.title;
var h1 = document.createElement("H1");
h1.innerHTML = title;
document.body.appendChild(h1);
I ported the Alex contribution to Composition API and works pretty well.
The only annoyance is that the created window ignores size and position, maybe because it is launched from a Chrome application that is fullscreen. Any idea?
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from "vue";
const props = defineProps<{modelValue: boolean;}>();
const emit = defineEmits(["update:modelValue"]);
let windowRef: Window | null = null;
const portal = ref(null);
const copyStyles = (sourceDoc: Document, targetDoc: Document): void => {
// eslint-disable-next-line unicorn/prefer-spread
for(const styleSheet of Array.from(sourceDoc.styleSheets)) {
if(styleSheet.cssRules) {
// for <style> elements
const nwStyleElement = sourceDoc.createElement("style");
// eslint-disable-next-line unicorn/prefer-spread
for(const cssRule of Array.from(styleSheet.cssRules)) {
// write the text of each rule into the body of the style element
nwStyleElement.append(sourceDoc.createTextNode(cssRule.cssText));
}
targetDoc.head.append(nwStyleElement);
}
else if(styleSheet.href) {
// for <link> elements loading CSS from a URL
const nwLinkElement = sourceDoc.createElement("link");
nwLinkElement.rel = "stylesheet";
nwLinkElement.href = styleSheet.href;
targetDoc.head.append(nwLinkElement);
}
}
};
const openPortal = (): void => {
nextTick().then((): void => {
windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
if(!windowRef || !portal.value) return;
windowRef.document.body.append(portal.value);
copyStyles(window.document, windowRef.document);
windowRef.addEventListener("beforeunload", closePortal);
})
.catch((error: Error) => console.error("Cannot instantiate portal", error.message));
};
const closePortal = (): void => {
if(windowRef) {
windowRef.close();
windowRef = null;
emit("update:modelValue", false);
}
};
watch(props, () => {
if(props.modelValue) {
openPortal();
}
else {
closePortal();
}
});
onMounted(() => {
if(props.modelValue) {
openPortal();
}
});
onBeforeUnmount(() => {
if(windowRef) {
closePortal();
}
});
</script>
<template>
<div v-if="props.modelValue" ref="portal">
<slot />
</div>
</template>