Vue.js 3 - Trying to build a system with 2 layouts - vue.js

I am a beginner with vue.js (3)
I try to build a system with 2 layouts :
1 for a connected user
1 for a not connected user
In my router/index.js, I add a meta for each route :
const routes = [
{
path: '/',
name: 'Home',
meta: { layout: 'layout-connected' },
component: Home
},
{
path: '/myspace',
name: 'MySpace',
meta: { auth: true },
component: MySpace
}
]
In my App.vue, I decide of which layout to use like that (see the ":is="layout"):
<template>
<div id="app">
<component :is="layout">
<router-view/>
</component>
</div>
</template>
<script>
const defaultLayout = 'layout-not-connected'
export default {
computed: {
layout () {
console.log(this.$route.meta.layout)
return (this.$route.meta.layout || defaultLayout)
}
},
And at least , in my layout I have :
<template>
<div class="container-fluid">
<div class="row essai">
<h1>layout non connected</h1>
<slot />
</div>
</div>
</template>
When I console.log which route to apply, it works fine : I have the correct layout in the console.
BUT I never see the layout (for example the tag). Only the component.
Do I have understood fine the concept ? What can be my errors ?
Thanks

The layouts are components which should be registered globally in main.js using :
app.component('layout-name',theLayoutComponent)
or locally in components option :
<template>
<div id="app">
<component :is="layout">
<router-view/>
</component>
</div>
</template>
<script>
const defaultLayout = 'layout-not-connected'
import LayoutConnected from 'path/to/LayoutConnectedComponent'
import DefaultLayout from 'path/to/DefaultLayout Component'
export default {
components:{
DefaultLayout,LayoutConnected
},
computed: {
layout () {
console.log(this.$route.meta.layout)
return (this.$route.meta.layout || defaultLayout)
}
},

Related

Dynamic import of SVG icon components

I'm trying to import SVG icons for each item in a v-for loop, with the filename changing depending on the item's id. The icons are loading, but I get the following error for each icon imported.
Is there a better way to approach this?
Uncaught (in promise) TypeError: Failed to resolve module specifier '~/assets/img/flags/ar.svg'
<template>
<NavigationItem v-for="item in topCountries">
<template #icon>
<component :is="getIcon(item.id)" />
</template>
<NavigationItem />
</template>
<script setup>
const getIcon = (id) => defineAsyncComponent(() =>
import(`~/assets/img/flags/${id}.svg`));
</script>
You can have a look at https://nuxt.com/modules/nuxt-svgo module.
This module allows to import SVG.
npm i --save nuxt-svgo
Add it as a module dependency in your nuxt.config file
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['nuxt-svgo']
})
Import SVG icons as follow:
<script setup lang="ts">
const getIcon = (id: string) => defineAsyncComponent(() => import(`#/assets/svg/${id}.svg`));
</script>
<template>
<div v-for="item in ['icon1', 'icon2']">
<component :is="getIcon(item)" />
</div>
</template>
Note that if you use Typescript, you will have to create a custom.d.ts file to fix import error
// custom.d.ts
declare module '*.svg' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
calls each icon from the data. uses font awesome icons. you can also add svgs between the i tags
<template>
<ul>
<!-- list rendering -->
<li v-for="item in items">
<span class="icon">
<i :class="[faClass(item.icon)]"
aria-hidden="true"></i>
</span>
</li>
</ul>
</template>
<script>
export default {
name: "navbarMobile",
data() {
return {
//listItems
items: [
{
icon: 'home',
},
{
icon: 'wrench',
},
{
icon: 'project-diagram',
},
{
icon: 'cogs',
},
{
icon: 'phone',
}
]
}
},
methods: {
faClass(icon) {
return `fa fa-${icon}`;
}
}
}
</script>
Us the component name instead of the component path. Also, don't forget to import SVG components and add ?inline at the end of the name.
<template>
<NavigationItem v-for="item in topCountries">
<template #icon>
<component :is="item.icon" />
</template>
<NavigationItem />
</template>
<script setup>
import Eye from '~/assets/img/flags/Eye.svg?inline';
import Balls from '~/assets/img/flags/Balls.svg?inline';
const topCountries = [
{ icon: 'Eye' },
{ icon: 'Balls' }
]
</script>

VUE JS: How to keep same structure in whole project but different header and footer for each page

I am using router-view. In App.vue I want this structure so in every other pages I can have different header, footer but overall structure:
<router-view>
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
</router-view>
In home page component I want to do something like this:
<slot name="header">header content</slot>
<slot name="footer">footer content</slot>
Othercontent here....
In about us page component I want to do something like this:
<slot name="header">about header content</slot>
<slot name="footer">about footer content</slot>
Othercontent here....
how can I achive this?
The solution goes around layouts and sub-pages where layouts are parents and related pages placed as children.
Consider these two layouts:
// src/layouts/PublicLayout.vue
<template>
<section class="public-layout">
<router-view/>
</section>
</template>
<script>
export default {
name: 'PublicLayout',
};
</script>
and
// src/layouts/MainLayout.vue
<template>
<section class="main-layout">
<!-- header -->
<router-view/>
<!-- footer -->
</section>
</template>
<script>
export default {
name: 'MainLayout',
};
</script>
Now, you can tune routes in your flavor:
// src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
const publicRoutes = [
{
path: '/public',
component: () => import('layouts/PublicLayout.vue'),
children: [
{
path: 'login',
name: 'Login',
component: () => import('pages/ULogin.vue'),
},
{
path: 'sign-up',
name: 'SignUp',
component: () => import('pages/USignUp.vue'),
},
],
},
];
const mainRoutes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('pages/UHome.vue'),
},
{
path: 'profile',
name: 'Profile',
component: () => import('pages/Profile.vue'),
},
],
},
];
const Router = new VueRouter({
routes: mainRoutes.concat(publicRoutes),
});
// snippet skipped
Here, MainLayout and PublicLayout play the structural role while other pages are placed as their children. By navigating to each page (e.g. from /login to /profile) the proper layout will get loaded as well.
<your-base-layout>
<slot #header>about header content</slot>
<slot #footer>about footer content</slot>
</your-base-layout>
its called "Named Slot"

Seperate Dashbord and Forum Component From App component in vuejs

Is there a way to mount multiple components on a single vue instance.
I have my admin dashboard and a forum page and i don't want header and navigation to show up on these pages.
Here's what I've tried:
import App from "./App.vue";
import Admin from "./Admin.vue";
import Forum from "./Forum.vue";
const app = new Vue({
router,
store,
components: {
App, Admin, Forum
}
}).$mount("#app");
Then in my App.vue, I have other child components
<template>
<div>
<div class="general-page">
<AppHeader></AppHeader>
<transition name="fade">
<router-view></router-view>
</transition>
<AppFooter></AppFooter>
</div>
</div>
</template>
<script>
import AppHeader from "./components/AppHeader";
import Login from "./components/Login.vue";
import Register from "./components/Register.vue";
import AppFooter from "./components/AppFooter.vue";
export default {
components: {
AppHeader,
Login,
Register,
AppFooter
}
};
</script>
In Forum.vue
<template>
<div>
<div class="forum-page">
<ForumHeader></ForumHeader>
<transition name="fade">
<router-view></router-view>
</transition>
<ForumFooter></ForumFooter>
</div>
</div>
</template>
<script>
import ForumHeader from "./components/ForumHeader";
import ForumFooter from "./components/ForumFooter.vue";
export default {
components: {
ForumHeader,
ForumFooter
}
};
</script>
Admin.vue
<template>
<div>
<div class="admin-page">
<AdminHeader></AdminHeader>
<transition name="fade">
<router-view></router-view>
</transition>
<AdminFooter></AdminFooter>
</div>
</div>
</template>
<script>
import AdminHeader from "./components/AdminHeader";
import AdminFooter from "./components/AdminFooter.vue";
export default {
components: {
AdminHeader,
AdminFooter
}
};
</script>
Routes for Forum and Admin
{
path: '/admin',
name: 'Admin',
component: Admin,
meta: {
requiresAuth: true
},
children: [
{
path: '',
name: 'Profile',
component: Profile
},
{
path: 'uploads',
name: 'Uploads',
component: Uploads,
meta: {
requiresCreatorAccess: true
}
},
{
path: 'add-post',
name: 'AddPost',
component: AddPost,
meta: {
requiresCreatorAccess: true
}
}
]
},
{
path: '/forum',
name: 'Forum',
component: Forum,
children: [
{
path: '',
name: 'Channel',
component: Channel
},
{
path: 'threads',
name: 'Threads',
component: Threads
},
{
path: 'topic',
name: 'Topic',
component: Topic
}
]
},
How do I dynamically go to each route and mount each component on el: #app ?
Without changing any routing and template structure, you could use CSS to hide the app header, footer.
Another option may be to v-if the app header,footer to not render when on those routes using something like $router.currentRoute for matching.
CSS
/*
Assuming app header and footer have an id attribute
Change to your needs
*/
#app-header,
#app-footer {
display: none;
}
v-if on currentRoute
We have to do a few of things here.
Create a data variable (showMe: true)
Create a method (evaluateShowMe)
Create a watcher for the route ('$route'()) Be aware of the quotes!
Note: Feel free to rename the variable and function to suit your needs.
We need to watch $route because this is outside of a <router-view/> so we need to do this dynamically so the variable performs the evaluator function every time the route changes.
App.vue:
<template>
<div>
<div class="general-page">
<AppHeader
v-if="showMe"
></AppHeader>
<transition name="fade">
<router-view></router-view>
</transition>
<AppFooter
v-if="showMe"
></AppFooter>
</div>
</div>
</template>
<script>
import AppHeader from "./components/AppHeader";
import Login from "./components/Login.vue";
import Register from "./components/Register.vue";
import AppFooter from "./components/AppFooter.vue";
export default {
components: {
AppHeader,
Login,
Register,
AppFooter
},
data() {
return {
showMe: true
}
},
methods: {
evaluateShowMe() {
// Get the substring of the path between first and second slash
// This will allow to include any child pathing
// NOTE: In my test the first index ([0]) was empty so used one ([1]) for the `filter`
const entryPath = this.$router.currentRoute.path.split('/').filter((x,i) => i === 1);
// We want to exclude the following paths i.e. hide when on these
// There should only be one item in the array so we extract with `[0]`
return (entryPath[0] !== 'admin' || entryPath[0] !== 'forum');
}
},
watch: {
'$route'() {
this.showMe = this.evaluateShowMe();
}
}
};
</script>

Vue-cli change object value globally

I have this code in file app.vue :
<template>
<div id="app">
<button v-on:click="component = 'login'">aa</button>
<component v-bind:is="component"></component>
</div>
</template>
<script>
import acceuil from './components/acceuil.vue'
import login from './components/login.vue'
export default {
name: 'app',
components: {
acceuil,
login
},
data(){
return {
component: 'acceuil'
}
}
}
</script>
How can I toggle between acceuil/login in component from a different vue file ?
You need to pass the imported dependency (the object or the name of the component as a string) to v-bind:is. You can do this by returning it in a computed function and pass it to a computed property, which you then can use in the template.
<template>
<div id="app">
<button v-on:click="isLogin = true">Show Login</button>
<component v-bind:is="currentComponent"></component>
</div>
</template>
<script>
import acceuil from './components/acceuil.vue';
import login from './components/login.vue';
export default {
name: 'app',
data () {
return {
isLogin: false
};
},
computed: {
currentComponent () {
return this.isLogin ? login : acceuil;
}
},
};
</script>
See also the documentation of dynamic components in the official docs.

vuejs application with different layouts (e.g. login layout, page layout, signup etc.)

I generated a project using vue-cli. I see project has one App.vue which is kinda main layout of the app - if I'm not mistaken. Here I put my basic HTML layout and <router-view></router-view>. Now the issue is that I need completely different layout for login (different wrappers , body has different classes) but I can't change it since App.vue has template which is kinda "fixed" as a layout. How to approach this issue? Is there recommended way?
Should I create new component that represents layout so in that case my App.vue template would only have <router-view></router-view> and then LoginLayout.vue would be included into it?
I think I found a solution. The approach has App.vue containing only <router-view></router-view> and then including different components that represent layout (if needed, containing <router-view> and subroutes). I found a project using it in that way here.
I think it keeps things more clean and organised. IMHO, hiding all elements which define layout structure (all the divs) would be too messy - especially for bigger apps.
A nice solution for this is using slots
First create your "layout component"
src/components/layouts/basic.vue
<template>
<div class="basic-layout">
<header>[Company logo]</header>
<hr>
<slot/>
<hr>
<footer>
Made with ❤ at Acme
</footer>
</div>
</template>
Then use it in another component:
<template>
<layout-basic>
<p>Hello world!</p>
</layout-basic>
</template>
<script>
import LayoutBasic from '#/components/layouts/basic'
export default {
components: {
LayoutBasic
}
}
</script>
"Hello world" will appear where the <slot/> tag is.
You can also have multiple slots with names, see the complete docs.
I find another solution by using router meta. I just have a few components need another layout.
I added a plainLayout meta key in src/router/index.js.
export default new Router({
mode: 'history',
linkExactActiveClass: 'app-head-menu--active',
routes: [
{
path: '/',
component: Features,
},
{
path: '/comics/:id',
component: Comic,
props: true,
},
{
path: '/comics/:comic_id/:chapter_index',
component: Chapter,
props: true,
meta: {
plainLayout: true,
},
},
],
});
Then render layout conditionally with playLayout in src/App.vue.
<template>
<div>
<div v-if="!$route.meta.plainLayout">
<div class="app-head">
</div>
<div class="app-content">
<router-view/>
</div>
</div>
<div v-if="$route.meta.plainLayout">
<router-view/>
</div>
</div>
</template>
<script>
export default {
name: 'app',
};
</script>
See a demo project here.
Utilizing Routes, and in particular, children routes is a great way to approach having common layouts in Vue.
All of this code is utilizing Vue 2.x
Start by having a really simple vue component called App that has no layout.
app.vue
<template>
<router-view></router-view>
</template>
Then have a Routes file that you'll bring into your Vue instance.
Routes.(ts|js)
import Vue from 'vue'
import VueRouter from 'vue-router'
const NotFoundComponent = () => import('./components/global/notfound.vue')
const Login = () => import('./components/account/login.vue')
const Catalog = () => import('./components/catalog/catalog.vue')
export default new VueRouter({
mode: 'history',
linkActiveClass: 'is-active',
routes: [
//Account
{ path: '/account', component: () => import('./components/account/layout.vue'),
children: [
{ path: '', component: Login },
{ path: 'login', component: Login, alias: '/login' },
{ path: 'logout',
beforeEnter (to: any, from: any, next: any) {
//do logout logic
next('/');
}
},
{ path: 'register', component: () => import('./components/account/register.vue') }
]
},
//Catalog (last because want NotFound to use catalog's layout)
{ path: '/', component: () => import('./components/catalog/layout.vue'),
children: [
{ path: '', component: Catalog },
{ path: 'catalog', component: Catalog },
{ path: 'category/:id', component: () => import('./components/catalog/category.vue') },
{ path: 'product', component: () => import('./components/catalog/product.vue') },
{ path: 'search', component: () => import(`./components/catalog/search.vue`)} ,
{ path: 'basket', component: () => import(`./components/catalog/basket.vue`)} ,
{ path: '*', component: NotFoundComponent }
]
}
]
})
The code is using lazy loading (with webpack) so don't let the () => import(...) throw you. It could have just been import(...) if you wanted eager loading.
The important bit is the children routes. So we set the main path of /account to utilize the /components/account/layout.vue but then the very first two children specify the main content vue (Login). I chose to do it this way because if someone just browses to /account I want to greet them with the login screen. It may be appropriate for your app that /account would be a landing page where they could check the order history, change passwords, etc...
I did the same thing for catalog... / and /catalog both load the catalog/layout with the /catalog/catalog file.
Also notice that if you don't like the idea of having "subfolders" (i.e. account/login instead of just /login) then you can have aliases as I show in the login.
By adding , alias: '/login' it means users can browse to /login even though the actual route is /account/login.
That is the key to the whole thing, but just to try and make the example complete...
Here is my boot file which hooks up my app.vue and routes:
boot.(ts|js)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import App from './components/app.vue';
import router from './routes';
new Vue({
el: '#app',
router,
render: h => h(App)
});
I created a layout.vue file for each of my main sections of my app (account, catalog, etc).
account/layout.vue
<template>
<div>
<cc-header></cc-header>
<div class="container">
<main>
<router-view></router-view>
</main>
<aside>
</aside>
</div>
<cc-footer></cc-footer>
</div>
</template>
<script lang="ts">
import ccHeader from "../common/cc-header.vue"
import ccFooter from "../common/cc-footer.vue"
export default {
components: {
ccHeader,
ccFooter
}
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
}
main {
flex: 3;
order: 2;
}
aside {
flex: 1;
order: 1;
}
</style>
And the layout for catalog...
catalog/layout.vue
<template>
<div>
<cc-header></cc-header>
<div class="catalog-container">
<main class="catalog">
<router-view></router-view>
</main>
<cc-categories></cc-categories>
</div>
<cc-footer></cc-footer>
</div>
</template>
<script lang="ts">
import ccHeader from "../common/cc-header.vue"
import ccFooter from "../common/cc-footer.vue"
import ccCategories from "./cc-categories.vue"
export default {
components: {
ccCategories,
ccHeader,
ccFooter
},
data : function() : any {
return {
search: ''
}
},
}
</script>
<style lang="scss" scoped>
.catalog-container {
display: flex;
}
.category-nav {
flex: 1;
order: 1;
}
.catalog {
flex: 3;
order: 2;
}
</style>
Both layouts use common components like header and footer, but they don't need to. The catalog layout has categories in the side nav, while the account layout doesn't. I put my common components under components/common.
common/footer.vue
<template>
<div>
<hr />
<footer>
<div class="footer-copyright">
<div>© Copyright {{year}} GlobalCove Technologies, LLC</div>
<div>All rights reserved. Powered by CoveCommerce.</div>
</div>
</footer>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.component('cc-footer', {
data : function() : any {
return {
year: new Date().getFullYear()
}
},
})
</script>
<style lang="scss">
</style>
Overall file structure
src/
boot.ts
routes.ts
components/
app.vue
catalog/
layout.vue
catalog.vue
category.vue
product.vue
search.vue
basket.vue
account/
layout.vue
login.vue
register.vue
global/
notfound.vue
common/
cc-header.vue
cc-footer.vue
The combination of routes, a plain app.vue, and specific layout files, along with common components should get you to where you want to be.
I route my apps through a layout. Eg login requires no structure, just the login component, but other pages require, header footer etc, so here is an example of how I do this in my routes:
// application routes
'/secure': {
name: 'secure',
component: require('../components/layouts/default'),
subRoutes: {
'/home': {
name: 'home',
component: require('../components/home/index')
}
}
}
//- public routes
'/insecure': {
name: 'insecure',
component: require('../components/layouts/full-bleed'),
subRoutes: {
'/login': {
name: 'login',
component: require('../components/session/login')
}
}
}
Both of these layout templates have a router-view tag, so you can them build your layouts as you require for different parts of the app.
I dynamically check the route globally on App.vue and use that to determine what needs to be shown.
App.vue
<template>
<div id="app">
<top :show="show" v-if="show.header"></top>
<main>
<router-view></router-view>
</main>
<bottom v-if="show.footer"></bottom>
</div>
</template>
<script>
export default {
mounted: function() {
if(window.location.hash == "#/" || window.location.hash.indexOf('route')) {
vm.show.header = true
vm.show.footer = true
vm.show.slideNav = true
}
}
watch: {
$route: function() {
// Control the Nav when the route changes
if(window.location.hash == "#/" || window.location.hash.indexOf('route')) {
vm.show.header = true
vm.show.footer = true
vm.show.slideNav = true
}
}
}
}
</script>
That way I'm also able to control what's shown in the top and bottom navs through props.
Hope this helps!
I don't know about any "recommended way" but my app is structured like this:
App.vue - just top menu bar (which is not rendered when user is not authenticated) and <router-view></router-view> for each component (page)
So every page could have totally different layouts.
Comment to the accepted answer
Kind of disagree with this. Had the same issue and this answer confused me. Basically when you have a component which you'd like to reuse everywhere (e.g. footer, header) in your application then you can keep it in the App.vue. It was my case, I wanted to have footer and header in every page, finding this answer put me into the wrong direction, but you can do it and it does works, for example App.vue:
<template>
<div id="app">
<app-header />
<router-view />
<app-footer />
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
// Imports related with custom logic.
import FooterComponent from "#/components/Footer.vue";
import HeaderComponent from "#/components/Header.vue";
#Component({
components: {
"app-footer": FooterComponent,
"app-header": HeaderComponent
}
})
export default class App extends Vue {}
</script>
<style lang="scss" scoped>
</style>
Footer.vue (located in components/Footer.vue):
<template>
<div>
<footer>
<div>© {{ year }} MyCompany</div>
</footer>
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
#Component({})
export default class FooterComponent extends Vue {
public year = new Date().getFullYear();
}
</script>
<style lang="scss" scoped>
</style>
Header.vue (located in components/Header.vue):
<template>
<div>
<header>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/contact">Contact</router-link>
</header>
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
#Component({})
export default class HeaderComponent extends Vue {}
</script>
<style lang="scss" scoped>
</style>