Vue router layouts

Addition layout systems into an existing project on Vue using Vue router.

In this example we will consider a project on Vue2 options api with use Vue Router v3. In theory, I don’t see any particular problems in using it for Vue3 With composition api.

Repository with implementation

Task: speed up the creation of standard pages by means of layout systems with the ability to transfer the name of the required template for a page, approximately similar to the implementation in nuxt3.

Create a directory for our templates in the root of the project @/layouts

We design the main component in which we will define the required template:

// @/layouts/index.vue
<template>
	<component :is="layoutComponent">
</template>

<script>
	import "Наш компонент с лейаутом"

	export default {
		name: "layout",
		components: { Наши компоненты layout },
		computed: {
			// генерируем имя нужного нам компонента заранее импортированного
			// не делаю динамического импорта тк компонентов с лейаутами 
			// как правило не много, при необходимости можно добавить 
	    layoutComponent() {
	      return `layout-${this.layoutType}`;
	    },
	    
	    routeHaveLayoutType() {
	      return this.$route.meta && 'layout' in this.$route.meta;
	    },
	    
	    // Определяем каой layout нам нужно использовать
	    // для указания используем $router.meta, по умолчанию default
	    // что будет равно нашему компоненту <layout-default />
	    layoutType() {
	      if (this.routeHaveLayoutType) {
	        return this.$route.meta.layout;
	      }
	      return "default";
	    },
		}
	}
</script>

Create a directory nearby for future templates @/layouts/components

We create default template for components:

// @/layouts/components/default.vue
<template>
  // Ваша логика для default layout
  // ключающая в себя router-view

  // Например
  <div class="container">
		  <header>My header</header>
		  <main>
			  <router-view />
		  </main>
		  <footer>My footer</footer>
  </div>
</template>

Create an additional custom template tabswhich will implement the page in the form of tabs, where tabs are a link to the page:

// @/layouts/components/tabs
<template>
  <div class="container">
    <div class="tabs">
      <RouterLink
          v-for="(tab, index) in tabs"
          :key="index"
          :to="tab.path"
          class="tabs__item"
          active-class="is-active"
      >
        {{ tab.title }}
      </RouterLink>
    </div>
    <div class="tabs__content">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  name: "LayoutTabs",
  props: {
    tabs: {
      type: Array,
      default: () => [],
    },
  },
}
</script>

Create another additional template extra for demonstration:

// @/layouts/components/extra
<template>
  <div class="container">
    <heading>Extra layout</heading>
    <router-view></router-view>
  </div>
</template>

At the root of the project, in my case it is App.vue in the root of the project, we use the previously created component instead of the existing one

<template>
  <div id="app">
    // Было
	  // <router-view />
  
	  // Стало
    <layout />
  </div>
</template>

Creating a basic structure routeraccording to the documentation vue-routerin the application or use the existing one:

const routes = [
	{
		path: "/",
		component: () => import(Ваш компонент страницы),
		...
	}
	...
]

To determine the required layout we use router.meta:

{
	path: "/",
	components: () => import(Ваш компонент),
	meta: {
		layout: "extra",
	},
}

To define tabs, you can also use router.meta in parent routefor this we will need to update the code in ours a little @/layouts/index:

<template>
  <component :is="layoutComponent" :tabs="layoutTabs" :parentRoute="parentRoute" />
</template>

<script>
import LayoutDefault from "./components/default.vue";
import LayoutExtra from "./components/extra.vue";
import LayoutTabs from "./components/tabs.vue";

export default {
  name: "Layout",
  components: {
    LayoutDefault,
    LayoutExtra,
    LayoutTabs,
  },
  computed: {
    routeHaveLayoutType() {
      return this.$route.meta && 'layout' in this.$route.meta;
    },
    parentRouteHaveLayoutType() {
      return this.parentRoute && this.parentRoute.meta && 'layout' in this.parentRoute.meta;
    },
    routeHaveLayoutTabs() {
      return this.$route.meta && 'layoutTabs' in this.$route.meta;
    },
    parentRouteHaveLayoutTabs() {
      return this.parentRoute && this.parentRoute.meta && 'layoutTabs' in this.parentRoute.meta;
    },
    layoutType() {
      // проверяем содержит ли текущий route layout
      if (this.routeHaveLayoutType) {
        return this.$route.meta.layout;
      }
      
      // проверяем содержит ли родительский route layout
      if (this.parentRouteHaveLayoutType) {
        return this.parentRoute.meta.layout;
      }
      
      // возвращаем дефолтный по умолчанию если ничего не нашли
      return "default";
    },
    layoutComponent() {
      return `layout-${this.layoutType}`;
    },
    layoutTabs() {
      if (this.layoutType === 'tabs') {
	      // если в текущем route указаны табы для layout то используем их
        if (this.routeHaveLayoutTabs) {
          return this.$route.meta.layoutTabs;
        }
        
        // пробуем найти перечисление табов в родительском route
        if (this.parentRouteHaveLayoutTabs) {
          return this.parentRoute.meta.layoutTabs;
        }
        return [];
      }
      return [];
    },
    
    // находим родительский route
    parentRoute() {
      const currentInMatched = this.$route.matched.find(i => i.regex.test(this.$route.path));
      return currentInMatched && currentInMatched.parent;
    }
  },
}
</script>

If the project is completely on Vue And Vue-routeryou can get attached to children and from them form the structure of child tabs, for this we update computed in the component :

{
	computed: {
		...
		
		
    layoutTabs() {
      // при использовании parentRoute.children как табы
      if (this.parentChildren) {
        return this.parentChildren.map((i, k) => {
          return {
            path: i.path,
            title: 'tabTitle' in i.meta && i.meta.tabTitle || `tab${k}`,
          }
        });
      }

      // при передачи табов как meta.layoutTabs
      if (this.layoutType === 'tabs') {
        if (this.routeHaveLayoutTabs) {
          return this.$route.meta.layoutTabs;
        }
        if (this.parentRouteHaveLayoutTabs) {
          return this.parentRoute.meta.layoutTabs;
        }
        return [];
      }
      return [];
    },
    
    // находим children текущего route
    parentChildren() {
      if (!this.parentRoute) return [];
      const parentRoute = this.$router.options.routes.find(i => this.parentRoute.regex.test(i.path) );
      return parentRoute && parentRoute.children;
    },
	}
}

When using your own route in child routes layout and the presence of its own subsidiaries route we format the code in the following format:

[
	{
		path: "default",
		component: () => import("компонент страницы"),
	},
	{
		path: "tabs",
		redirect: "/tabs/red",
		meta: {
			layout: "tabs",
			layoutTabs: массив табов // если берем табы из meta
		},
		children: [
			{
				path: "red",
				component: () => import("компонент заглушка который состоит из одного <router-view />"),
				// кейс с еще более глубокой вложенностью и самостоятельным компонентом для родительского route "red"
				children: [
					{
						// для корневого роута "red"
						path: "",
						component: () => import("ваш компонент red страницы"),
					},
					{
						path: "orange",
						component: () => import("ваш компонент oragne страницы"),
						meta: {
							layout: "extra",
						},
					},
					{
						path: "tomato",
						component: () => import("ваш компонент tomato страницы"),
						meta: {
							layout: "extra",
						},
					}
				],
			},
			{
				path: "green"
				component: () => import("ваш компонент green страницы"),
			},
			{
				path: "blue",
				component: () => import("ваш компонент blue страницы"),
			}
		]
	}
]

The full implementation of the example can be seen at Github.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *