Separate navigation drawers for certain routes in vue-router - vue.js

I'm building a Vuetify app in combination with Vuex and vue-router. Some of the views uses the default navigation drawer, but others has different items in their navigation drawers. This documentation say I can pass props to view components. So I implement it like this:
routes/index.js
{
path: '/courses/:courseId/lessons/:lessonId',
name: 'Course 1',
components: {
default: () => import('#/views/ViewLesson.vue'),
sidebar: () => import('#/components/CourseNavBar/CourseNavBar.vue')
},
props: {
items: [
{ text: "Link 1", href:"/link1" },
{ text: "Link 2", href:"/link2" }
]
}
}
src/App.vue
<template>
<v-app>
<v-app-bar
app
color="primary"
dark
>
<h1>My Project</h1>
</v-app-bar>
<v-navigation-drawer><router-view :items="items" name="sidebar"/></v-navigation-drawer>
<v-content>
<router-view />
</v-content>
</v-app>
</template>
But apparently,
src/components/CourseNavBar.vue
<template>
<!-- <v-navigation-drawer :value="1"> -->
<v-list dense>
<navbar-item v-for="(item, i) in items" :key="i" :item="item" >
{{ item.text }}
</navbar-item>
</v-list>
<!-- </v-navigation-drawer> -->
</template>
<script>
import NavBarItem from './NavBarItem.vue'
export default {
props: {
items: Array
},
components: {
'navbar-item': NavBarItem
}
}
</script>
But <CourseNavBar>'s props is still undefined. What am I doing wrong here?

There are a few issues...
Replace = with :...
items: [
{ text:"Link 1", href:"/link1" },
{ text:"Link 2", href:"/link2" }
]
And the sidebar component (not router-view) should be in the slot for the navigation-drawer...
<v-navigation-drawer><sidebar :items="items"></sidebar></v-navigation-drawer>
Demo: https://codeply.com/p/oNInfpTwvK

In your
routes\index.js
you need to define the props option for each of the named views:
{
path: '/courses/:courseId/lessons/:lessonId',
name: 'Course 1',
components: {
default: () => import('#/views/ViewLesson.vue'),
sidebar: () => import('#/components/CourseNavBar/CourseNavBar.vue')
},
props: {
default: true,
sidebar: true,
items: [
{ text: "Link 1", href: "/link1" },
{ text: "Link 2", href: "/link2" }
]
}
}
And following on from what #Zim said, you've used "=" instead of ":" when assigning the href value to the props items array.
You can use <router-view name="sidebar"/> to output the named component.

Related

Children of NuxtLink rendered twice (hydration error?)

My hunch is that there is some hydration mismatch where the FontAwesomeIcon was not rendered on the server (only the span) and then on the client both child nodes of the NuxtLink were rendered (the svg and the span), prompting Nuxt to render the span twice.
The console does not return an error, though.
Any thoughts on how to debug this?
This is the Vue component:
<template>
<ul v-if="routes.length > 0" class="col-span-2 flex flex-col">
<li v-for="(item, i) in routes" :key="item.name">
<NuxtLink :to="item.path" target="_blank">
<FontAwesomeIcon :icon="item.icon" class="mr-3" fixed-width />
<span>{{ item.title }}</span>
</NuxtLink>
</li>
</ul>
</template>
<script lang="ts">
export default defineComponent({
props: {
links: {
type: Array,
default: () => ["instagram", "facebook", "email"],
},
},
computed: {
routes() {
return [
{
name: "instagram",
path: "https://www.instagram.com/insta.name/",
title: "Instagram",
icon: ["fab", "instagram"],
},
{
name: "facebook",
path: "https://www.facebook.com/fb.name",
title: "Facebook",
icon: ["fab", "facebook"],
},
{
name: "email",
path: "mailto:hello#example.com",
title: "Email",
icon: ["fas", "envelope"],
},
].filter((e) => this.links.includes(e.name));
},
},
});
</script>

Pass component as prop in Vue JS

Intro: I am exploring Vue Js and got stuck while trying to make a dynamic data table component the problem I am facing is that I cannot pass a component via props and render it inside a table.
Problem: So basically what I am trying to do is to pass some custom component from headers prop in v-data-table such as:
headers = [
{ text: 'Name', value: 'name' },
{
text: 'Phone Number',
value: 'phone_number',
render: () => (
<div>
<p>Custom Render</p>
</div>
)
},
{ text: 'Actions', value: 'actions' }
]
So from the code above we can see that I want to render that paragraph from the render function inside Phone Number header, I did this thing in React Js before, but I cannot find a way to do it in Vue Js if someone can point me in the right direction would be fantastic. Thank you in advance.
You have 2 options - slots and dynamic components.
Let's first explore slots:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<v-chip>{{ item.phone_number }}</v-chip>
</template>
<template slot="item.company_name" slot-scope="{item}">
<v-chip color="pink darken-4" text-color="white">{{ item.company_name }}</v-chip>
</template>
</v-data-table>
</template>
The data table provides you slots where you can customize the content. If you want to make your component more reusable and want to populate these slots from your parent component - then you need to re-expose these slots to the parent component:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="props">
<slot name="phone" :props="props" />
</template>
<template slot="item.company_name" slot-scope="props">
<slot name="company" :props="props" />
</template>
</v-data-table>
</template>
If you don't know which slots will be customized - you can re-expose all of the data-table slots:
<template>
<v-data-table
:headers="headers"
:items="items"
:search="search"
hide-default-footer
:options.sync="pagination"
:expanded="expanded"
class="tbl_manage_students"
height="100%"
fixed-header
v-bind="$attrs"
#update:expanded="$emit('update:expanded', $event)"
>
<!-- https://devinduct.com/blogpost/59/vue-tricks-passing-slots-to-child-components -->
<template v-for="(index, name) in $slots" v-slot:[name]>
<slot :name="name" />
</template>
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data" />
</template>
<v-alert slot="no-results" color="error" icon="warning">
{{ $t("no_results", {term: search}) }}"
</v-alert>
<template #footer="data">
<!-- you can safely skip the "footer" slot override here - so it will be passed through to the parent component -->
<table-footer :info="data" #size="pagination.itemsPerPage = $event" #page="pagination.page = $event" />
</template>
</v-data-table>
</template>
<script>
import tableFooter from '#/components/ui/TableFooter'; // you can safely ignore this component in your own implementation
export default
{
name: 'TeacherTable',
components:
{
tableFooter,
},
props:
{
search:
{
type: String,
default: ''
},
items:
{
type: Array,
default: () => []
},
sort:
{
type: String,
default: ''
},
headers:
{
type: Array,
required: true
},
expanded:
{
type: Array,
default: () => []
}
},
data()
{
return {
pagination:
{
sortDesc: [false],
sortBy: [this.sort],
itemsPerPageOptions: [25, 50, 100],
itemsPerPage: 25,
page: 1,
},
};
},
watch:
{
items()
{
this.pagination.page = 1;
},
sort()
{
this.pagination.sortBy = [this.sort];
this.pagination.sortDesc = [false];
},
}
};
</script>
Dynamic components can be provided by props:
<template>
<v-data-table :items="dataItems" :headers="headerItems">
<template slot="item.phone_number" slot-scope="{item}">
<component :is="compPhone" :phone="item.phone_number" />
</template>
<template slot="item.company_name" slot-scope="{item}">
<component :is="compCompany" :company="item.company_name" />
</template>
</v-data-table>
</template>
<script>
export default
{
name: 'MyTable',
props:
{
compPhone:
{
type: [Object, String], // keep in mind that String type allows you to specify only the HTML tag - but not its contents
default: 'span'
},
compCompany:
{
type: [Object, String],
default: 'span'
},
}
}
</script>
Slots are more powerful than dynamic components as they (slots) use the Dependency Inversion principle. You can read more in the Markus Oberlehner's blog
Okay, I don't believe this is the best way possible but it works for me and maybe it will work for someone else.
What I did was I modified the headers array like this:
headers = [
{ text: 'Name', align: 'start', sortable: false, value: 'name' },
{
text: 'Phone Number',
key: 'phone_number',
value: 'custom_render',
render: Vue.component('phone_number', {
props: ['item'],
template: '<v-chip>{{item}}</v-chip>'
})
},
{ text: 'Bookings', value: 'bookings_count' },
{
text: 'Company',
key: 'company.name',
value: 'custom_render',
render: Vue.component('company_name', {
props: ['item'],
template:
'<v-chip color="pink darken-4" text-color="white">{{item}}</v-chip>'
})
},
{ text: 'Actions', value: 'actions', sortable: false }
]
And inside v-data-table I reference the slot of custom_render and render that component there like this:
<template v-slot:[`item.custom_render`]="{ item, header }">
<component
:is="header.render"
:item="getValue(item, header.key)"
></component>
</template>
To go inside the nested object like company.name I made a function which I called getValue that accepts 2 parametes, the object and the path to that value we need which is stored in headers array as key (ex. company.name) and used loadash to return the value.
getValue function:
getValue (item: any, path: string): any {
return loadash.get(item, path)
}
Note: This is just the initial idea, which worked for me. If someone has better ideas please engage with this post. Take a look at the props that I am passing to those dynamic components, note that you can pass more variables in that way.

Vue.js – New routes are always placed on top of current route

In my Vue.js application I use a navigation drawer on the left side of the screen for navigation purposes. I am using Vuetify and Vue Router.
Problem
After changing my routes a lot of my previously routing functionalities got messed up. Whenever a user clicks on an element to navigate to a new site the route get's attached on top of the previous one like this.
Route before a user clicks on a link:
http://localhost:8080/organization/:organization_id/planner/events
New route after a user clicks on a link:
http://localhost:8080/organization/:organization_id/planner/management/contracts
Router
routes: [
// Login
{
path: '/login',
component: Login,
},
// Organization
{
path: '/organization',
component: Organization,
},
// Organization Detail Page
{
path: '/organization/:organization_id',
component: OrganizationDetail,
children: [
// Module: Planner
{
path: 'planner',
component: Planner
},
// Planner > Events
{
path: 'planner/events',
component: Events
},
{
path: 'planner/events/new',
component: NewEvent
},
{
path: 'planner/events/details/:event_id',
component: EventDetails
},
// Planner > Calendar
{
path: 'planner/calendar',
component: Calendar
},
// Module: Management
{
path: 'management',
component: Management
},
// Management > Users
{
path: 'management/users',
component: Events
},
{
path: 'management/users/new',
component: NewUser
},
{
path: 'management/users/details/:user_id',
component: UserDetails
},
// Planner > Contracts
{
path: 'management/contracts',
component: Contracts
}
]
}
]
NavigationDrawer.vue (updated):
<template>
<div id="navigation">
<nav>
<v-navigation-drawer
v-model="drawer"
:clipped="$vuetify.breakpoint.lgAndUp"
width="400"
fixed
app
>
<v-list>
<template v-for="item in items">
<v-row
v-if="item.heading"
:key="item.heading"
align="center"
>
</v-row>
<v-list-group
v-else-if="item.children"
:key="item.text"
v-model="item.model"
append-icon="mdi-chevron-down"
class="nav"
link
router :to="item.route"
>
<v-list-item
v-for="(child, i) in item.children"
:key="i"
link
router :to="child.route"
>
<v-list-item-action v-if="child.icon">
<v-icon>{{ child.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ child.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
</template>
</v-list>
</v-navigation-drawer>
<v-app-bar
:clipped-left="$vuetify.breakpoint.lgAndUp"
app
dark
>
<v-app-bar-nav-icon #click.stop="drawer = !drawer"></v-app-bar-nav-icon>
</v-app-bar>
</nav>
</div>
</template>
<script>
export default {
name: 'NavigationDrawer',
el: '#app',
data: () => ({
drawer: true,
menu: false,
items: [
{
text: 'Planner',
children: [
{ text: 'Events', route:'/organization/' + this.$route.params.organization_id + '/planner/events'},
{ text: 'Calendar', route:'/organization/' + this.$route.params.organization_id + '/planner/calendar'}
],
},
{
text: 'Management',
children: [
{ text: 'Users', route:'/organization/' + this.$route.params.organization_id + '/management/users'},
{ text: 'Contracts', route:'/organization/' + this.$route.params.organization_id + '/management/contracts'}
],
},
]
}),
methods: {
logout() {
// some awesome stuff
}
}
}
</script>
Other routes like (NewUser, NewEvent) are not stored in the navigation drawer and called manually by a button.
I know that this has something to do with my routes. However those aren't misconfigured because the relations between parent and children are important for displaying the right content and implementing some header informations. The navigation drawer works also the right way. I didn't change anything there.
Also if I use router.back() on NewEvent or NewUser nothing happens. :(
Help is very much appreciated, thanks!
The issue is because of
{ text: 'Events', route:'/planner/events'},
The above route redirects the website to http://localhost:8080/planner/events because of initial forward slash.
Use the $route.params to get params from URL like the following.
<div>User {{ $route.params.organization_id }}</div>
or
this.$route.params.organization_id

Vuejs - Component not rendering to screen

I'm trying to create a vue app for budget tracking and I have a BudgetItems component that I want to render in the /budget route. All the other components and raw HTML render but this one component does not
This is the BudgetItems component:
<template>
<div>
<BudgetItem v-for="item in Items" v-bind:key='item.id' v-bind:Item="item" />
</div>
</template>
<script>
import BudgetItem from './BudgetItem'
export default {
name: 'BudgetItems',
components: {
BudgetItem,
},
props: [
'Items'
]
}
</script>
And this is the BudgetItem component I used to render a single item:
<template>
<div class="budgetitem">
<h1>{{item.title}}</h1>
<h1>{{item.value}}</h1>
</div>
</template>
<script>
export default {
name: 'BudgetItem',
props: [
'Item'
]
}
</script>
Last of all, this is the Budget page view:
<template>
<div class="budget">
<Nav />
<h1>Budget</h1>
<BudgetItems v-bind:Items="items" />
</div>
</template>
<script>
import Nav from "../components/Nav"
import BudgetItems from "../components/BudgetItems"
export default {
name: 'Budget',
components: {
Nav,
BudgetItems,
},
data(){
return{
items: [
{
id: 1,
income: false,
title: "Item 1",
value: 200
},
{
id: 2,
income: true,
title: "Item 2",
value: 500
},
{
id: 3,
income: false,
title: "Item 3",
value: 10
},
]
}
}
}
</script>
Also, when I look in the vue dev tools tab, the component appears, it just doesn't show on the screen
You need to change the v-bind declarations to lower case. Replace each instance of Items and Item with items and item.
Vue.JS doesn't like it if you capitalise props when using binding.
Please read this for more explanation.
Essentially, browsers treat all attribute names as lowercase. As a result, it interprets "Items" as being "items".
Budget page view:
<BudgetItems v-bind:items="items" />
BudgetItems:
<BudgetItem v-for="item in items" v-bind:key='item.id' v-bind:item="item"/>
props: [
'items'
]
BudgetItem:
props: [
'item'
]
Once you make these changes, it works perfectly as seen here:

Dynamic Buttons in Navigation Bar using Vuetify

I have a problem...
My VueJS Application using Vuetify.
I have a <v-toolbar>, and on the right, I want to place some buttons that change depending on the component shown in <router-view>, but i can't access to component properties from $route or $route for get objects and methods bind to model of my component.
I would like to know if there is any way to assign a model to from my main component.
I have tried with "named-routes" but I do not know what is the way that properties can be shared between components that are managed by an <router-view> and updated live.
In resume:
I have my application skeleton with a navigation bar, additionally in the dynamic content I have a <router-view>. Depending on the component that is displayed in <router-view>, I would like to see buttons in the navigation bar corresponding to that component, which interact and change the data or execute methods of the component.
App.vue
<template>
<v-app>
<router-view></router-view>
</v-app>
</template>
<script>
export default {
name: 'App',
data() {
return {
};
}
};
</script>
index.js (router)
import Vue from 'vue'
import Router from 'vue-router'
import AppLogin from '#/components/AppLogin'
import Skeleton from '#/components/Skeleton'
import ShoppingCart from '#/components/ShoppingCart'
import ShoppingCartButtons from '#/components/ShoppingCartButtons'
import ProductSelection from '#/components/ProductSelection'
import ProductSelectionButtons from '#/components/ProductSelectionButtons'
import ProductDetail from '#/components/ProductDetail'
Vue.use(Router)
export default new Router({
routes: [
{
path : '/login',
name : 'AppLogin',
component : AppLogin
},
{
path : '/app',
name : 'Skeleton',
component : Skeleton,
children : [{
path : 'shopping-cart',
components : {
navigation : ShoppingCart,
navButtons : ShoppingCartButtons
}
}, {
path: 'product-selection',
name : 'ProductSelection',
components : {
navigation : ProductSelection,
navButtons : ProductSelectionButtons
}
},
{
path: 'product-detail',
name : 'ProductDetail',
components : {
navigation : ProductDetail
},
props : true
}
]
}
]
})
Skeleton.vue
<template>
<v-container fluid>
<v-navigation-drawer
persistent
:mini-variant="miniVariant"
:clipped="true"
v-model="drawer"
enable-resize-watcher
fixed
app
>
<v-list>
<v-list-tile
value="true"
v-for="(item, i) in items"
:key="i"
:to="item.path">
<v-list-tile-action>
<v-icon v-html="item.icon"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar
app
:clipped-left="clipped"
>
<v-toolbar-side-icon #click.stop="drawer = !drawer">
</v-toolbar-side-icon>
<v-toolbar-title v-text="$route.meta.title"></v-toolbar-title>
<v-spacer></v-spacer>
<router-view name="navButtons"></router-view>
</v-toolbar>
<v-content>
<router-view name="navigation"/>
</v-content>
<v-footer :fixed="true" app>
<p style="text-align : center; width: 100%">© CONASTEC 2018</p>
</v-footer>
</v-container>
</template>
<script>
export default {
data() {
return {
clipped: true,
drawer: false,
fixed: false,
items: [
{
icon: "shopping_cart",
title: "Carrito de Compras",
path : "/app/shopping-cart"
},
{
icon: "attach_money",
title: "Facturas"
},
{
icon: "account_balance_wallet",
title: "Presupuestos"
},
{
icon: "insert_chart",
title: "Informes"
},
{
icon: "local_offer",
title: "Productos"
},
{
icon: "person",
title: "Clientes"
},
{
icon: "layers",
title: "Cuenta"
},
{
icon: "comment",
title: "Comentarios"
},
{
icon: "settings",
title: "Ajustes"
}
],
buttons : [],
miniVariant: false,
right: true,
rightDrawer: false
};
},
name: "Skeleton"
};
</script>
EDITED
My solution is create a new component Toolbar and add slots for buttons to right and left.
<template>
<div>
<v-navigation-drawer persistent :mini-variant="false" :clipped="true" v-model="drawer" enable-resize-watcher fixed app>
<v-list>
<v-list-tile value="true" v-for="(item, i) in items" :key="i" :replace="true" :to="item.path">
<v-list-tile-action>
<v-icon v-html="item.icon"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar app :clipped-left="true" color="primary" :dark="true" flat>
<v-toolbar-side-icon v-if="showDrawer" #click.stop="drawer = !drawer">
</v-toolbar-side-icon>
<v-toolbar-side-icon v-if="!!back" #click="back">
<v-icon>keyboard_backspace</v-icon>
</v-toolbar-side-icon>
<v-toolbar-title v-text="title" style="font-size: 1.4em"></v-toolbar-title>
<v-spacer></v-spacer>
<v-card-actions>
<slot name="right"></slot>
</v-card-actions>
</v-toolbar>
<v-snackbar
:timeout="5000"
:top="true"
:multi-line="true"
:vertical="true"
v-model="snackbar.show"
>
{{ snackbar.content }}
<v-btn flat color="white" #click.native="snackbar.show = false">Cerrar</v-btn>
</v-snackbar>
</div>
</template>
<script>
export default {
name: 'app-toolbar',
props: ['title','showDrawer', 'back'],
data() {
return {
drawer : false,
items: [{
icon: "shopping_cart",
title: "Carrito de Compras",
path: "/carrito-compras"
}, {
icon: "attach_money",
title: "Facturas",
path: "/documentos-tributarios"
}, {
icon: "account_balance_wallet",
title: "Presupuestos"
}, {
icon: "insert_chart",
title: "Informes"
}, {
icon: "local_offer",
title: "Productos"
}, {
icon: "person",
title: "Clientes"
}, {
icon: "layers",
title: "Cuenta"
}, {
icon: "comment",
title: "Comentarios"
}, {
icon: "settings",
title: "Ajustes"
}]
};
},
computed : {
snackbar() {
return this.$store.getters.snackbar;
}
}
}
</script>
and use is:
<app-toolbar title="Carrito de Compras" :showDrawer="true">
<template slot="right">
<v-toolbar-side-icon #click="confirm">
<v-icon>monetization_on</v-icon>
</v-toolbar-side-icon>
</template>
</app-toolbar>
I did the same thing as you in a recent project and found altering the structure was the easier way to fix issues like this.
My structure was as follows:
app.vue: Only contains <router-view> no other components
router.js: Parent route is a layout component, all sub routes which contains my toolbars and other layout components and it's own <router-view> which receives child routes
ex:
{
path: '/login',
name: 'Login',
component: load('login')
},
{
path: '/',
component: load('main-layout'),
children: [
{
path: '',
name: 'Home Page',
component: load('homePage')
},
{
path: '/settings',
name: 'Settings',
component: load('settings'),
}
]
}
Now in your main-layout:
computed: {
showHomeButton () {
if (this.$route.path === '/') {
return true
}
return false
// Repeat for other routes, etc...
},
}
If you are using Vuex, you can use vuex-router-sync, then you can
access the route from any component with this.$state.route.path.
If not, Scott's answer is probably the best way to do it.
I had the same problem, My solution was manage the left button action from de meta of vue router like this:
{
path: '/feedstocks/:categoryId/:id',
name: 'Feedstock',
component: () =>
import(
/* webpackChunkName: "client-chunk-feedstock-details" */ '#/views/FeedstockDetails.vue'
),
props: true,
meta: {
authNotRequired: true,
backRoute: 'Material'
}
}
Then I'm able to check that metadata in app bar button action:
<v-app-bar app color="primary" dark>
<v-btn text icon color="white" #click="leftButtonAction">
<v-icon>{{ leftButtonIcon }}</v-icon>
</v-btn>
<v-toolbar-title>
{{ currentAppTitle }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
leftButtonIcon() {
if (this.$route.meta.backRoute) {
return 'mdi-chevron-left'
}
return 'mdi-menu'
}
leftButtonAction() {
if (this.$route.meta.backRoute) {
this.$router.push({ name: this.$route.meta.backRoute })
} else {
this.toggleDrawer()
}
}
I guess it's a time for a more up-to-date answer.
For my requirement to dynamically modify the item list in the app-bar based on the selected view, the solution was to use the Named Views