Vuejs : Rendering components conditionally based on route - javascript

Some of the components needs to be hidden for particular Routes. I was able to achieve that using watcher for route change found from this SO question - Vuejs: Event on route change. I don't want to show header and sidebar in customizePage ( route - /customize ). But there is a problem when I do a hard reload from that particular page. That doesn't execute the watch and hence the it fails. The solution I found was having it also in mounted(), so that it executes also on reload.
But having the same function in mounted and watcher looks weird. Is there a better way to do it ?
<template>
<div>
<TrialBanner v-if="$store.state.website.is_trial"/>
<div class="page-body-wrapper" :class="{ 'p-0' : isCustomizePage}">
<Sidebar :key="$store.state.user.is_admin" v-if="!isCustomizePage"/>
<div class="main-panel" :class="{ 'm-0 w-100' : isCustomizePage}">
<Header v-if="!isCustomizePage"/>
<div class="content-wrapper" :class="{ 'p-0' : isCustomizePage}">
<router-view :key="$store.state.websiteId"></router-view>
</div>
</div>
</div>
</div>
</template>
mounted() {
if(this.$route.path == '/customize') {
this.isCustomizePage = true;
} else {
this.isCustomizePage = false;
}
},
watch: {
$route (to, from){
if(this.$route.path == '/customize') {
this.isCustomizePage = true;
} else {
this.isCustomizePage = false;
}
}
}

Easy fix:
Use an immediate watcher
watch: {
$route: {
immediate: true,
handler(to, from) {
if(this.$route.path == '/customize') {
this.isCustomizePage = true;
} else {
this.isCustomizePage = false;
}
}
}
}
More complex but more extensible fix:
Use "layout" components.
Demo
General idea is to create "Layout" components, use the meta tag on routes to define the layouts for each route, and then use a dynamic component in App.vue to tell the app which layout to use.
App.vue
<template>
<div id="app">
<component :is="layout">
<router-view></router-view>
</component>
</div>
</template>
<script>
export default {
name: "App",
computed: {
layout() {
return this.$route.meta.layout || 'default-layout';
}
}
};
</script>
Default layout component
<template>
<div>
<TrialBanner v-if="$store.state.website.is_trial"/>
<div class="page-body-wrapper" >
<Sidebar :key="$store.state.user.is_admin" />
<div class="main-panel">
<Header />
<div class="content-wrapper">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DefaultLayout',
};
</script>
Sample customize page layout
<template>
<div>
<TrialBanner v-if="$store.state.website.is_trial"/>
<div class="page-body-wrapper" class="p-0">
<div class="main-panel" class="m-0 w-100">
<div class="content-wrapper" class="p-0">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CustomizeLayout',
};
</script>
Main.js: register layout components as global components
import DefaultLayout from '#/layouts/DefaultLayout.vue';
import CustomizeLayout from '#/layouts/CustomizeLayout.vue';
Vue.component('default-layout', DefaultLayout);
Vue.component('customize-layout', CustomizeLayout);
Router.js: routes define the layouts for each route
const routes = [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/customize',
name: 'customize',
component: CustomizeView,
meta: {
layout: 'customize-layout'
}
}
];
The <slot></slot> in each layout component is where the View will render. You can also have multiple named slots and named views if you want to render different components in areas per layout.

Related

VueJS2 - Triggering modal component from another component

So this one is a bit tricky for me. I have a Sidebar.vue component with a link(Help). I want to trigger my Modal.vue component with different data for every main view eg. Home, About, Contact etc. So whenever I am on Home I want to trigger help modal with hints via that link for Home.vue, when I am on About I want to show hints for About.vue and so on..
My code:
Sidebar.vue
<template>
<ul>
<li>
<a #click="openHelp">Help</a>
</li>
</ul>
</template>
<script>
export default {
name: 'Sidebar',
methods: {
openHelp() {
this.$emit('help-modal');
},
},
};
</script>
Modal.Vue
<template>
<div class="modal" v-if="open">
<div class="modal-title">
<h4>Help</h4>
</div>
</div>
</template>
<script>
export default {
name: 'Modal',
props: {
open: {
type: Boolean,
default: false,
}
},
methods: {
close() {
this.$emit('closed');
},
},
};
</script>
Home.vue
<template>
<div>
Home
<modal
:open="helpOpen"
#closed="openHelpModal">
<p>Home Help</p>
</modal>
</div>
<template>
<sidebar/>
</template>
</template>
<script>
import Sidebar from '#/components/Sidebar.vue';
export default {
name: 'Home',
components: {
Sidebar,
},
data() {
return {
helpOpen: false,
}
}
methods: {
openHelpModal() {
this.helpOpen = !this.helpOpen;
},
}
}
</script>
I know that vuex would be the best solution but don't have an idea how to approach it. Modal would show only static images with a bit of text for every main view.
One way of doing this would be to simply add a property to the modal component e.g helpData and pass the relevant data to this prop in each of the main pages as shown below
Modal.vue
<template>
<div class="modal" v-if="open">
<div class="modal-title">
<h4>Help</h4>
</div>
</div>
</template>
<script>
export default {
name: 'Modal',
props: {
open: {
type: Boolean,
default: false,
},
helpData: {
type: String,
required: true
}
},
methods: {
close() {
this.$emit('closed');
},
},
};
</script>
Home.vue
<template>
<div>
Home
<modal
:open="helpOpen"
#closed="openHelpModal"
:help-data="This is a sample help data for the home view"
/>
</div>
<template>
<sidebar #help-modal="helpOpen = true"/>
</template>
</template>
<script>
import Sidebar from '#/components/Sidebar.vue';
export default {
name: 'Home',
components: {
Sidebar,
},
data() {
return {
helpOpen: false,
}
}
methods: {
openHelpModal() {
this.helpOpen = !this.helpOpen;
},
}
}
</script>
As indicated by #ljubadr, I have included the logic that would open the modal in the sidebar component of Home.vue. Also, I would recommend you change the name of the function openHelpModal to closeHelpModal (seeing as this function will basically close the modal) or toggleHelpModal (since from the logic of the function, it toggles the modal state).

beforeRouteEnter doesn't work in vue child component

I try to detect what is the previous route in my component. I use beforeRouteEnter to find it. it works in CreditAdd.vue but when I use beforeRouteEnter in Back.vue it doesn't work!
I think it because the Back component is a child. but I can't solve the problem
Back.vue:
<template>
<i class="bi ba-arrow-right align-middle ml-3" style="cursor: pointer" #click="handleBack"></i>
</template>
<script>
export default {
name: 'Back',
data() {
return {
id: null
};
},
beforeRouteEnter(to, from, next) {
console.log('please log');
next(vm => {
vm.fromRoute = from;
});
},
mounted() {},
methods: {
handleBack() {
if (!this.fromRoute.name) {
this.$router.push({ name: 'dashboard' });
} else {
this.$router.back();
}
}
}
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
CreditAdd.vue:
<template>
<Layout>
<template slot="header">
<Back />
<span>افزایش اعتبار</span>
</template>
<div class="col-lg-10 px-0">
<Card>
<template v-if="type == 'select'">
<credit-add-way #select="changeType"></credit-add-way>
</template>
<template v-if="type == 'bank'">
<credit-add-bank-way #back="changeType('select')"></credit-add-bank-way>
</template>
<template v-if="type == 'code'">
<credit-add-code-way #back="changeType('select')"></credit-add-code-way>
</template>
</Card>
</div>
</Layout>
</template>
beforeRouteEnter and other navigation guards should only works on the Vue file that is defined the link in the router, thats why Back.vue not working.
You can use plain javascript to get the previous URL
in Back.vue
mounted() {
console.log(document.referrer);
}
Another way is you can store the previous route in Vuex store,
In AddCredit.vue where navigation guards work
beforeRouteEnter(to, from, next) {
// store this in vuex
},
then in Back.vue can just retrieve right away from the store

Vue.js - How to pass prop as a json array and use it correctly in child component?

Laravel and Vue are being used. Data is being returned to Vue from Laravel. The Vue prop being send to the Vue child component is a json object array of objects, however, the child component errors when reading it. The error in the console is:
"TypeError: Cannot read property 'id' of undefined". Any help would be greatly appreciated.
This is the raw data returned from Laravel is:
{"channels":[{"id":4,"name":"AI","created_at":"2020-04-27T15:18:01.000000Z","updated_at":"2020-04-27T15:18:01.000000Z"},{"id":2,"name":"Android Development","created_at":"2020-04-27T15:18:01.000000Z","updated_at":"2020-04-27T15:18:01.000000Z"},{"id":3,"name":"iOS Development","created_at":"2020-04-27T15:18:01.000000Z","updated_at":"2020-04-27T15:18:01.000000Z"},{"id":1,"name":"Web Development","created_at":"2020-04-27T15:18:01.000000Z","updated_at":"2020-04-27T15:18:01.000000Z"}]}
The data being passed as a Vue prop to a child component:
<template>
<div id="component">
<router-link :to="{ name: 'home' }">Home</router-link>
<router-view></router-view>
<br>
<vue-chat :channels="channels"></vue-chat>
</div>
</template>
<script>
export default {
data() {
return {
channels: [],
}
},
methods: {
fetchChannels() {
let endpoint = `/channels`;
axios.get(endpoint).then(resp => {
this.channels = resp.data.channels;
});
},
},
created() {
this.fetchChannels();
}
}
</script>
The child component that errors when attempting to access the Vue prop:
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<!-- <div class="card-header">Chat</div> -->
<div class="card-body">
<div class="container">
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['channels'],
data() {
return {
activeChannel: this.channels[0].id,
}
},
}
</script>
<style scoped>
#import '/sass/app.scss';
</style>
At first time channels is not available which raises that error, i recommend to define activeChannel as computed property like :
export default {
props: ['channels'],
data() {
return {
}
},
computed:{
activeChannel(){
return this.channels[0]? this.channels[0].id:null,
}
}
Did you check your data channels with Vue Devtools on chrome?
You may try to change below line.
this.channels = resp.data.channels; to this.channels = JSON.parse(resp.data.channels);

How to dynamically update a var from a parallel component in vue.js

I have a 'views' page that imports two components, one of which is a NavBar that will display a loading animation until the other component is fully loaded in.
The way I'm trying to accomplish this, is I am trying to define a 'loading' var in the view, pass that var into the NavBar AND releases components. IF I change the loading to false from the releases component that should propagate over to the NavBar (to stop the loading animation).
views/Release.vue
<template>
<div>
<NavBar v-bind:loading="this.loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases v-bind:loading="this.loading"></Releases>
<Footer></Footer>
</div>
</div>
</template>
<script>
import Releases from "../components/Releases.vue";
import NavBar from "./components/NavBar.vue";
export default {
name: "releases",
data () {
return {
loading: 'loading'
}
},
components: {
NavBar,
Releases,
}
};
</script>
components/NavBar.vue
<template>
<div>
<div id="nav">
<a href='/link1'>Link 1</a>
<a href='/link2'>Link 2</a>
<a href='/link3'>Link 3</a>
<pulse-loader :loading="this.loading"></pulse-loader>
</div>
</div>
</template>
<script>
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
export default {
name: 'NavBar',
props: ['loading'],
components: {
PulseLoader
},
};
</script>
I have left out Releases.vue from this post for brevity, but no matter where I set
this.loading=false
It does not seem to propagate over to NavBar component.
What am I doing wrong here? Not sure If I need to use $emit for something like this?
No, you should NOT modify the prop loading from Releases.vue.
In Releases.vue when data loaded, call $emit:
this.loadReleases()
.then(() => {
// Your logic.
this.$emit('loaded', true);
});
In the view Release.vue
<template>
<div>
<NavBar :loading="loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases #loaded="updateLoading"></Releases>
<Footer></Footer>
</div>
</div>
</template>
<script>
import Releases from "../components/Releases.vue";
import NavBar from "./components/NavBar.vue";
export default {
name: "releases",
data () {
return {
loading: true,
}
},
components: {
NavBar,
Releases,
},
methods: {
updateLoading(val) {
this.loading = !val; // loading = false;
},
},
};
</script>
Please, use : instead of v-bind, # instead of v-on for making the code clear. And it's no need to use this on the template.
Do not use this in your templates.
<NavBar v-bind:loading="loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases v-bind:loading="loading"></Releases>
<Footer></Footer>
<pulse-loader :loading="loading"></pulse-loader>
In fact, eveything in the template refers to this component, and you can't refer to anything else directly from the template.
$emit is for sending data up to the parent, and the main use case for that is to tell the parent to update a property that then flows back down to the component. Your use case is updating children, and using v-bind is appropriate, as the NavBar owns the data.

Vue Router: hide parent template when child is open

Im trying to install Vue.js with the router and im running into some view issues. I have a router.js with child routes. I want to use this method for simple breadcrumbs and generate a clear overview so i know which route belongs where.
Opening each route works like a charm, everything shows up. When i open /apps I get a nice view from my Apps.vue that displays App overview</h1>. But now im opening /apps/app-one and then I see the Apps.vue and AppOne.vue template. How can I prevent that both templates are displayed?
The vue components looks like this:
Router.js
import Router from 'vue-router';
import AppsPage from './components/Apps.vue'
import AppOne from './components/AppOne.vue'
import AppTwo from './components/AppTwo.vue'
export default new Router({
// mode: 'history',
routes: [
{
path: '/apps',
component: AppsPage,
children: [
{
path: '/apps/app-one',
component: AppOne,
},
{
path: '/apps/app-two',
component: AppTwo,
},
]
},
]
});
Apps.vue
<template>
<div id="app-overview">
<h1>App overview</h1>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app_page'
}
</script>
App1.vue
<template>
<div>
<h1>App 1</h1>
</div>
</template>
<script>
export default {
name: 'app_one'
}
</script>
App2.vue
<template>
<div>
<h1>App 2</h1>
</div>
</template>
<script>
export default {
name: 'app_two'
}
</script>
Having your routes in a parent-child relationship means that the child component will be rendered inside the parent component (at the <router-view>). This is expected behavior.
If you do not want the parent component to be visible when the child route is active, then the routes should be siblings, not nested:
[
{
path: '/apps',
component: AppsPage,
},
{
path: '/apps/app-one',
component: AppOne,
},
{
path: '/apps/app-two',
component: AppTwo,
},
]
The structure of the routes reflects the way they are rendered on the page.
It's possible and pretty easy too.You can achieve this by followings:
<template>
<div>
<div v-show="isExactActive">
Parent component contents will be here
</div>
<router-view ref="rv"></router-view>
</div>
</template>
<script>
export default {
data() {
return {
isExactActive: true,
}
},
updated() {
this.isExactActive = typeof this.$refs.rv === 'undefined';
},
mounted() {
this.isExactActive = typeof this.$refs.rv === 'undefined';
}
}
</script>
Hope, this will be helpful.

Categories