I have struggling to get the vue router to work properly, despite the fact that I have tried multiples different ways and read through the documentation, there seems to be something that I don't understand
I know app component is a bit messy, with router view covering router link and there is the component. its because I have tried it all, included the router-view first then it showed everything from my firstUser component, then tried the link, it worked, but it did not go to that page seperately,
I need to make it so that when clicked it should show the firstUser page only, Please help, much appreciated
//This is my app component
<template>
<div id="app">
<h1>Welcome</h1>
<router-view>
<router-link to="firstUser"> <first-user/> Register </router-link>
</router-view>
<div>
<h2>Returning user</h2>
<returning-user />
</div>
<div>
<h2>Registered User</h2>
<registered-user />
</div>
</div>
</template>
<script>
import firstUser from "#/components/firstUser.vue";
import returningUser from "#/components/returningUser.vue";
import registeredUser from "#/components/registeredUser.vue";
export default {
components: { returningUser, registeredUser, firstUser },
data() {
return {};
},
};
</script>
<style>
#app {
padding: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
input:focus {
outline: none;
}
</style>
//this is my fisrtUser component
<template>
<div>
<form class="form" #submit.prevent="registerUser">
<h4 class="">Register</h4>
<my-input
v-model="this.firstName"
type="text"
class="input"
placeholder="First Name"
/>
<my-input
v-model="this.lastName"
type="text"
class="input"
placeholder="Last Name"
/>
<myButton #click="registerUser"> Create </myButton>
</form>
</div>
</template>
<script>
import myInput from "./UI/myInput.vue";
import myButton from "./UI/myButton.vue";
export default {
name: "firstUser",
components: { myInput, myButton },
data() {
return {
user: [
{
firstName: "",
lastName: "",
},
],
users: [],
};
},
methods: {
registerUser() {
const newUser = {
firstName: this.firstName,
lastName: this.lastName,
};
this.users.push(newUser);
console.log(newUser);
},
},
};
</script>
<style>
</style>
///then this is the index.js from router
import { createRouter, createWebHistory } from 'vue-router';
import firstUser from '#/views/firstUser';
const routes = [{
path: '/firstUser',
name: 'firstUser',
component: firstUser
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
and
It is not entirely clear what the firstUsers are. I will assume what you did is intentional and there are two firstUsers (one view and one component).
Specify the to prop of the router-link component as you would the href attribute of a link/anchor <a> element. That is, the route needs to begin with a slash.
<router-link to="/firstUser">
Alternatively, you might want to reduce the coupling by declaring the link by route name instead. This way, you can change the route to whatever you want without having to update your link. This can be done by using v-bind to tell Vue to parse the prop contents, and by using different syntax.
<router-link :to="{ name: 'firstUser' }">
See the Vue Router documentation.
Related
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>
Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...
how do you affect multiple (in this case just 2) children components owned by two different parent components when an action is triggered by one of the children components?
For example I have a component, lets call it <component-one/>. Inside this component I have something like below:
<div #mouseover="hover=true" #mouseleave="hover=false" :class="setColour">
<div class="icon-wrapper commercial-layout position-relative">
<u-button icon color="transparent" #click="toggleCommercials">
<u-icon :icon="icon" color="white"/>
</u-button>
<small class="commercial-ind">COMMERCIAL ADS</small>
<div class="commercial-layout commercial-ind">{{hide}}</div>
</div>
</div>
computed: {
setColour () {
if (this.hover) {
return 'bg-danger'
}
else if (this.commercials) {
return 'bg-primary'
}
else if (!this.commercials) {
return 'bg-secondary'
}
},
watch: {
setColour: function(val) {
console.log("val",val)
}
}
But somewhere else in the code base I have two other components, lets call them <component-two/> and <component-three/>. Inside those components I use component-one. When I push on the button from component-two I want the same effect to also be triggered in component-three, and vice versa, but I'm not quite sure how to achieve that.
Currently both component-two and component-three just have component-one. I've tried adding a watch in component-one but it doesn't really do anything other than capturing changes to the setColour computed property. (I naively thought by capturing the change, all places where component-one is used will get updated)
I'm not sure I totally understand your specific component relationships, but in general I recommend using Vuex.
Using Vue 2 and the CLI, I created sample SFCs that use Vuex to store the background color CSS style. Each child is associated with a specific color, and clicking it's button updates the color of all sibling components.
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
commonBgColor: 'navajowhite'
},
mutations: {
updateBgColor(state, newColor) {
state.commonBgColor = newColor;
}
}
})
Parent.vue
<template>
<div class="parent">
<child initBgColor="aquamarine" instanceName="One" />
<child initBgColor="mediumorchid" instanceName="Two" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
Child.vue
<template>
<div class="child">
<div class="row">
<div class="col-md-6" :style="currentBgColor">
<span>Sibling Component {{ instanceName }}</span>
<button type="button" class="btn btn-secondary" #click="updateCommonBgColor">Change All Sibling Colors</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
initBgColor: {
type: String,
required: true
},
instanceName: {
type: String,
required: true
}
},
data() {
return {
backgroundColor: this.initBgColor
}
},
computed: {
currentBgColor() {
return 'background-color: ' + this.$store.state.commonBgColor;
}
},
methods: {
updateCommonBgColor() {
this.$store.commit('updateBgColor', this.backgroundColor);
}
}
}
</script>
<style scoped>
.child {
margin-top: 0.5rem;
}
span {
font-size: 1.5rem;
padding: 0.5rem;
}
button {
float: right;
}
</style>
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>
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>