How to get slot to work inside a Quasar Layout - vuejs2

So I have a basic layout :
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated class="bg-secondary text-white">
<q-toolbar >
<slot name="toolbar"></slot>
</q-toolbar>
</q-header>
<q-page-container >
<router-view ></router-view>
</q-page-container>
</q-layout>
</template>
And a basic router:
const routes = [
{
path: '/',
component: () => import('layouts/MyLayout.vue'),
children: [
{ path: '', component: () => import('pages/Index.vue') }
]
}
]
and Index.vue:
<template>
<q-page>
<template v-slot:default> CONTENT SHOWS</template>
<template v-slot:toolbar> CONTENT DOESN'T SHOW</template>
</q-page>
</template>
What do I do wrong? How can I get the slot up the router?

Simple answer: there is no way to accomplish that using slots.
You are seeing CONTENT SHOW because q-page has the default slot, but there is no definition for the toolbar slot there. Remember, slots get passed to their direct parents, this is a Vue fundamental rule. Your practice moved me to the age of master pages.
Using Name Views is the proper solution -- should move your toolbars into separate components and define them in routes.
Another solution, check the current route and render a proper toolbar:
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated class="bg-secondary text-white">
<q-toolbar>
<template v-if="$route.name === 'Index'">
<div>Toolbar for Index Page </div>
</template>
<template v-else-if="$route.fullPath.startsWith('/nested')">
<div>Toolbar for Index2 Page </div>
</template>
<template v-else>
<div>Toolbar for Default Pages </div>
</template>
</q-toolbar>
</q-header>
<q-page-container>
<router-view></router-view>
</q-page-container>
</q-layout>
</template>
Of course, moving these v-if logics into a separate component is a better practice.

It's been a long time since the original post, but maybe it can help someone else. Use names in router-view.
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated class="bg-secondary text-white">
<q-toolbar >
<router-view name="toolbar"></router-view>
</q-toolbar>
</q-header>
<q-page-container >
<router-view></router-view>
</q-page-container>
</q-layout>
</template>
Then, the route setup can contains something like this
const routes = [
{
path: '/',
component: () => import('layouts/MyLayout.vue'),
children: [{
path: '',
components : {
default: () => import('pages/Index.vue'),
toolbar: () => import('some another page or component.vue')
}
}]
}
]

Related

Executing js on slot

I'm a beginner in web development and I'm trying to help out friends restarting an old game. I'm in charge of the tooltip component but I hit a wall...
There are many Vue components and in a lot of them I want to call a child component named Tooltip, I'm using vue-tippy for easy configuration. This is the component:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<slot name='tooltip-content'>
</slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
}
</script>
In one of the other components I try to use the tooltip:
<template>
<a class="action-button" href="#">
<Tooltip>
<template #tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
<template #tooltip-content>
<h1>{{action.name}}</h1>
<p>{{action.description}}</p>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
From here everything is fine, the tooltip is correctly displayed with the proper content.
The thing is, the text in the {{ named.description }} needs to be formatted with the formatContent content. I know I can use the props, the components would look like that:
Tooltip.vue:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
props: {
title: {
type: String,
required: true
},
content: {
type: Array,
required: true
}
}
}
</script>
Parent.vue:
<template>
<a class="action-button" href="#">
<Tooltip :title="action.name" :content="action.description">
<template v-slot:tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
But I need to use a slot in the tooltip component because we'll have some "extensive" lists with v-for.
Is there a way to pass the data from a slot into a JS function?
If I understand you correctly, you're looking for scoped slots here.
These will allow you to pass information (including methods) from child components (the components with <slot> elements) back to the parents (the component(s) filling those slots), allowing parents to use chosen information directly in the slotted-in content.
In this case, we can give parents access to formatContent(), which will allow them to pass in content that uses it directly. This allows us to keep the flexibility of slots, with the data passing of props.
To add this to your example, we add some "scope" to your content slot in Tooltip.vue. This just means we one or more attributes to your <slot> element, in this case, formatContent:
<!-- Tooltip.vue -->
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<!-- Attributes we add or bind to this slot (eg. formatContent) -->
<!-- become available to components using the slot -->
<slot name='tooltip-content' :formatContent="formatContent"></slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods: {
formatContent(value) {
// Rewrote as a ternary, but keep what you're comfortable with
return !value ? '' : formatText(value.toString());
}
},
}
</script>
Now that we've added some scope to the slot, parents filling the slot with content can use it by invoking a slot's "scope":
<!-- Parent.vue -->
<template>
<a class="action-button" href="#">
<Tooltip>
. . .
<template #tooltip-content="{ formatContent }">
<!-- Elements in this slot now have access to 'formatContent' -->
<h1>{{ formatContent(action.name) }}</h1>
<p>{{ formatContent(action.description) }}</p>
</template>
</Tooltip>
</a>
</template>
. . .
Sidenote: I prefer to use the destructured syntax for slot scope, because I feel it's clearer, and you only have to expose what you're actually using:
<template #tooltip-content="{ formatContent }">
But you can also use a variable name here if your prefer, which will become an object which has all your slot content as properties. Eg.:
<template #tooltip-content="slotProps">
<!-- 'formatContent' is now a property of 'slotProps' -->
<h1>{{ slotProps.formatContent(action.name) }}</h1>
<p>{{ slotProps.formatContent(action.description) }}</p>
</template>
If you still need the v-html rendering, you can still do that in the slot:
<template #tooltip-content="{ formatContent }">
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>

Vue subrouting with navbar and sidebar

I'm trying to set up a routing system with vue. For my purpose, I need a fixed navbar on the top that needs to be displayed on every page and a sidebar that I want to display only on the settings page. Following the documentation I tried:
const routes = [
{
path: '/settings',
name: 'Settings',
component: Settings,
children: [
{
path: 'route1',
name: 'Route1',
component: Route1
},
{
path: 'route2',
name: 'Route2',
component: Route2
}
]
}
]
Then on the settings template:
<template>
<div class="flex items-start">
<div class="lg:w-3/12 w-12 sm:w-16 md:w-24 pb-10 lg:pr-8">
<Sidebar />
</div>
</div>
<div class="lg:w-9/12 w-full pt-10 pb-8 text-justify">
// My subroute goes here
</div>
</template>
I feel that I'm missing something. First, I can't understand how to properly display the subroutes. I tried with <router-view /> but it seems to refer to the parent navigation.
Second, I don't want the user to visit the /settings route but only /settings/route1 and settings/route2.
I can achieve this by simply adding the sidebar in every settings route but this seems bad because it forces the <Sidebar/> component to be mounted every time
Where am I wrong?
Thanks
As you probably have guessed, the <router-view /> element goes in your Settings component:
<template>
<div class="flex items-start">
<div class="lg:w-3/12 w-12 sm:w-16 md:w-24 pb-10 lg:pr-8">
<Sidebar />
</div>
</div>
<div class="lg:w-9/12 w-full pt-10 pb-8 text-justify">
<router-view /> <!-- Here is your router view -->
</div>
</template>
Then as it was pointed out in the comments, /settings will always be a valid route.
What you can do when the client directly navigates to /settings is to replace the current route with one of the two children (possibly based on some logic) in the mounted hook:
mounted() {
if(this.$router.currentRoute.path.endsWith('/settings')) {
this.$router.replace('/settings/route1')
}
}
Or use $router.push() instead based on what you want the navigation history to look like.

Vue how to customize global navbar at view level

Im super new to Vue.
i have a Vue-CLI app, which have a navbar and content.
Navbar is common to all pages, but i want to customize in each page whit some additional content.
Example:
Common-> home | about
View home -> home | about | your are in view home
View about -> home | about | your are in view about
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import NavBar from '#/components/NavBar.vue';
Vue.use(VueRouter);
Vue.component('nav-bar', NavBar);
//...
components/navbar.vue
<template>
<div>
<b-nav-item to="/">home</b-nav-item>
<b-nav-item to="/about">about</b-nav-item>
{{customContent}}
</div>
</template>
<script>
export default {
name: 'NavBar',
props: {
customContent: {
type: String,
default: 'default Content',
},
},
};
</script>
App.vue
<template>
<div id="app">
<nav-bar />
<div class="container-fluid">
<router-view />
</div>
</div>
</template>
views/home.vue
<template>
<div class="row">
<div class="col-12">
<image-card :images="images"/>
</div>
</div>
</template>
<script>
//how can i customize here the navbar by adding for example 'your are in view home'???
</script>
Thanks so much!
There are a few ways in which you can solve this problem. I'll list two of them.
1. Update NavBar by $route
In this approach, the NavBar component already contains all of the possible combinations, and will display the relevant portion(s) depending on what $route contains.
Here's some pseudo code:
navbar.vue
<template>
<div class="navbar">
<div class="navbar-left>
APPNAME
</div>
<div v-if="name === 'landing'">
...
</div>
<div v-else-if="name === 'room'">
...
</div>
</div>
</template>
App.vue
<template>
<div id="app">
<NavBar :name="$route.name"/>
<main>
<router-view/>
</main>
</div>
</template>
In this example, the NavBar component is very rigid, and doesn't really lend itself to much reuse. However, it does encapsulate all the relevant code relating to the nav bar.
2. Extensible NavBar with slots
In this approach, the NavBar only provides the bare-minimum to create a nav bar. The rest of the route-specific elements are to be filled in by the views.
navbar.vue
<template>
<div class="navbar">
<div class="navbar-left">
<div class="navbar-brand">
APPNAME
</div>
<slot name="left"></slot>
</div>
<div class="navbar-right">
<slot name="right"></slot>
</div>
</div>
</template>
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
landing.vue
<template>
<div>
<header>
<NavBar>
<template slot="right">
<span>
<div class="navbar-item">
<div class="buttons">
<button class="button" #click="...">Start Watching</button>
</div>
</div>
</span>
</template>
</NavBar>
</header>
<main>
...
</main>
</div>
</template>
This approach has a bit of repetition in terms of DOM elements, but gives you an extremely flexible NavBar that can be customized by each view.
The approach you want to use depends on what is important to you.
If strict encapsulation is what you want, then you may want to use approach 1, as all of the NavBar-related code is contained within a single file.
However, if you believe that there is a potential for reuse, or if you would like all view-related code to live in one place, then it makes sense to use slots instead and extend the NavBar as required by each view.
I use a breadcrumb to achieve a similar thing. Just an idea but Vue router allows you to add meta data to the current route which you always have access to
router.js
path: '/add',
name: 'add',
component: () => import(/* webpackChunkName: "add" */ '../../views/Add.vue'),
meta: {
breadCrumb: [
{ name: 'Add New' }
]
},
Notice the meta object attached to the route.. this will be used to describe the current view.
Breadcrumb.vue component
<template>
<div class="breadcrumb">
<ul class="d-flex m-0 p-0"
<li
v-for="(breadcrumb, idx) in breadcrumbList"
:key="idx">
{{ breadcrumb.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Breadcrumb',
data () {
return {
breadcrumbList: []
}
},
mounted () { this.updateList() },
watch: { '$route' () { this.updateList() } },
methods: {
routeTo (pRouteTo) {
if (this.breadcrumbList[pRouteTo].link) this.$router.push(this.breadcrumbList[pRouteTo].link)
},
updateList () { this.breadcrumbList = this.$route.meta.breadCrumb },
formatPath(path) {
const newPath = path.replace(/\//g, " > ")
return newPath
}
}
}
</script>
And then you can import the breadcrumb into your navbar or where ever you would like to place it
<Breadcrumb class="breadcrumb" />
import Breadcrumb from '#/components/Breadcrumb.vue'
components: {Breadcrumb}
So basically the breadcrumb will always watch your current route and change the data based on the meta data you provide in your router.js file
You can access to router name like this:
<div v-if="this.$route.name == 'home'">
<HeaderTransparent />
</div>
<div v-else>
<HeaderWhite />
</div>

Set up router for subpages in Vue.js

I wounder how I best set up the router in Vue.js for handling ”subpages”. For example I got a navbar that routes to different pages. From one of these pages I want to have links to subpages. How do I best set this up?
I have done like this so far:
App.js
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view />
</div>
</template>
Then I set up my router:
export default new Router({
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/about",
name: "about",
component: About,
children: [
{
path: "/child1",
name: "child1",
component: Child1
}
]
}
]
})
And my About.vue where I provide the link to Child1
<template>
<div class="about">
<h1>This is an about page</h1>
<router-link to="/child1">Child1</router-link>
<router-view></router-view>
</div>
</template>
And finally my Child1.vue
<template>
<div class="child1">
<p>My message</p>
</div>
</template>
My problem is that the link to Child1 is displayed both on the About page and on Child1 page. I just want to display it on the about page and only the content from the Child1 on the Child1 page
How is the best practice of setting up things like this?
Thanks
My problem is that the link to Child1 is displayed both on the About page and on Child1 page. I just want to display it on the about page
Just to clarify what's happening here: the link to Child1 is always visible within the About component even if child routes are active, but you don't want to show the link when the child route is active.
Way 1
You can provide fallback content to <router-view> when there is no matching route (i.e. when no child route is active). This would be a good opportunity to show the link.
<template>
<div class="about">
<h1>This is an about page</h1>
<router-view>
<router-link to="/child1">Child1</router-link>
</router-view>
</div>
</template>
Way 2
The above solution may not work if your template is more complicated and if you want to situate the link elsewhere in the template.
So you'll have to manually control the visibility of the link by using v-if so that it is only visible when the child route is not active.
<template>
<div class="about">
<h1>This is an about page</h1>
<!-- Show only when no child routes are active -->
<router-link v-if="$route.name === 'about'" to="/child1">Child1</router-link>
<!-- Or, do not show when Child1 route is active -->
<router-link v-if="$route.name !== 'child1'" to="/child1">Child1</router-link>
<router-view></router-view>
</div>
</template>

vue-router: How to use view-router in more than one element?

A very simple example for using a vue-router in template is the following code:
<template>
<div id="app">
<router-view/>
</div>
</template>
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
what I understand is that the content of router-view will be switched by the relevant component to the path.
However, if I have a template with more than one element affected by router. For example,
<template>
<div id="app">
<h1> header </h1>
<router-view 1/>
<h1> Inner </h1>
<router-view 2/>
<h1> Footer </h1>
</div>
</template>
and let's say that router-view 1 and router-view 2 both can get different components based on the path.
In this case, how would you recommend me to use router?
Based on official doc, you have to use named-views.
Like that, you can have multiple router-view rendering differents components for the same path.
With your example, it becomes :
<template>
<div id="app">
<h1> header </h1>
<router-view /> // this will be the default
<h1> Inner </h1>
<router-view name="inner"/>
<h1> Footer </h1>
</div>
</template>
and your router will look like :
// Don't forget your imports
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: HeaderComponent, // Will render in default router-view
inner: InnerComponent // Will render in router-view named "inner"
}
}
]
})
More complex layouts are also describes in the official doc.