Vuetify 3 v-dialog not opened immediately after click on mobile browser - vue.js

I am using vue3 and vuetify3 to build a site. When I click the button to open v-dialog, it won't be shown immediately. And during this delay time, whole site will be freezed until the dialog is opened.(Closing dialog has same issue) When there are more objects in the page, it will take more time to open and close.
In addition, I found that there just a liitle bit delay on desktop device but huge delay on mobile device.
How could I imporve the performance of this?
Environment
Vue: 3.2.37
Vuetify: 3.0.0-beta5
Mobile Device: iPhone 13 Pro Max (iOS 15.5)
Mobile Browser: Safari, Edge, Chrome
Desktop OS: Windows 11
Desktop Browser: Edge, Chrome
This is my code structure
ItemList.vue
<template>
<div v-for="(item,index) in itemList" :key="index">
<Item :item="item" />
</div>
<v-dialog v-model="dialog.open" transition="fade-transition">
<!-- item detail -->
<div>{{ dialog.item }}</div>
<v-btn #click="dialog.open = false">Close</v-btn>
</v-dialog>
</template>
<script>
import Item from './Item.vue';
export default {
name: 'ItemList',
data: () => ({
dialog: {
open: false,
item: {}
},
}),
props: ['itemList'],
components: {
Item,
},
methods: {
openDialog(item) {
this.dialog.item = item;
this.dialog.open = true;
},
}
};
</script>
Item.vue
<template>
<div>
<!-- item text and image -->
<v-btn #click.stop="openDialog(item)">Info</v-btn>
</div>
</template>
<script>
export default {
name: 'Item',
data: () => ({}),
components: {},
props: ['item'],
methods: {
openDialog(item) {
this.$emit('openDialog', item);
},
},
};
</script>

Related

Vue3 objects from Array only rendering after making a small change in component

ers,
Experiencing a strange rendering issue. I am grabbing user data from localForage located in my Vuex store in a promise in the following component:
<template>
<div>
<h1>Users available for test {{ $route.params.id }}</h1>
<v-form>
<div v-if="this.import_complete">
<UserList
:users="users"
/>
</div>
</v-form>
</div>
</template>
<script>
import UserList from './UserList.vue';
export default {
name: 'UserManagement',
components: {
UserList,
},
data: () => ({
users: [],
import_complete: false,
}),
mounted() {
Promise.resolve(this.$store.getters.getUsersByTestId(
this.$route.params.testId,
)).then((value) => {
this.users = value;
this.import_complete = true;
});
},
};
</script>
Since it's a promise, I am setting a boolean import_complete to true, and a div in the template is only passing through the data as a prop when this boolean is true
Next, I am consuming the data in another template, in a for loop.
<template>
<div>
<v-container>
<v-banner v-for="user in this.users" :key="user.index">
{{ user.index }} {{ user.name }} {{ user.profile }}
<template v-slot:actions>
<router-link
:to="`/usering/${user.test}/user/${user.index}`">
<v-btn text color="primary">Open usering analysis</v-btn>
</router-link>
<v-btn text color="warning" #click="deleteUser(user.index)">Delete</v-btn>
</template>
</v-banner>
</v-container>
</div>
</template>
<script>
export default {
name: 'UserList',
props: {
users: Object,
},
methods: {
deleteUser(index) {
this.$store.dispatch('delete_user', index);
},
},
mounted() {
console.log('mounted user list, here come the users');
console.log(this.users);
},
};
</script>
The thing is, the first time it doesn't show anything. Only when I make a tiny change in the last component (can be an Enter followed by a save command) and suddenly the users are displayed on the page.
Interestingly, in the first scenario, the user's array is already filled, I see it in the console (created in the mount method) as well in the Chrome developer Vue tab.
It's probably some kind of Vue thing I am missing? Does someone have a clue?
[edit]
I've changed the code to this, so directly invoking the localForage. It seems to work, but I would still like to understand why the other code won't work.
this.test = this.$store.getters.getTestByTestId(this.$route.params.testId);
this.test.store.iterate((value, key) => {
if (key === (`user${this.$route.params.userId}`)) {
this.user = value;
}
}).then(() => {
this.dataReady = true;
}).catch((err) => {
// This code runs if there were any errors
console.log(err);
});

beforeRouteLeave doesn't work imediately when using with modal and emit function

I have a Vue application with many child components. In my case, I have some parent-child components like this. The problem is that in some child components, I have a section to edit information. In case the user has entered some information and router to another page but has not saved it, a modal will be displayed to warn the user. I followed the instructions on beforeRouteLeave and it work well but I got a problem. When I click the Yes button from the modal, I'll emit a function #yes='confirm' to the parent component. In the confirm function, I'll set this.isConfirm = true. Then I check this variable inside beforeRouteLeave to confirm navigate. But in fact, when I press the Yes button in modal, the screen doesn't redirect immediately. I have to click one more time to redirect. Help me with this case
You can create a base component like the following one - and then inherit (extend) from it all your page/route-level components where you want to implement the functionality (warning about unsaved data):
<template>
<div />
</template>
<script>
import events, { PAGE_LEAVE } from '#/events';
export default
{
name: 'BasePageLeave',
beforeRouteLeave(to, from, next)
{
events.$emit(PAGE_LEAVE, to, from, next);
}
};
</script>
events.js is simply a global event bus.
Then, in your page-level component you will do something like this:
<template>
<div>
.... your template ....
<!-- Unsaved changes -->
<ConfirmPageLeave :value="modified" />
</div>
</template>
<script>
import BasePage from 'src/components/BasePageLeave';
import ConfirmPageLeave from 'src/components/dialogs/ConfirmPageLeave';
export default
{
name: 'MyRouteName',
components:
{
ConfirmPageLeave,
},
extends: BasePage,
data()
{
return {
modified: false,
myData:
{
... the data that you want to track and show a warning
}
};
}.
watch:
{
myData:
{
deep: true,
handler()
{
this.modified = true;
}
}
},
The ConfirmPageLeave component is the modal dialog which will be shown when the data is modified AND the user tries to navigate away:
<template>
<v-dialog v-model="showUnsavedWarning" persistent>
<v-card flat>
<v-card-title class="pa-2">
<v-spacer />
<v-btn icon #click="showUnsavedWarning = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pt-2 pb-3 text-h6">
<div class="text-h4 pb-4">{{ $t('confirm_page_leave') }}</div>
<div>{{ $t('unsaved_changes') }}</div>
</v-card-text>
<v-card-actions class="justify-center px-3 pb-3">
<v-btn class="mr-4 px-4" outlined large tile #click="showUnsavedWarning = false">{{ $t('go_back') }}</v-btn>
<v-btn class="ml-4 px-4" large tile depressed color="error" #click="ignoreUnsaved">{{ $t('ignore_changes') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import events, { PAGE_LEAVE } from '#/events';
export default
{
name: 'ConfirmPageLeave',
props:
{
value:
{
// whether the monitored data has been modified
type: Boolean,
default: false
}
},
data()
{
return {
showUnsavedWarning: false,
nextRoute: null,
};
},
watch:
{
showUnsavedWarning(newVal)
{
if (!newVal)
{
this.nextRoute = null;
}
},
},
created()
{
events.$on(PAGE_LEAVE, this.discard);
window.addEventListener('beforeunload', this.pageLeave);
},
beforeDestroy()
{
events.$off(PAGE_LEAVE, this.discard);
window.removeEventListener('beforeunload', this.pageLeave);
},
methods:
{
discard(to, from, next)
{
if (this.value)
{
this.nextRoute = next;
this.showUnsavedWarning = true;
}
else next();
},
pageLeave(e)
{
if (this.value)
{
const confirmationMsg = this.$t('leave_page');
(e || window.event).returnValue = confirmationMsg;
return confirmationMsg;
}
},
ignoreUnsaved()
{
this.showUnsavedWarning = false;
if (this.nextRoute) this.nextRoute();
},
}
};
</script>
<i18n>
{
"en": {
"confirm_page_leave": "Unsaved changes",
"unsaved_changes": "If you leave this page, any unsaved changes will be lost.",
"ignore_changes": "Leave page",
"go_back": "Cancel",
"leave_page": "You're leaving the page but there are unsaved changes.\nPress OK to ignore changes and leave the page or CANCEL to stay on the page."
}
}
</i18n>

Vue trigger click event after v-for has create a new dom

I use v-for to generate task tabs.
Tasks can be created by users, and after users have created a new task I want the UI changing to the newly created tab automatically.
i.e. After a new tab has been added to the dom tree, itself click event will be triggered and the callback function activateTask will execute.
<template>
<v-container>
<v-btn #click="createNewTask"></v-btn>
<v-tabs>
<v-tab v-for="task in tasks" :key="task.uuid" #click="activateTask">
{{ task.name }}
</v-tab>
</v-tabs>
</v-container>
</template>
<script>
export default {
data: {
tasks: [],
},
method: {
createNewTask() {
this.tasks.push({
uuid: util.generateUUID(),
name: "name",
});
},
},
};
</script>
You can bind v-tabs with v-model to control active-tab.
<template>
<v-container>
<v-btn #click="createNewTask"></v-btn>
<v-tabs v-model="currentTab">
<v-tab v-for="task in tasks" :key="task.uuid" #click="activateTask">
{{ task.name }}
</v-tab>
</v-tabs>
</v-container>
</template>
<script>
export default {
data: {
tasks: [],
currentTab: null
},
method: {
createNewTask() {
this.tasks.push({
uuid: util.generateUUID(),
name: "name",
});
this.currentTab = this.tasks.length-1
},
},
};
</script>
Figure out a way that using the update hook and a variable to check whether a new tab dom is created. If true select the last child node from dom tree and dispatch click event.
So ugly 😔.
updated() {
this.$nextTick(() => {
if (!this.newTaskCreated) {
return
}
this.$el.querySelectorAll('.task-tab:last-child')[0].dispatchEvent(new Event('click'))
this.newTaskCreated = false
})
}

Render a child component in the global app component

I wrote a dialog component (global) to show modal dialogs with overlays like popup forms.
Right now the dialog gets rendered inside the component where it is used. This leads to overlapping content, if there is something with position relative in the html code afterwards.
I want it to be rendered in the root App component at the very end so I can force the dialog to be always ontop of every other content.
This is my not working solution:
I tried to use named slots, hoping, that they work backwards in the component tree too. Unfortunately they don't seem to do that.
Anybody a solution how to do it?
My next idea would be to render with an extra component that is stored in the app component and register the dialogs in the global state. But that solution would be super complicated and looks kinda dirty.
The dialog component:
<template v-slot:dialogs>
<div class="dialog" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if(!this.persistent) {
this.$emit('close')
}
}
}
}
</script>
The template of the app component:
<template>
<div class="application">
<div class="background">
<div class="satellite"></div>
<div class="car car-lr" :style="{ transform: `translateY(${car.x}px)`, left: adjustedLRLeft + '%' }" v-for="car in carsLR"></div>
</div>
<div class="content">
<login v-if="!$store.state.user"/>
<template v-else>
<main-menu :show-menu="showMainMenu" #close="showMainMenu = false"/>
<router-view/>
</template>
<notifications/>
<div class="dialogs"><slot name="dialogs"></slot></div>
</div>
</div>
</template>
Another possibility is to use portals. These provide a way to move any element to any place in the dom. Checkout the following library: https://github.com/LinusBorg/portal-vue
You can just place the dialog component directly in the app component and handle the dialog logic/which dialog to display in that component?
In case you want to trigger these dialogs from other places in your app, this would would be a good use case for vuex! That, combined with dynamic webpack imports is how I handle this.
With the help of the guys from the vuetify2 project, I found the solution. The dialog component gets an ref="dialogContent" attribute and the magic happens inside the beforeMount function.
<template>
<div class="dialog" ref="dialogContent" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {
show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if (!this.persistent) {
this.$emit('close')
}
}
},
beforeMount() {
this.$nextTick(() => {
const target = document.getElementById('dialogs');
target.appendChild(
this.$refs.dialogContent
)
})
},
}
</script>

Can't show the all the map on a DIV with VueJs and leaflet until resizing the screen

I am using VueJS and Leaflet to show the map with special localisation. I have added the css leaflet on index.html as told in the documentation.
link rel="stylesheet"
href="https://unpkg.com/leaflet#1.2.0/dist/leaflet.css">
But I have just a part of the map.
I have to change the size of the screen to have all the map with the marker.
This is the vue where I implement the map (iMap.vue)
<template>
<div id="professionnel">
<b-row>
<b-tabs>
<b-tab title="A" >
<div>
<b-col class="col-12">
<div>Où nous joindre</div>
</b-col>
</div>
</b-tab>
<b-tab title="B">
<div class="tab-content-active" >
<b-col class="col-6">
<div>heure</div>
</b-col>
<b-col class="col-6 data_map">
<iMap1></iMap>
</b-col>
</div>
</b-tab>
</tabs>
</b-row>
</div>
</template>
<script>
import iMap1 from './iMap1'
export default {
name: 'professionnel',
components: {
iMap1
},
data() {
return {}
}
</script>
And this is the vue of the Map (iMap.vue)
<template>
<div id="imap1" class="map" >
</div>
</template>
<script>
import leaflet from 'leaflet'
export default {
name: 'imap1',
components: {
leaflet
},
data() {
return {
professionnel: {},
map1: null,
tileLayer: null,
}
},
methods: {
initMap() {
this.map1 = L.map('imap1').setView([47.413220, -1.219482], 12)
this.tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
//'https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}.png',
{
maxZoom: 18,
center: [47.413220, -1.219482],
attribution: '© OpenStreetMap, © CARTO',
}).addTo(this.map1)
L.marker([47.413220, -1.219482]).addTo(this.map1).bindPopup('name')
.openPopup()
this.map1.invalidateSize()
})
},
},
created () {
this.initMap()
}
}
</script>
Use the mounted lifecycle hook instead of the created one.
created is typically to subscribe to some data / start some async processes, whereas mounted is rather when the DOM of your component is ready (but not necessarily insterted in the page IIRC).
Then, as explained in Data-toggle tab does not download Leaflet map, you have to use invalidateSize after the Tab that contains your Map container is opened, i.e. you have to listen to an event that signals that your user has opened the Tab.
In the case of Bootstrap-Vue, you have the <b-tabs>'s "input" event, but which signals only when the user has clicked on the Tab. But the latter is still not opened. Therefore you have to give it a short delay (typically with setTimeout) before calling invalidateSize:
Vue.use(bootstrapVue);
Vue.component('imap', {
template: '#imap',
methods: {
resizeMap() {
if (this.map1) {
this.map1.invalidateSize();
// Workaround to re-open popups at their correct position.
this.map1.eachLayer((layer) => {
if (layer instanceof L.Marker) {
layer.openPopup();
}
});
}
},
initMap() {
this.map1 = L.map('imap1').setView([47.413220, -1.219482], 12)
this.tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
center: [47.413220, -1.219482],
attribution: '© OpenStreetMap',
}).addTo(this.map1)
L.marker([47.413220, -1.219482]).addTo(this.map1).bindPopup('name')
.openPopup() // Opening the popup while the map is incorrectly sized seems to place it at an incorrect position.
},
},
mounted() {
this.initMap()
},
});
new Vue({
el: '#app',
methods: {
checkMap(tab_index) {
if (tab_index === 1) {
// Unfortunately the "input" event occurs when user clicks
// on the tab, but the latter is still not opened yet.
// Therefore we have to wait a short delay to allow the
// the tab to appear and the #imap1 element to have its final size.
setTimeout(() => {
this.$refs.mapComponent.resizeMap();
}, 0); // 0ms seems enough to execute resize after tab opens.
}
}
},
});
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.1/dist/leaflet-src.js" integrity="sha512-IkGU/uDhB9u9F8k+2OsA6XXoowIhOuQL1NTgNZHY1nkURnqEGlDZq3GsfmdJdKFe1k1zOc6YU2K7qY+hF9AodA==" crossorigin=""></script>
<script src="https://unpkg.com/vue#2"></script>
<link rel="stylesheet" href="https://unpkg.com/bootstrap#4/dist/css/bootstrap.css" />
<link rel="stylesheet" href="https://unpkg.com/bootstrap-vue#2.0.0-rc.11/dist/bootstrap-vue.css" />
<script src="https://unpkg.com/bootstrap-vue#2.0.0-rc.11/dist/bootstrap-vue.js"></script>
<div id="app">
<!-- https://bootstrap-vue.js.org/docs/components/tabs -->
<b-tabs #input="checkMap">
<b-tab title="First Tab" active>
<br>I'm the first fading tab
</b-tab>
<b-tab title="Second Tab with Map">
<imap ref="mapComponent"></imap>
</b-tab>
</b-tabs>
</div>
<template id="imap">
<div id="imap1" style="height: 130px;"></div>
</template>