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

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
})
}

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.js pass $store data from different modules

Hi I need to understand how to "pass" some $store values from settings module to header module, being the $store updated by settingsmodule
I have this app.vue module:
<template>
<v-app>
<router-view name="sidebar" />
<router-view name="header" :handleSettingsDrawer="handleSettingsDrawer" />
<v-main class="vue-content">
<v-fade-transition>
<router-view name="settings" />
</v-fade-transition>
</v-main>
<router-view name="footer" />
<app-settings-drawer
:handleDrawer="handleSettingsDrawer"
:drawer="settingsDrawer"
/>
</v-app>
</template>
in the <router-view name="header" /> there are a couple of v-menu to select currency and language:
<v-menu offset-y close-on-click>
<template v-slot:activator="{ on }">
<v-btn v-on="on" small fab class="mr-3">
<v-avatar size="30">
<v-img :src="currentLocaleImg"></v-img>
</v-avatar>
</v-btn>
</template>
<v-list dense class="langSel">
<v-list-item
v-for="(item, index) in langs"
:key="index"
#click="handleInternationalization(item.value,item.rtl)"
>
<v-list-item-avatar tile class="with-radius" size="25">
<v-img :src="item.img"></v-img>
</v-list-item-avatar>
<v-list-item-title>{{ item.text }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
in the script section I import the availableLanguages and availableCurrencies , I set them in data() where I also reference the user:
data() {
return {
items: [
{ icon: "settings", text: "Settings",link: "/settings" },
{ icon: "account_balance_wallet", text: "Account", link:"/tos" },
{ divider: true },
{ icon: "login", text: "Log In",link:"/auth/login" },
{ icon: "desktop_access_disabled", text: "Lock Screen",link:"/auth/lockscreen" },
{ icon: "exit_to_app", text: "Logout",link:"/auth/logout" },
],
langs: availableLocale,
currs: availableCurrency,
user: this.$store.state.userdata.user,
};
},
then, in computed: I have the 2 functions that should get the current value of curr and lang:
currentLocaleImg()
{
return this.langs.find((item) => item.value === this.$store.state.userdata.user.locale).img;
},
currentCurrencyImg() {
return this.currs.find((itemc) => itemc.value === this.$store.state.userdata.user.currency).img;
}
}
BUT the value of this.$store.state.userdata.user is updated firing the loadData() function mounted in the <router-view name="settings" /> module of App.vue
mounted(){
this.loadData();
},
methods:
{
async loadData(){
let jwt=sessionStorage.getItem('jwt');
try{
const res = await this.$http.post('/ajax/read_settings.php', {"jwt":jwt});
this.$store.dispatch("setUser", res.data.user);
this.$store.dispatch("setLocale", res.data.user.locale);
this.$store.dispatch("setCurrency", res.data.user.currency);
}
the problem is that header module does not have the value of this.$store.state.userdata.user.locale
(and .currency) and I get twice the following message:
vue.runtime.esm.js:1888 TypeError: Cannot read property 'img' of undefined
I do not know how to "pass" the data from one module to the other (or perhaps because the header is rendered before the settings module) and therefore the value of the currency and of the language are not known yet
What's wrong in this procedure?
How can I fix it?
There's a race condition in which you try to use data in the template that isn't loaded yet. But first, avoid setting store data directly into component data because the component data won't be updated when the store changes. Replace this:
user: this.$store.state.userdata.user
With a computed:
computed: {
user() {
return this.$store.state.userdata.user;
}
}
You can use v-if to conditionally render only when some data is available:
<v-img :src="currentLocaleImg" v-if="user"></v-img>
The currentLocaleImg computed will not even trigger until user has some data so you will avoid the error. Do the same for currentCurrencyImg if necessary.

Why doesn't the '#drop' event work for me in vue?

The #drop listener doesn't work for me. It doesn't call the method I'm telling it to call.
I want to drag the chip and be able to drop it on another component, and perform a function, but at the time of dropping the chip, the dropLink method is not executed, so I assume that the #drop event is not emitted.
No errors are displayed on the console.
The rest of the events do work well, like #dragstart.
This is the code of the component I use:
<template>
<div
#keydown="preventKey"
#drop="dropLink"
>
<template
v-if="!article.isIndex"
>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-chip
small
draggable
class="float-left mt-2"
v-on="on"
#dragstart="dragChipToLinkToAnotherElement"
>
<v-icon x-small>
mdi-vector-link
</v-icon>
</v-chip>
</template>
<span>Link</span>
</v-tooltip>
<v-chip-group
class="mb-n2"
show-arrows
>
<v-chip
v-for="(lk, index) in links"
:key="index"
small
outlined
:class="{'ml-auto': index === 0}"
>
{{ lk.text }}
</v-chip>
</v-chip-group>
</template>
<div
:id="article.id"
spellcheck="false"
#mouseup="mouseUp($event, article)"
v-html="article.con"
/>
</div>
</template>
<script>
export default {
name: 'ItemArticle',
props: {
article: {
type: Object,
required: true
}
},
computed: {
links () {
return this.article.links
}
},
methods: {
mouseUp (event, article) {
this.$emit('mouseUp', { event, article })
},
preventKey (keydown) {
this.$emit('preventKey', keydown)
},
dragChipToLinkToAnotherElement (event) {
event.dataTransfer.setData('text/plain', this.article.id)
},
dropLink (e) {
//but this method is never called
console.log('evento drop is ok', e)
}
}
}
</script>
In the project I am also using Nuxt in case that is relevant.
In order to make the div a drop target, the div's dragenter and dragover events must be canceled. Firefox also needs the drop event to be canceled.
You can invoke Event.preventDefault() on those events with the .prevent event modifier:
<div #drop.prevent="dropLink" #dragenter.prevent #dragover.prevent></div>
If you need to accept/reject drops based on the drag data type, set a handler that conditionally calls Event.preventDefault():
<div #drop.prevent="dropLink" #dragenter="checkDrop" #dragover="checkDrop"></div>
export default {
methods: {
checkDrop(e) {
if (/* allowed data type */) {
e.preventDefault()
}
},
}
}
demo

Problem when creating a menu by iteration

I'm new to vue and vuetify. I need to create a submenu and for that I am using v-menu. Its construction is by iteration, where I need each sub menu to assign it a method. But it turns out that the way I'm doing it generates an error
[Vue warn]: Error in v-on handler: 'TypeError: handler.apply is not a function'
. What am I doing wrong?
https://codepen.io/maschfederico/pen/vMWBPV?editors=1011
<div id="app">
<v-app id="inspire">
<div class="text-xs-center">
<v-menu>
<template #activator="{ on: menu }">
<v-tooltip bottom>
<template #activator="{ on: tooltip }">
<v-btn
color="primary"
dark
v-on="{ ...tooltip, ...menu }"
>Dropdown w/ Tooltip</v-btn>
</template>
<span>Im A ToolTip</span>
</v-tooltip>
</template>
<v-list>
<v-list-tile
v-for="(item, index) in items"
:key="index"
#click="item.f"
>
<v-list-tile-title>{{ item.title }}</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</div>
</v-app>
</div>
new Vue({
el: '#app',
data: () => ({
items: [
{ title: 'Click Me1',f:'login'},
{ title: 'Click Me2',f:'login' },
{ title: 'Click Me3',f:'login' },
{ title: 'Click Me4' ,f:'login' }
]
}),
methods: {
login(){console.log('login')}
}
})
Try to pass the method name to another one and handle it inside the last one like :
<v-list-tile
v-for="(item, index) in items"
:key="index"
#click="handle(item.f)"
>
inside the methods :
methods: {
handle(f){
this[f]();
},
login(){console.log('login')}
}
check this codepen
You are passing the method's name - a string - instead of a function. The click event listener generated by vue is trying to call a function using apply, this is why you are getting that error.
One solution would be to pass directly the function when the Vue instance is created (before that, the method might not be available, so passing it directly to the data { title: 'Click Me1', f: this.login } would not work).
For example, you could keep having method names in the data, and replace them with the actual methods at create:
new Vue({
el: '#app',
data: () => ({
items: [
{ title: 'Click Me1', f: 'login' }
]
}),
created () {
this.items.forEach(item => {
item.f = this[item.f]
})
},
methods: {
login (){
console.log('login')
}
}
})