How to append #click navigateTo() event to v-list items props - vue-router

I am using Nuxt3 with Vuetify3, and I am having trouble getting this to work:
<template>
<v-navigation-drawer
v-model="menu"
class="pa-4"
temporary
>
<v-list
:items="items"
nav
density="compact"
/>
</v-navigation-drawer>
</template>
<script setup lang="ts">
const menu = ref(false);
const items = [
{
title: 'Dashboard',
props: {
'#click': () => {
navigateTo('./dashboard');
},
prependIcon: 'mdi-view-dashboard-variant-outline'
}
},
{
title: 'Orders',
props: {
to: './orders',
prependIcon: 'mdi-package-variant'
}
}
];
</script>
The Orders list-item works as expected, but it is causing other bugs in my app with Nuxt, so I would like to use Nuxt's navigateTo() function. The Dashboard link doesn't have the click event attached.
I have also tried '#click': navigateTo('./dashboard') but this creates an endless navigation loop in Nuxt. I also tried 'v-on:click' and click () {}, but I can't seem to get it to append the click event to the v-list-items.
Obviously I could rewrite this without the shorthand <v-list :items="items" />, but I'm sure there must be a way to do this shorthand.

Related

Vue 3: access VueComponent object placed in slots

I'm working on tab component and I want to render tab labels in parent component by getting child's slot, named 'label'
In Vue 2.x I could approach that, by referring to $slots property of tab component, in Tabs.vue:
<template>
<section class="tabs">
<ul class="tabs-labels">
<li
v-for="tab in tabs"
:key="tab._uid"
:class="[{'active': tab.isActive}, 'tab-label']"
#click="selectTab(tab);"
>
{{ tab.$slots.label }}
</li>
</ul>
<div class="tabs-content">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: 'Tabs',
data () {
return {
tabs: [],
};
},
mounted () {
// filter tabs in case there were additional vue components placed in slots
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
},
methods: {
selectTab (selectedTab) {
// set isActive property of the tab by comparing their uids
this.tabs.forEach(tab => {
tab.isActive = (tab._uid === selectedTab._uid);
});
},
},
};
</script>
TabsContent.vue:
<template>
<div v-show="isActive" class="single-tab-content">
<slot/>
</div>
</template>
<script>
export default {
name: 'TabContent',
data () {
return {
isActive: false
};
},
};
</script>
Here, when the tab label clicked, in Tabs.vue I iterate through tabs array and setting their isActive property, comparing their uid and uid of selectedTab
But in Vue 3.x API of slots has changed, so I changed the way of getting tab contents:
from
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
to
this.tabs = this.$slots.default().filter(tab => tab.type.name === 'TabContent');
but as I understand, it getting only vNodes, not actual VueComponent that rendered, so when I'm executing selectTab method
tab.isActive = (tab._uid === selectedTab._uid);
it updates only isActive properties for tabs, that were saved in tabs array, not for actual tab contents, so v-show never changes.
Is there any way to get actual rendered VueComponents from <slots>? Or maybe this approach is wrong from the beginning and I should try something else?
Edit
CodeSandboxes for both versions:
Vue 2.x -ignore the error about refering to children during render, it's a bug on CodeSandbox
Vue 3.x
Its a bit more complicated with Vue 3. You will want to look into using provide and inject. here is a good example.
https://gist.github.com/cathrinevaage/4eed410b31826ce390153d6834909436
sandbox - https://codesandbox.io/s/happy-rubin-z414h?file=/src/App.vue
The example above is using typescript however you get the idea.

How can I get 'mobile-breakpoint' state enabled, in vuefity?

I want to control the <v-navigation-drawer> by mobile-breakpoint status.
When it is enabling, use to drawer else to mini.
How can I refer the mobile-breakpoint status?
<template>
<v-app>
<v-navigation-drawer
v-model="drawer"
:mini-variant.sync="mini"
mobile-breakpoint="960"
app>
<h1>v-navigation-drawer</h1>
</v-navigation-drawer>
<v-btn #click="click"> Button </v-btn>
</v-app>
</template>
<script>
export default {
name: "foo",
data: () => ({
drawer: true,
mini: true
}),
methods: {
click() {
// How to refer 'mobile-breakpoint' status??
if (mobileBreakpointEnabled) {
this.drawer = !this.drawer
} else {
this.mini = !this.mini
}
}
}
}
</script>
If the value is gonna be hardcoded and not changed, I would recommend to just use a computed to check if the breakpoint is reached.
You can use:
this.$vuetify.breakpoint.name
to see if you have reached your breakpoint.
Perhaps just having a breakpoint variable with the width you want to break it at is helpful to have.
See documentation:
https://vuetifyjs.com/en/features/breakpoints/#breakpoint-service

Why is switching a Vue component reloading the page?

I have something that looks like this (in a Nuxt application):
<template>
<component :is="dynamicComponent"></component>
</template>
<script>
export default {
components: {
Component1,
Component2
},
created() {
this.$nuxt.$on("switch1", () => {
this.dynamicComponent = Component1;
});
this.$nuxt.$on("switch2", () => {
this.dynamicComponent = Component2;
});
},
data() {
return {
dynamicComponent: Component1
};
}
}
</script>
So an event triggers switching the component. However, if I try to switch from the default Component1 to Component2, Component2 briefly appears, only for the page to be reloaded with Component1. That seems to be such a basic thing and I have no idea why it's not working.
EDIT:
I found the mistake. Somehow during refactoring/reformatting the call got changed from:
<a class="nav-item" href="#" #click="$nuxt.$emit('switch2')">..</a>
to
<a class="nav-item" href="" #click="$nuxt.$emit('switch2')">..</a>
which triggers a reload of the page on click.

How to prevent parent component from reloading when changing a parameterised child component in Vue js

I have a page where a ClientPortfolio (parent component) containing a list of Securities (child component) are loaded in a v-data-table list.
The issue I have is that ClientPortfolio is fully reloaded every time I click on a security in the list causing the entire list to be refreshed causing scroll and selected class to reset, as well as unncessary performance overhead.
I have looked at the documentation of Vue and nothing seems to point out how to only refresh a child component when it has parameters, it looks like the parent component is being refreshed as the route is changing every time a security is selected, despite expecting that Vue would know that only sub (nested route) is changing hence need to only reload the child component
The closest answer I got was explained on https://github.com/vuejs/vue-router/issues/230 which does not explain in the code how to achieve this.
routes.js:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
children: [
{ path: 'security/:securityNo', component: Security }
]
},
]
Router link in ClientPortfolios.vue:
<router-link tag="tr" style="cursor:pointer"
:to="`/client/${$route.params.clientno}/portfolios/${selectedPortfolioSequenceNo}/security/${props.item.SecurityNo}-${props.item.SequenceNo}`"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
Router view (for Security component) in ClientPortfolios.vue:
<v-flex xs10 ml-2>
<v-layout>
<router-view :key="$route.fullPath"></router-view>
</v-layout>
</v-flex>
Any hint on how to prevent parent from getting reloaded is appreciated.
EDIT: Trying to get closer to the issue, I notice that the "Key" attr in ClientPortfolios changes (as shown in the Vue debug window above) whenever I change the Security, could that be the reason? Is there a way to assign a key to ClientPortfolios component although its not a child one? Or a way to not update its key when navigating to different securities?
UPDATE: Full code
ClientPortfolios.vue
<template>
<v-layout row fill-height>
<v-flex xs2>
<v-layout column class="ma-0 pa-0 elevation-1">
<v-flex>
<v-select v-model="selectedPortfolioSequenceNo" :items="clientPortfolios" box label="Portfolio"
item-text="SequenceNo" item-value="SequenceNo" v-on:change="changePortfolio">
</v-select>
</v-flex>
<v-data-table disable-initial-sort :items="securities" item-key="Id" hide-headers hide-actions
style="overflow-y: auto;display:block;height: calc(100vh - 135px);">
<template slot="items" slot-scope="props">
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'Security', params: { securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
>
</router-link>
</template>
<template v-slot:no-data>
<v-flex class="text-xs-center">
No securities found
</v-flex>
</template>
</v-data-table>
</v-layout>
</v-flex>
<v-flex xs10 ml-2>
<v-layout>
<keep-alive>
<router-view></router-view>
</keep-alive>
</v-layout>
</v-flex>
</v-layout>
</template>
<script>
import Security from '#/components/Security'
export default {
components: {
security: Security
},
data () {
return {
portfoliosLoading: false,
selectedPortfolioSequenceNo: this.$route.params.portfolioNo,
selectedPortfolio: null,
securityNo: this.$route.params.securityNo
}
},
computed: {
clientPortfolios () {
return this.$store.state.ClientPortfolios
},
securities () {
if (this.clientPortfolios == null || this.clientPortfolios.length < 1) {
return []
}
let self = this
this.selectedPortfolio = global.jQuery.grep(this.clientPortfolios, function (portfolio, i) {
return portfolio.SequenceNo === self.selectedPortfolioSequenceNo
})[0]
return this.selectedPortfolio.Securities
}
},
mounted () {
this.getClientPortfolios()
},
activated () {
},
methods: {
changePortfolio () {
this.$router.push({
path: '/client/' + this.$route.params.clientno + '/portfolios/' + this.selectedPortfolioSequenceNo
})
},
getClientPortfolios: function () {
this.portfoliosLoading = true
let self = this
this.$store.dispatch('getClientPortfolios', {
clientNo: this.$route.params.clientno
}).then(function (serverResponse) {
self.portfoliosLoading = false
})
}
}
}
</script>
Security.vue
<template>
<v-flex>
<v-layout class="screen-header">
<v-flex class="screen-title">Security Details </v-flex>
</v-layout>
<v-divider></v-divider>
<v-layout align-center justify-space-between row class="contents-placeholder" mb-3 pa-2>
<v-layout column>
<v-flex class="form-group" id="security-portfolio-selector">
<label class="screen-label">Sequence</label>
<span class="screen-value">{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Security</label>
<span class="screen-value">{{security.SecurityNo}}-{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Status</label>
<span class="screen-value-code" v-if="security.Status !== ''">{{security.Status}}</span>
</v-flex>
</v-layout>
</v-layout>
</v-flex>
</template>
<script>
export default {
props: ['securityNo'],
data () {
return {
clientNo: this.$route.params.clientno,
securityDetailsLoading: false
}
},
computed: {
security () {
return this.$store.state.SecurityDetails
}
},
created () {
if (this.securityNo.length > 1) {
this.getSecurityDetails()
}
},
methods: {
getSecurityDetails: function () {
let self = this
this.securityDetailsLoading = true
this.$store.dispatch('getSecurityDetails', {
securityNo: this.securityNo,
clientNo: this.clientNo
}).then(function (serverResponse) {
self.securityDetailsLoading = false
})
}
}
}
</script>
router.js
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Dashboard
},
{
path: '/client/:clientno/details',
component: Client,
props: true
},
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: 'ClientPortfolios',
children: [
{ path: 'security/:securityNo',
component: Security,
name: 'Security'
}
]
}
]
})
UPDATE:
Just to update this as it’s been a while, I finally got to find out what the problem is, which is what #matpie indicated elsewhere, I have found out that my App.vue is the culprit where there is a :key add to the very root of the application: <router-view :key="$route.fullPath" /> this was a template I used from somewhere but never had to look at as it was "working", after removing the key, all is working as it should, marking matpie answer accepted.
Preventing component reload is the default behavior in Vue.js. Vue's reactivity system automatically maps property dependencies and only performs the minimal amount of work to ensure the DOM is current.
By using a :key attribute anywhere, you are telling Vue.js that this element or component should only match when the keys match. If the keys don't match, the old one will be destroyed and a new one created.
It looks like you're also pulling in route parameters on the data object (Security.vue). Those will not update when the route parameters change, you should pull them in to a computed property so that they will always stay up-to-date.
export default {
computed: {
clientNo: (vm) => vm.$route.params.clientno,
}
}
That will ensure that clientNo always matches what is found in the router, regardless of whether Vue decides to re-use this component instance. If you need to perform other side-effects when clientNo changes, you can add a watcher:
vm.$watch("clientNo", (clientNo) => { /* ... */ })
Could you please check again after removing the local registration of the security component? As it's not needed because this is being handled by the vue router itself.
components: { // delete this code
security: Security
},
Instead of using router here. Declare two variable at root level for selected security and portfolio,
list the securities based on the selected portfolio.
on selecting a security from displayed securities, update the root variable using,
this.$root.selectedSecurityId = id;
you can have watch at security component level.
In root,
<security selectedid="selectedSecurityId" />
In component security,
....
watch:{
selectedid:function(){
//fetch info and show
}
}
...
the components will be look like following,
<portfolio>
//active. list goes here
</portfolio>
........
<security selectedid="selectedSecurityId">
//info goes here
</security>
Above approach will help to avoid routers. hope this will help.
I had a similar issue once. IMO it was caused by path string parsing.
Try to set a name for your route. And replace your router-link to param with an object.
And remove router-view :key prop. It doesn't need to be there. It is used to force component update when a route changes. It is usually a sign of bad code. Your component (Security) should react to route params update. Not the parent component force it to.
So, try to change your code to:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: "ClientPortfoliosName", // it can be anything you want. It`s just an alias for internal use.
children: [
{
path: 'security/:securityNo',
name: "PortfolioSecurities", // anyway, consider setting route names as good practice
component: Security
}
]
},
]
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'PortfolioSecurities', params: { clientno: $route.params.clientno, portfolioNo: selectedPortfolioSequenceNo, securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
And it should work.
P.S. In your router-link you shall point to the route you want to navigate to. In this case PortfolioSecurities

Reusable component to render button or router-link in Vue.js

I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>