Prevent Vuetify v-tabs from change - vue.js

Is there a way to prevent the v-tabs from actually changing when being clicked on?
In my case I first need to check if stuff on the page has changed and want to cancel the switch to another tab if it has.
Neither a event.prevent nor event.stop will stop the v-tabs from changing:
<v-tab #click.prevent.stop="..."> ... </v-tab>
At the moment I'm using a window.requestAnimationFrame to reset the tab index to the old value. It gets the job done but this feels like a really nasty technique to me.
HTML:
<v-tabs v-model="currentIndex">
<v-tab v-for="(route, index) in list" :key="index" #change="handleTabChange(route, $event)" >
{{ route.meta.title }}
</v-tab>
</v-tabs>
TS:
public handleTabChange(routeConf:RouteConfig):void {
let currentIndex:number = this.currentIndex;
window.requestAnimationFrame(() => {
this.currentIndex = currentIndex;
Store.app.router.goto(routeConf.name, null, this.$route.params);
// Once the page actually changes this.currentIndex is set to the correct index..
});
}

I solve this problem by using separate variable between v-tabs and v-tabs-items.
<v-tabs v-model="tab" #change="onTabChange">
<v-tab v-for="item in items" :key="item">
{{ item }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="currentTab">
<v-tab-item v-for="item in items" :key="item">
<v-card>
<v-card-text>{{ item }}</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
methods: {
onTabChange() {
if (/* reject */) {
this.$nextTick(() => {
this.tab = this.currentTab
})
} else {
this.currentTab = this.tab
}
}
}
Demo
Another possible solution is to extend the v-tab component which is a bit more complicated but can actually override the behavior.
Create new file my-tab.js:
import { VTab } from 'vuetify/lib'
export default {
extends: VTab,
methods: {
async click (e) {
if (this.disabled) {
e.preventDefault()
return
}
// <-- your conditions
let ok = await new Promise(resolve => {
setTimeout(() => {
resolve(false)
}, 2000)
})
if (!ok) {
this.$el.blur()
return
}
// -->
if (this.href &&
this.href.indexOf('#') > -1
) e.preventDefault()
if (e.detail) this.$el.blur()
this.$emit('click', e)
this.to || this.toggle()
}
}
}
The original source code is here. You can also override the render function to change the styles.
Then just use it as normal component:
<v-tabs v-model="tab">
<my-tab v-for="item in items" :key="item">
{{ item }}
</my-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="item in items" :key="item">
<v-card>
<v-card-text
>{{ item }} ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.</v-card-text
>
</v-card>
</v-tab-item>
</v-tabs-items>

For me it works the following way:
...
<v-tab href="#tab-1">Tab-1</v-tab>
<v-tab href="#tab-2" #click.native.prevent.stop.capture="goto2()">Tab-2</v-tab>
...
...
private goto2() {
if(tab-1Changes) {
// do something
return;
}
this.tab = "tab-2";
}

You should have to follow this way in your code this is example which will help you:
In ts file:
<template>
<v-tabs v-model="activeTab">
<v-tab v-for="tab in tabs" :key="tab.id" :to="tab.route">{{ tab.name }}
</v-tab>
<v-tabs-items v-model="activeTab" #change="updateRouter($event)">
<v-tab-item v-for="tab in tabs" :key="tab.id" :to="tab.route">
<router-view />
</v-tab-item>
</v-tabs-items>
</v-tabs>
</template>
Script:
export default {
data: () => ({
activeTab: '',
tabs: [
{id: '1', name: 'Tab A', route: 'component-a'},
{id: '2', name: 'Tab B', route: 'component-b'}
]
}),
methods: {
updateRouter(val){
this.$router.push(val)
}
}
}

Related

Vuetify tabs with components - mounted called twice on components

I have a set of vuetify tabs where the content of each tab is conditionally rendered. This works as expected when the first tab is opened. When a second tab or beyond is opened, mounted is fired twice on the component inside that tab.
Here's how the tabs are structured:
<v-tabs
dark
background-color="blue-grey darken-4"
show-arrows
height="30"
v-model="name"
v-if="openTabs.length"
>
<v-tabs-slider color="accent"></v-tabs-slider>
<v-tab
v-for="item in openTabs"
:key="item.name"
v-model="activeTab"
class="pr-0 white--text"
color="accent"
active-class=""
#click="setActiveTab(item)"
>
<template class="white--text text-capitalize">
<v-icon>
{{ item.icon }}
</v-icon>
{{ item.name }}
<div v-if="item.changes">*</div>
<v-btn plain width="10" class="px-0" #click="closeTab(item.id)">
<v-icon
dark
small
width="10"
>mdi-close</v-icon>
</v-btn>
</template>
</v-tab>
</v-tabs>
<v-tabs-items v-model="name">
<v-tab-item
v-for="item in openTabs"
:key="item.name"
style="height: 100%"
:transition="false"
>
<BlockForm v-if="activeType === 'blocks'" v-bind:name="activeTab" v-bind:activeProject="activeProject" />
<BiomeForm v-if="activeType === 'biomes'" v-bind:name="activeTab" v-bind:projectName="activeProject" />
<ItemForm v-if="activeType === 'items'" v-bind:name="activeTab" v-bind:projectName="activeProject" />
<RecipeForm v-if="activeType === 'recipes'" v-bind:name="activeTab" v-bind:projectName="activeProject" />
</v-tab-item>
</v-tabs-items>
Here's what happens in setActiveTab:
setActiveTab: function (item) {
this.activeTab = item.name;
this.activeType = item.type;
}
BlockForm, ItemForm, BiomeForm, and RecipeForm are the components where mounted fires twice if they are the second (or third etc) tab opened. I'm not sure if this has something to do with the way I'm using v-if.
Hey mate have the same issue. I found explanation and as result code it.
It works properly. Try it.
<template>
<v-tabs centered="centered" grow v-model="activeTab">
<v-tab
v-for="tab of tabs"
:key="tab.id"
:id="tab.id"
:to="tab.route" exact>
{{ tab.title }}
</v-tab>
<v-tabs-items
v-model="activeTab"
#change="updateRouter($event)">
<v-tab-item
v-for="tab of tabs"
:key="tab.id"
:value="tab.route"
class="tab_content">
<router-view
v-if="tab.route === $route.fullPath && tab.route === activeTab">
</router-view>
</v-tab-item>
</v-tabs-items>
</v-tabs>
</template>
Here script data.
data() {
return {
activeTab: '',
tabs: [
{id: 1, title: 'Business', route: '/business'},
{id: 2, title: 'System', route: '/system'},
]
};
},
methods: {
updateRouter(tab) {
this.$router.push(tab)
},
}
And router
children: [
{
path: '/business',
name: 'business',
component: () => import(
/* webpackChunkName: "dashboard-page" */ './views/DashboardPage/DashboardTabs/DashboardBusinessTab'
),
meta: {
permission: null,
},
},
{
path: 'system',
name: 'system',
component: () => import(
/* webpackChunkName: "dashboard-page" */ './views/DashboardPage/DashboardTabs/DashboardSystemTab'
),
meta: {
permission: null,
},
},
],
This was key for solution
v-if="tab.route === $route.fullPath && tab.route === activeTab">

Keyboard movement Vuetify v-list-item

I have a Vuetify v-list-item that is iterated over to create me a list, I want to be able to use the up and down arrows to traverse it. How can I do this? It doesn't seem to be default behavior.
The default focusing between elements behavior works through tab. You can try something like this in order to use arrows keys:
<template>
<v-card
class="mx-auto"
max-width="300"
tile
>
<v-list dense>
<v-subheader>REPORTS</v-subheader>
<v-list-item-group
v-model="selectedItem"
color="primary"
>
<v-list-item
v-for="(item, i) in items"
:key="i"
>
<v-list-item-icon>
<v-icon v-text="item.icon"></v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="item.text"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card>
</template>
<script>
export default {
data: () => ({
selectedItem: 1,
items: [
{ text: 'Real-Time', icon: 'mdi-clock' },
{ text: 'Audience', icon: 'mdi-account' },
{ text: 'Conversions', icon: 'mdi-flag' },
],
}),
methods: {
nextItem () {
if (event.keyCode == 38 && this.selectedItem > 0) {
this.selectedItem--
} else if (event.keyCode== 40 && this.selectedItem < 3) {
this.selectedItem++
}
}
},
mounted () {
document.addEventListener("keyup", this.nextItem);
},
}
</script>

Nuxt not loading nested component

I've created a third party components library as described on this page https://nuxtjs.org/blog/improve-your-developer-experience-with-nuxt-components#third-party-component-library. Than I used the components in a new clean nuxt project. I have a BaseCard component which has 3 slots and I have a BaseImage component. Now I want to use the BaseImage component in a slot from the BaseCard component but it is not rendered. If I add an additional BaseImage component outside of the BaseCard component, than all BaseImage components are rendered (see screenshots below). Seems like that the components within a slot are not loaded.
Screenshots
without additional BaseImage
with additional BaseImage
Code
Don't work
<template>
<div>
<BaseCard>
<template v-slot:image>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</template>
<template v-slot:header>
Here might be a page title
</template>
<template v-slot:content>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum
pariatur distinctio cum. Ratione doloribus asperiores eaque
laboriosam repellendus perferendis iusto magni in necessitatibus
exercitationem eum expedita aliquam autem, tenetur itaque.
</p>
</template>
</BaseCard>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({});
</script>
works
<template>
<div>
<BaseCard>
<template v-slot:image>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</template>
<template v-slot:header>
Here might be a page title
</template>
<template v-slot:content>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum
pariatur distinctio cum. Ratione doloribus asperiores eaque
laboriosam repellendus perferendis iusto magni in necessitatibus
exercitationem eum expedita aliquam autem, tenetur itaque.
</p>
</template>
</BaseCard>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({});
</script>
nuxt.config.js (shared-components is my library for the components)
export default {
// Target: https://go.nuxtjs.dev/config-target
target: "static",
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: "demo",
htmlAttrs: {
lang: "en",
},
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ hid: "description", name: "description", content: "" },
],
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: ["#/assets/scss/variables.scss"],
styleResources: {
scss: ["./assets/scss/*.scss"],
},
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/typescript
"#nuxt/typescript-build",
"shared-components",
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: ["#nuxtjs/style-resources"],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
};
I use nuxt 2.15.2.

why when click on an item, all item opens

I have list with an accordion, when you click on a item, all items opens, I need to open just one,
I understand that a loop is needed to iterate over all the items and apply the class to a specific one, but how to do this, please help
component:
<ul class="accordion accordion__trigger"
:class="{'accordion__trigger_active': visible}"
#click="open">
<li class="accordion__item" v-for="category in MAIN_CATS">
<nuxt-link exact no-prefetch active-class="link-active"
:to="`/category/${category.id}`"
class="menu-button">
{{ category.title }}
</nuxt-link>
<div class="accordion__content">
<div class="menu-sub-list" v-show="visible">
<ul class="sub-list">
<li class="menu-item"
v-for="sub in SUB_CATS(category.id)"
:key="sub.id">
<nuxt-link :to="`/category/${sub.id}`" class="menu-button">
{{ sub.title }}
</nuxt-link>
</li>
</ul>
</div>
</div>
</li>
</ul>
code:
name: "acc",
data() {
return {
index: null,
Accordion: {
count: 0,
active: null
}
};
},
computed: {
...mapGetters([
'MAIN_CATS',
'SUB_CATS'
]),
visible() {
return this.index === this.Accordion.active;
}
},
methods: {
...mapActions([
'GET_CATEGORIES_LIST',
]),
open() {
if (this.visible) {
this.Accordion.active = null;
} else {
this.Accordion.active = this.index;
}
},
start(el) {
el.style.height = el.scrollHeight + "px";
},
end(el) {
el.style.height = "";
}
},
created() {
this.index = this.Accordion.count++;
},
mounted() {
this.GET_CATEGORIES_LIST()
},
I have list with an accordion, when you click on a item, all items opens, I need to open just one,
I understand that a loop is needed to iterate over all the items and apply the class to a specific one, but how to do this, please help
There are multiple differences between your code and code from the answer that you referred to.
You can notice that #click is placed in the same line as v-for.
The main reason for that is to be able to easily access index of each element in a loop.
Not to overcomplicate it for you, I created a basic use case scenario:
<template>
<div id="accordion" class="accordion-container">
<ul
v-for="(category, index) in items"
:key="index"
class="accordion accordion__trigger"
:class="{'accordion__trigger_active': visible===index}"
#click="visible=index"
>
<li class="accordion__item">
{{ category.title }}
<div class="accordion__content">
<div class="menu-sub-list" v-show="visible===index">
<ul class="sub-list">
<li class="menu-item">{{ category.sub }}</li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "trial-page",
data() {
return {
items: [
{
title: "Accordion 1",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
},
{
title: "Accordion 2",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
},
{
title: "Accordion 3",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
}
],
visible: null
};
}
};
</script>
<style>
.accordion__trigger_active {
background-color: blue;
color: white;
}
</style>
You can see that the idea is to operate with index value that is assigned to visible data property in this case.
We simply check if the visible is equal to the currently pressed item with the value of index.
With that we conditionally v-show element and trigger the class :class="{'accordion__trigger_active': visible===index}".
Note that if you had more v-for loops in the same component then you would need to make sure the value used for visible is always unique, for that you could simply add some string to it like:
#click="visible=index+'category'"
Also remember to assign a :key when using v-for.
Example:
v-for="(category, index) in items" :key="index"

vue js how to get notified when any property's value is being read?

i'm working in vue js and i'm trying to achieve something which has dependency. Actually inside data i have a property of boolean, what i want is that whenever this property's value is being used or this property is accessed i'm get notified so that i'm able to change other properties before this property's value getting used.
<template>
<!-- <v-card> -->
<v-navigation-drawer
v-model="drawer"
:mini-variant.sync="mini"
permanent
height="100%"
style="border:1px solid black;"
>
<v-list-item class="px-2">
<v-list-item-avatar>
<v-img src="https://randomuser.me/api/portraits/men/85.jpg"></v-img>
</v-list-item-avatar>
<v-list-item-title>John Leider</v-list-item-title>
<v-btn
icon
#click.stop="changeMiniValue()"
>
<v-icon>mdi-chevron-left</v-icon>
</v-btn>
</v-list-item>
<v-divider></v-divider>
<v-list dense>
<v-list-item
v-for="item in items"
:key="item.title"
link
>
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<!-- </v-card> -->
</template>
<script>
export default {
data () {
return {
drawer: true,
items: [
{ title: 'Home', icon: 'mdi-home-city' },
{ title: 'My Account', icon: 'mdi-account' },
{ title: 'Users', icon: 'mdi-account-group-outline' },
],
mini:this.getMini(),
}
},
methods:{
changeMiniValue(){
this.mini=!this.mini;
this.$store.dispatch('changeMini',!this.$store.state.mini);
},
getMini(){
this.$store.dispatch('changeColsToMin','9');
console.log('method executed');
return this.$store.state.mini;
}
},
created(){
this.$store.dispatch('changeColsToMin','11');
this.mini=this.$store.state.mini;
},
// computed:{
// getMiniValueCompute(){
// this.$store.dispatch('changeColsToMin','9');
// return this.$store.state.mini;
// }
// }
}
</script>
<style scoped>
</style>
This could be a possibile solution: create an "hidden" field and expose it through computed properties, with your custom logic.
<script>
export default {
data () {
return {
_mini: false
}
},
methods: {
// Your methods here...
},
computed: {
mini {
get: function () {
// TODO: notify your listeners, functions, etc.
return this._mini;
},
set: function (value) {
this._mini = mini;
}
}
}
}
</script>