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

vue.jsVue Router

vue.js Problem Overview


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?

vue.js Solutions


Solution 1 - vue.js

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.

Solution 2 - vue.js

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.

Solution 3 - vue.js

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.

Solution 4 - vue.js

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.

Solution 5 - vue.js

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.

Solution 6 - vue.js

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!

Solution 7 - vue.js

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.

Solution 8 - vue.js

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>&copy; {{ 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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser2343398View Question on Stackoverflow
Solution 1 - vue.jsuser2343398View Answer on Stackoverflow
Solution 2 - vue.jslipsumarView Answer on Stackoverflow
Solution 3 - vue.jslingcengView Answer on Stackoverflow
Solution 4 - vue.jsChad CarterView Answer on Stackoverflow
Solution 5 - vue.jsTremendus AppsView Answer on Stackoverflow
Solution 6 - vue.jsericgmView Answer on Stackoverflow
Solution 7 - vue.jslukpepView Answer on Stackoverflow
Solution 8 - vue.jsDaniel DanieleckiView Answer on Stackoverflow