Vue3: Nested Routes and Dynamic Layout component - javascript

Hi Vue enthusiasts out there,
I have been working on an multi-tenant application and stuck at dynamic layout problem.
Requirement: Load tenant specific layout.vue file from public folder and wrap <router-view> around it.
Tried few things like dynamic imports, defineAsyncComponent etc but couldn't get it working.
// router:
import store from '../store/index';
import NestedApp from '../views/NestedApp.vue';
// const layoutA = () => defineAsyncComponent(import(store.getters.pageLayout('LayoutA')));
const routes = [
{
path: '/:tenant:/:locale',
name: 'NestedApp',
component: NestedApp,
children: [
{
path: 'about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
meta: { layout: () => import(store.getters.pageLayout('LayoutA')) }
}
]
]
// NestedApp.vue:
<template>
<div class="NestedApp">
<navbar/>
<component :is="layoutWrapper">
<router-view/>
</component>
</div>
</template>
<script>
import Navbar from '../components/Navbar.vue';
export default {
name: 'NestedApp',
components: {
Navbar,
},
computed: {
layoutWrapper() {
console.info(`layout: ${this.$route.meta.layout}`);
return this.$route.meta.layout || 'div';
}
}
}
// LayoutA.vue:
<template>
<div class="LayoutA">
<span>Layout A</span>
<slot/>
</div>
</template>
I get following error in browser console:

Got a workaround to this problem.
Sending component via template string from backend API call and then creating a component out of it via defineComponent and markRaw methods.
API response:
"Layouts": {
"LayoutA": {
"name": "LayoutAbout",
"template": "<div class='LayoutA' style='background-color: darkgray'><span>Layout A</span><slot/></div>"
}
},
and then use in App.vue:
import { defineComponent, markRaw } from 'vue';
export default {
name: 'App',
methods: {
loadLayout(pageLayout) {
const layout = this.$store.getters.pageLayout(pageLayout);
this.layoutWrapper = layout ? defineComponent(markRaw({...layout})) : 'div';
}
},
created() {
this.loadLayout(this.$route.meta.layout);
},
beforeRouteUpdate(to) {
this.loadLayout(to.meta.layout);
},
}
<template>
<div class="App">
<navbar/>
<component :is="layoutWrapper">
<router-view/>
</component>
</div>
</template>

Related

How to get a specific child component object property from parent component?

I would like to grab a child component's "meta" property from parent. Is it possible somehow ?
I know there is a solution with an emit method, but is there some easier way to make it happen ?
// Default.vue <-- parent component
<template>
<h1>{{ pagetitle }}</h1>
<router-view />
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'LayoutDefault',
computed: {
pagetitle () {
let title = this.$route.meta.title // <--- I want to access child's component meta here
// if title not provided, set to empty string
if (!title) title = ''
return title
}
}
})
</script>
// router/routes.js
const routes = [
{
path: '/',
component: () => import('layouts/Default.vue'),
children: [
{
path: 'dashboard',
name: 'dashboard',
meta: { title: 'Dashboard', auth: true, fullscreen: false }, // <--- TAKE THIS
component: () => import('pages/dashboard.vue')
}
]
}
]
// pages/dashboard.vue <-- child component
<template>
<div>
dashboard content
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Dashboard',
meta: { // <--- this should be reachable from the parent component (Default.vue)
title: 'Dashboard',
auth: true,
fullscreen: false
}
})
</script>
You can get component info via $route.matched.
Here's a PoC:
const Dashboard = Vue.defineComponent({
template: "<div>Some dashboard</div>",
meta: { title: "Dashboard" },
})
const router = new VueRouter({
routes: [{ path: "/", component: Dashboard }],
})
const app = new Vue({
router,
computed: {
// Note that this takes the *last* matched component, since there could be a multiple ones
childComponent: (vm) => vm.$route.matched.at(-1).components.default,
},
}).$mount('#app')
<div id="app">
<h1>{{ childComponent.meta.title }}</h1>
<router-view />
</div>
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router#3/dist/vue-router.js"></script>
As suggested by Estus Flash in a comment, instead of taking the last matched component we can take the last matched component that has meta defined. To do that, replace the following:
vm.$route.matched.at(-1).components.default
with:
vm.$route.matched.findLast((r) => "meta" in r.components.default)
.components.default
Some approaches I could figure from the web:
Using ref by this.$refs.REF_NAME.$data (As done here: https://stackoverflow.com/a/63872783/16045352)
Vuex or duplicating the logic behind stores (As done here: https://stackoverflow.com/a/40411389/16045352)
Source: VueJS access child component's data from parent

Passing vue.js Route Params to Component

I'm having trouble getting a route param to pass directly into a component. I followed multiple sets of directions in the docs (including using the Composition API as in the following code), but I'm still getting undefined when the CourseModule.vue first renders.
Route Definition
{
path: '/module/:id',
name: 'Course Module',
props: true,
component: () => import('../views/CourseModule.vue'),
},
CourseModule.vue:
<template>
<div class="AppHome">
<CustomerItem />
<CourseModuleItem :coursemodule-id="this.CoursemoduleId"/>
</div>
</template>
<script>
import { useRoute } from 'vue-router';
import CustomerItem from '../components/customers/customer-item.vue';
import CourseModuleItem from '../components/coursemodules/coursemodule-item.vue';
export default {
setup() {
const route = useRoute();
alert(`CourseModule.vue setup: ${route.params.id}`);
return {
CoursemoduleId: route.params.id,
};
},
components: {
CustomerItem,
CourseModuleItem,
},
mounted() {
alert(`CourseModule.vue mounted: ${this.CoursemoduleId}`);
},
};
</script>
coursemodule-item.vue:
<template>
<div id="module">
<div v-if="module.data">
<h2>Course: {{module.data.ModuleName}}</h2>
</div>
<div v-else-if="module.error" class="alert alert-danger">
{{module.error}}
</div>
<Loader v-else-if="module.loading" />
</div>
</template>
<script>
import Loader from '../APILoader.vue';
export default {
props: {
CoursemoduleId: String,
},
components: {
Loader,
},
computed: {
module() {
return this.$store.getters.getModuleById(this.CoursemoduleId);
},
},
mounted() {
alert(`coursemodule-item.vue: ${this.CoursemoduleId}`);
this.$store.dispatch('setModule', this.CoursemoduleId);
},
};
</script>
The output from my alerts are as follows:
CourseModule.vue setup: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
coursemodule-item.vue: undefined
CourseModule.vue mounted: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
As you can see, the path parameter works fine in the top level Vue, but not it's still not getting passed into the component.
your kebab-cased :coursemodule-id props that you're passing to the CourseModuleItem component becomes a camelCased coursemoduleId props
Prop Casing (camelCase vs kebab-case)
try this
// coursemodule-item.vue
...
props: {
coursemoduleId: String,
},
...
mounted() {
alert(`coursemodule-item.vue: ${this.coursemoduleId}`);
this.$store.dispatch('setModule', this.coursemoduleId);
},

How to react to route changes on Vue 3, "script setup" composition API?

This is an example of routes I have in my application:
{
path: "/something",
name: "SomeRoute",
component: SomeComponent,
meta: {showExtra: true},
},
{
path: "/somethingElse",
name: "SomeOtherRoute",
component: SomeOtherComponent,
},
Then I have the following component, which as you will notice has two script tags, one with composition API, one without:
<template>
<div>
This will always be visible. Also here's a number: {{ number }}
</div>
<div v-if="showExtra">
This will be hidden in some routes.
</div>
</template>
<script setup lang="ts">
import type { RouteLocationNormalized } from "vue-router"
import { ref } from "vue"
const number = 5
const showExtra = ref(true)
const onRouteChange = (to: RouteLocationNormalized) => {
showExtra.value = !!to.meta?.showExtra
}
</script>
<script lang="ts">
import { defineComponent } from "vue"
export default defineComponent({
watch: {
$route: {
handler: "onRouteChange",
flush: "pre",
immediate: true,
deep: true,
},
},
})
</script>
This works correctly: when I enter a route with meta: {showExtra: false} it hides the extra div, otherwise it shows it.
What I want to do, however, is achieve the same by only using the composition API, in other words removing the second <script> tag entirely. I have tried this:
<script setup lang="ts">
import type { RouteLocationNormalized } from "vue-router"
import { ref } from "vue"
import { onBeforeRouteUpdate } from "vue-router"
// ...
// same as before
// ...
onBeforeRouteUpdate(onRouteChange)
</script>
But this won't take effect as expected when I switch route. I'm aware of the watch function I could import, but I'm unsure how to get the meta information about the route and how to appease the type checker.
You can convert your watcher to composition api by importing watch method from vue
<script lang="ts">
import { defineComponent, vue } from "vue"
import { useRoute } from "vuex"
export default defineComponent({
setup() {
const route = useRoute()
watch(route, (to) => {
showExtra.value = !!to.meta?.showExtra
}, {flush: 'pre', immediate: true, deep: true})
},
})
</script>

VueJS passing router to child component

How Can I pass router to my child component.
I have this as my router
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
export default function () {
const Router = new VueRouter({
mode: 'history',
routes : [
{
path: '/',
beforeEnter: ifAuthenticated,
component: () => {
return import('./../container/Index.vue')
}
},
{
path: '/login',
beforeEnter: ifNotAuthenticated,
component: () => {
return import('./../container/logn.vue')
}
}
],
})
return Router
}
Now my "/" (index.vue) route have a component Navbar and the Navbar have a logout button which logs out the user and redirect them to login page
Consider this to be my index.vue (with what I have done)
<template>
<q-layout>
<Navbar :thisInfo="routerAndStore"/>
</q-layout>
</template>
<script>
import Navbar from "./../components/navbar.vue";
export default {
name: "PageIndex",
components: {
Navbar
},
data() {
return {
routerAndStore: this
};
}
};
</script>
And then in my navbar.vue I have done something like this
<template>
<div class="nav-pages-main">
<a #click="logoutUser">
<h5>Logout</h5>
</a>
</div>
</template>
<script>
export default {
name: "navbar",
methods: {
logoutUser: () => {
return this.thisInfo.$store.dispatch("GOOGLE_PROFILE_LOGOUT").then(() => {
this.$router.push("/login");
});
}
},
props: {
thisInfo: {
type: Object
}
}
};
</script>
but this doesn't seem to be working (this is coming out to be undefined), So if someone can help me figure out how we can pass this to our child component
Please refer to Vue-Router official documentation here
Basically, in their use case, the main component (index.vue) take a router as argument and provide <router-view> in its template as placeholder for component that would be rendered based on the current route.
In your code, I see that you use it the other way around using router to render the main component.
routes : [
{
path: '/',
beforeEnter: ifAuthenticated,
component: () => {
return import('./../container/Index.vue')
}
},
...
]
Could you try it again using the right way described in the documentation and tell me the result?
Edit: According to the App.vue that you posted (assuming it's the app entry point) then you should provide router to the App component.
<template>
<div id="q-app"> <router-view/> </div>
</template>
<script>
import router from '/path/to/your/router';
export default { name: "App", router };
</script>
<style>
</style>
The full code for this can be found at Vue-Router example

Vue router button not clickable because setup is incorrect

I'm trying to setup Vue router for the first time and I'm running into trouble.
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Services from '../components/Services'
import App from '../app'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'App',
component: App
},
{
path: '/services',
name: 'Services',
component: Services
}
]
})
app.vue
<template>
<div id='app'>
<Navigation></Navigation>
<div class="Site-content">
<router-view></router-view>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Services from "../javascript/components/Services";
import Footer from "../javascript/components/Footer";
import Navigation from "../javascript/components/Navigation";
export default {
components: {
Footer,
Navigation,
Services
},
data: function () {
return {
message: "Welcome to Ping Party From Vue!"
}
}
}
</script>
Navigation.vue
<template>
<div id="navigation">
<nav v-bind:class="active" v-on:click>
Home
Projects
<router-link to="/services">Services</router-link>
Contact
</nav>
</div>
</template>
<script>
import Services from './Services'
export default {
data () {
return { active: 'home' }
},
methods: {
makeActive: function(item) {
this.active = item;
}
}
}
</script>
That vue-router option is not working in my navigation. It shows up on the page but it's not clickable and I'm getting this error in the console.
ERROR
Unknown custom element: <router-link> - did you register the component
correctly? For recursive components, make sure to provide the "name"
option.
found in
---> <Navigation> at app/javascript/components/Navigation.vue
<App> at app/javascript/app.vue
<Root>
Unknown custom element: <router-view> - did you register the component
correctly? For recursive components, make sure to provide the "name"
option.
found in
---> <App> at app/javascript/app.vue
Make sure to register your router with your Vue instance.
So in your
import router from './router'
new Vue({
el: '#some-element'
router, // This line is important
render: h => h(App)
})

Categories

Resources