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.