VueJS Router - change named view in root with child route - vue.js

The ask
Control a root-level view from a child route. The example below is one level deep, but I could need to control the other view from deeper routes.
Have both the default view and another view in a single template, other simply must live at this level and will not work how I need if nested within other components:
<router-view name="default"/>
<router-view name="other">/>
Must have a parent-child route relationship to maintain navigation functionality (router-link-active)
Attempts
For a route, have the view set:
{
path: '/my-path',
component: ComponentOne,
}
Going to /my-path shows ComponentOne in default view, other view is empty, as it should.
Now at a child route, I want the other view to show ComponentTwo.
This does not work because it expects other view to be within default view, and so ComponentTwo is simply not rendered:
{
path: '/my-path',
component: ComponentOne,
children: [
{
path: '/my-path/more',
components: {
'other': ComponentTwo
}
}
]
}
This does not work because it does not maintain the parent-child relationship:
{
path: '/my-path',
component: ComponentOne
},
{
path: '/my-path/more',
components: {
default: ComponentOne,
other: ComponentTwo
}
}

I don't think it's possible to control a router-view from a nested route.
However, to do what you ask quickly, the solution would be to manage the contents of all (/my-path and sub-routes), in a nested route even for the path /my-path as well. You don't even need named routes.
I'll explain better with an example:
{
path: '/my-path',
component: ComponentWrapper,
children: [
{
path: '', // Match '/my-path' or '/my-path/'
component: ComponentOne
},
{
path: 'more', '/my-path/more'
component: ComponentTwo
}
]
}
In the ComponentWrapper you have to insert a router-view to render the nested routes and everything will work fine, even parent-child route relationship.

Unfortunately Leonardi's solution does not work if you wanted to refer to the the current child's component from a component that's more than 1 level higher in the hierarchy (i.e. a layout).
I was able to work around this by writing a computed method to determine the component of the current child route in the parent component.
{
path: '/my-path',
component: ComponentOne,
children: [
{
path: '/my-path/more',
components: {
'other': ComponentTwo
}
}
]
}
<!-- App.vue or SomeLayout.vue -->
<template>
<router-view /> <!-- ComponentOne -->
<component :is="componentOfCurrentChildRoute" /> <!-- ComponentTwo -->
</template>
<script>
export default {
computed: {
componentOfCurrentChildRoute() {
// Get an array of the current route ancestor hierarchy
const {matched} = this.$route;
// Return the component defined by the current child route
return matched?.[matched.length - 1]?.component; // or components.sidebar if using named views
}
}
}
</script>

Related

VueJS-3 How to render <router-view></router-view> from vue-router?

In vuejs 2 I was able to render directly in the router by return a render html tag. I'm trying to do the same things in vue3 with vue-router 4, but it doesn't appear to work:
{
path: 'posts',
redirect: '/posts/all',
name: 'posts',
meta: {'breadcrumb': 'Posts'},
component: {
render() {
return h('router-view');
}
},
children: [ //Lots of sub routes here that I could load off the anonymous router-view]
}
Any idea how I can get the component to render that router-view and allow me to use my children routes? I rather not load up a single component just to house "". This worked perfectly in vue-router 3, no idea what is wrong with 4. I also am importing the {h} from 'vue'.
From the Vue 3 Render Function docs:
In 3.x, with VNodes being context-free, we can no longer use a string ID to implicitly lookup registered components. Instead, we need to use an imported resolveComponent method:
You can still do h('div') because it's not a component, but for components you have to use resolveComponent:
import { h, resolveComponent } from Vue; // import `resolveComponent` too
component: {
render() {
return h(resolveComponent('router-view'))
}
},
Alternatively, if you wanted to use the runtime compiler, you could use the template option (not recommended because the compiler increases app size):
component: {
template: `<router-view></router-view>`
}
Since Vue Router 4, you can use the RouterView component directly:
import { RouterView } from 'vue-router';
//...
{
path: 'posts',
redirect: '/posts/all',
name: 'posts',
meta: {'breadcrumb': 'Posts'},
component: RouterView, // <- here
children: [ /* ... */ ]
}
Source: https://github.com/vuejs/vue-router/issues/2105#issuecomment-761854147
P.S. As per linked issue, there are allegedly some problems with <transition> when you use this technique, so use caution.

Detecting named view is used

I am looking to build a layout with a dynamic side-bar. The sidebar on its own has its own template, but I do not want that to be rendered unless the underlying router-view is used. Below the pseudo-code:
<template>
<transition name="slide">
<div class="sidebar" v-if="showSidebar">
<SidebarHeader />
<router-view name="side"><router-view>
<SidebarFooter>
</div>
</transition>
<div class=main><router-view></router-view></div>
</template>
Is there any obvious way of achieving that? I failed to find any property or function on $router or $route object.
As a workaround I've made Sidebar component that has to be used for each sidebar view, but is there a better option?
It seems I found the answer - it requires a bit of trick int he routes:
{
path: '',
component: 'Layout',
children: [
{
path: '',
components: { default: SomeMainComponent },
children: [
{ path: '' },
{
path: '',
components: { side: SideBox },
children: [
path: 'hello',
component: HelloSidebox
]
}
]
}
]
}
That empty route {path: ''} is to prevent the next route from being displayed for the exact route match and only render SideBox component for any matching sub-routes.

Vue-router and deeply nested routes

Given following Layout component:
Layout:
<template>
<div class="container">
<div class="sidebar-container">
<router-view name='sidebar'></router-view>
</div>
<div class="main">
<router-view></router-view>
</div>
</div>
</template>
Is there a way to specify in deeply nested route to use sidebar outlet?
const routes = {
path: '',
components: { default: SomeMainComponent },
children: [{
path: 'foo',
children: [{
path: 'bar',
components: { sidebar: BarSide }
}]
}]
}
It seems that router is only looking to resolve the router outlets of the direct route parent - if I move components: { side: BarSide } to the foo definition, then the component is rendered as expected. As is, component is not even being created.
Is there a way to achieve this?
JsFiddle: https://jsfiddle.net/xptkhf4z/5/ - clicking on a With helper link updates the route, keeps default component rendered but does not render the additional component in helper slot.
Although I don't have the full answer, you seem to have to include a component on each nested level. That can either be your page component or a layout component.
This should work:
const routes = {
path: '',
components: { default: SomeMainComponent },
children: [{
path: 'foo',
component: () => import('layouts/DummyLayoutComponent'),
children: [{
path: 'bar',
components: { sidebar: BarSide }
}]
}]
}
The DummyLayoutComponent should be a neutral layout, because it is added to the layout of SomeMainComponent.
I find it a little awkward that we seem to have to define an "empty" layout just to get this to work. But maybe I'm just missing an important point.

Dynamic Vue Router

I am researching whether a vue router is the best approach for the following scenario:
I have a page containing 'n' number of divs. Each of the divs have different content inside them. When a user clicks on a button in the div, I would like the div to open in a separate browser window (including its contents).
Can a route name/component be created on the fly to route to? Since I have 'n' number of divs, that are created dynamically, I cannot hard-code name/components for each one
<router-link :to="{ name: 'fooRoute'}" target="_blank">
Link Text
</router-link>
I want to avoid the same component instance being used (via route with params) since I may want multiple divs to be open at the same time (each one in their own browser window)
If the link is opening in a separate window, it makes no sense to use a <router-link> component as the application will load from scratch in any case. You can use an anchor element instead and generate the href property dynamically for each div.
To answer your questions:
A route name cannot be created dynamically since all routes must be defined at the beginning, when the app (along with router) is being initialized. That said, you can have a dynamic route and then dynamically generate different paths that will be matched by that route.
There is no way for the same component instance to be reused if it's running in a separate browser window/tab.
It is possible to create dynamic router name.
profileList.vue
<template>
<main>
<b-container>
<b-card
v-for="username in ['a', 'b']"
:key="username"
>
<b-link :to="{ name: profileType + 'Profile', params: { [profileType + 'name']: username }}">Details</b-link>
</b-container>
</main>
</template>
<script>
export default {
name: 'profileList',
data () {
return {
profileType: ''
}
},
watch: {
// Call again the method if the route changes.
'$route': function () {
this.whatPageLoaded()
}
},
created () {
this.whatPageLoaded()
},
methods: {
whatPageLoaded () {
this.profileType = this.$route.path // /user or /place
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
</style>
b-container, b-card, b-link are taken from bootstrap-vue, so you can freely change it.
router.js
const router = new Router({
mode: 'hash',
base: process.env.BASE_URL,
linkExactActiveClass: 'active',
routes: [
// USERS
{
path: '/user/:username',
name: userProfile,
component: userProfile
},
{
path: '/user',
name: 'userList',
component: profileList
},
// PLACES
{
path: '/place/:placename',
name: placeProfile,
component: placeProfile
},
{
path: '/place',
name: 'placeList',
component: ProfileList
}
]
})

VueJS vue-router passing a value to a route

In VueJS 2 with vue-router 2 I am using a parent view with subcomponents like this:
WidgetContainer.vue with route /widget_container/:
<template>
<component :is="activeComponent"></component>
</template>
<script>
import WidgetA from './components/WidgetA'
import WidgetB from './components/WidgetB'
export default {
name: 'WidgetContainer',
components: {
WidgetA,
WidgetB
},
data () {
return {
activeComponent: 'widget-a'
}
}
}
</script>
In WidgetA I have the option of selecting a widget id
<template>
// v-for list logic here..
<router-link :to="{ path: '/widget_container/' + widget.id }"><span>{{widget.name}} </span></router-link>
</template>
<script>
export default {
name: 'WidgetA',
data() {
return {
widgets: [
{ id: 1,
name: 'blue-widget'
}]}}
routes.js:
export default new Router({
routes: [
{
path: '/widget_container',
component: WidgetContaner
},
{
path: '/widget_container/:id?',
redirect: to => {
const { params } = to
if (params.id) {
return '/widget_contaner/:id'
} else {
return '/widget_container'
}
}
}]})
From the WidgetContainer if the route is /widget_container/1 (where '1' is the id selected in WidgetA) I want to render WidgetB, but I cant work out:
1) how to pass the selected widget id into the router-link in WidgetA
2) How to know in WidgetContainer the the route is /widget_contaner/1 instead of /widget_container/ and render WidgetB accordingly.
Any ideas?
You can pass data to parent using by emitting event, you can see more details around here and here.
Once the data is change, you can watch over it and update the variable which has stored your widget.
Another option, if communication between components become unmanageable over time is to use some central state management, like vuex, more details can be found here.
Wouldn't it be easier and more scallable to use Vuex for that?
Just commit id to store and than navigate ?