Deep nested routes and different components rendering, based on route path - javascript

I have deep nested routes in my routes.js file. As you can see in code bellow I have to render different component, based on route (if route is products I need to render Products.vue component, but if route goes deeper I need to render EmptyRouterView.vue component which contains template <router-view></router-view> so I can render sub route components).
{
path: '/products',
name: 'products',
component: {
render(c) {
if (this.$route.name === 'products') {
return c(require('pages/Products/Products.vue').default)
} else {
return c(require('components/EmptyRouterView.vue').default);
}
}
},
meta: {
requiresAuth: true,
allowedPositions: '*'
},
children: [
// Scan product to get info
{
path: '/products/search-product',
name: 'search-product',
component: () => import('pages/Products/SearchProduct.vue'),
meta: {
requiresAuth: true,
allowedPositions: '*'
}
},
....
]
}
I wonder if there is some short or better way to do this? For example (I know I can't call this in arrow function) something like this?
component: () => {
this.$route.name === 'products' ? require('pages/Products/Products.vue').default : require('components/EmptyRouterView.vue').default
}
Or do you see if there is possible to do this some completely other way?
If you need any additional informations, please let me know and I will provide. Thank you!

You can i.e. create another .vue-file and include both components inside (<cmp-1 /> & <cmp2 />). Then you can build your if-statement inside the template with another template-tag:
<template v-if="boolean">
<cmp-1 />
</template>
<template v-else>
<cmp-2 />
</template>
The if depends on your route then.

Related

Vue - pass prop via router to component

I have a component with the object "project_element" and I want to transfer the object to another component through the "vue-router".
This is the code from my first component which opens the second component if the user clicks on the button.
<router-link :to="{ name: 'project', params: { project_url: project_element.project_name, project_element: project_element} }">
<b-button> Open </b-button>
</router-link>
This is the code from my Vue Router in index.js
{
path: '/projects/:project_url',
component: SingleProjectViewApp,
name: 'project',
props: { project_element: project_element }
},
I already managed to set the "project_element.project_name" to the url but I also need the "project_element" itself in my second component.
In the compenent I have set the object in the "props section"
props: {
project_element: {
type: Object,
required: true
}
},
The problem is in the Vue Router, I can't pass the project_element like a variable, only with quotation marks. But then I get an error because obviously the component expected an object and not a string.
Thanks for your help!
Try this in your router
path: '/projects/:project_url?',
component: SingleProjectViewApp,
props(route) {
const props = {
projectElement: route.params.project_url
};
return props;
}
and in your props change "project_element" to "projectElement" (generally you want to do camel case in vue props)
props: {
projectElement: {
type: Object,
required: true
}
},
The first thing that came to my mind is JSON.stringify() and JSON.parse(). Although I am not sure this is a perfect solution.
Any way try this:
<router-link :to="{ name: 'project', params: { project_url: project_element.project_name, project_element: JSON.stringify(project_element)} }">
<b-button> Open </b-button>
</router-link>
// router.js
{
path: '/projects/:project_url',
component: SingleProjectViewApp,
name: 'project',
props(route) {
return {
project_element: JSON.parse(route.params.project_element),
}
}
},
Note: I honestly think this is an antipattern and you should use different aproach for comunicating through components. custom-events, centralized-state, event-bus or even provide & inject will all be better for that kind of work.
routes: [
{
path: '/',
name: 'start',
component: Start,
meta: {
my_data: "my data here",
},
},]
to get data inside the component
this.$route.currentRoute.meta.my_data
UPDATE
this.$route.push("/"+ JSON.stringify(data) )
routes: [
{
path: '/:data',
name: 'start',
component: Start,
},]
to get data inside the component
JSON.parse(this.$route.params.data)

Basic Vue help: Accessing JS object values in component

I’ve been experimenting with vue.js and I'm having difficulty accessing JS object values in components when routing.
Using this repo to experiment, https://github.com/johnayeni/filter-app-vue-js, I'm just trying to replicate a basic a “product list” and “product description” app, but I can't get it working. The repo's homepage (the SearchPage.vue component) serves as the "product list," and I'm just trying to add the "product description" component to display only one item at a time.
I've added a "description page" component (calling it "item.vue") to allow a user to click on one of the languages/frameworks that will then route to item.vue to just display that specific object's associated information (item.name, item.logo, etc.), i.e., and not display any of the other languages.
Following some tutorials, here's what I've tried:
First, I added ids to the JS objects (found in data/data.js), i.e., id:'1'.
const data = [
{
id: '1',
name: 'vue js',
logo: 'http://... .png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ],
},
{
id: '2',
name: 'react js',
logo: 'http://... .png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ]
},
...
];
export default data
Then, I wrapped the item.name (in ItemCard.vue) in router-link tags:
<router-link :to="'/item/'+item.id"> {{ item.name}} </router-link>
I then added a new path in router/index.js:
{
path: './item/:id',
component: item,
props: true
}
But, when that router-link is clicked I can only access the ".id" (via $route.params.id), but I can't get .name or .logo. How do I access the other values (i.e. item.name, item.logo, etc.)? I have a feeling I'm going down the wrong track here.
Thank you so much for your help.
The only reason you have access the id because it's an url param: ./item/:id.
You have a couple options here, which depends on what you're trying to accomplish:
As suggested by #dziraf, you can use vuex to create a store, which in turn would give you access to all the data at any point in your app:
export default {
computed: {
data() {
return this.$store.data;
}
}
}
Learn more here: https://vuex.vuejs.org/
As an alternative, you can just import your data, and grab the correct item by its id:
import data from './data.js';
export default {
computed: {
data() {
return data.find(d => d.id === this.$route.params.id);
}
}
}
Just depends on what you're trying to do.
I guess you just need a wrapper component that takes the desired item from the URL and renders the proper item. Let's say an ItemWrapper:
<template>
<item-card :item="item"></item-card>
</template>
<script>
import ItemCard from './ItemCard.vue';
import data from '../data/data';
export default {
components: {
ItemCard,
},
props: {
stackNameUrl: {
required: true,
type: String,
},
},
data() {
return {
item: {},
}
},
computed: {
stackName() {
return decodeURI(this.stackNameUrl);
}
},
created() {
this.item = data.find( fw => fw.name === this.stackName);
}
}
</script>
<style>
</style>
This component takes a prop which is a stack/fw name uri encoded, decodes it, finds the fw from data based on such string, and renders an ItemCard with the fw item.
For this to work we need to setup the router so /item/vue js f.i. renders ItemWrapper with 'vue js' as the stackNameUrl prop. To do so, the important bit is to set props as true:
import Vue from 'vue';
import Router from 'vue-router';
import SearchPage from '#/components/SearchPage';
import ItemWrapper from '#/components/ItemWrapper';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'SearchPage',
component: SearchPage
},
{
path: '/item/:stackNameUrl',
name: 'ItemWrapper',
component: ItemWrapper,
props: true,
},
]
});
Now we need to modify SearchPage.vue to let the stack boxes act as links. Instead of:
<!-- iterate data -->
<item-card v-for="(item, index) in filteredData" :key="index" :item="item"></item-card>
we now place:
<template v-for="(item, index) in filteredData" >
<router-link :to="'/item/' + item.name" :key="index">
<item-card :key="index" :item="item"></item-card>
</router-link>
</template>
So now every component is placed within a link to item/name.
And voilá.
Some considerations:
the :param is key for the vue router to work. You wanted to use it to render the ItemCard itself. That could work, but you would need to retrieve the fw from data from the component created(). This ties your card component with data.js which is bad, because such component is meant to be reusable, and take an item param is much better than go grabbing data from a file in such scenario. So a ItemWrapper was created that sort of proxies the request and pick the correct framework for the card.
You should still check for cases when an user types a bad string.
Explore Vue in depth before going for vuex solutions. Vuex is great but usually leads to brittle code and shouldn't be overused.

Angular access methods of parent routes from child routes

So to explain clearly my problem, I have a component for each of my entities in my application like Author component and Book component. And for each of them I will have two child which is a list component and a form component.
So basically my route configuration look like this :
export const routing = RouterModule.forRoot([
{
path: 'author', component: AuthorComponent,
children: [
{ path: 'author-list', component: AuthorListComponent },
{ path: 'author-form', component: AuthorFormComponent }
]
},
{
path: 'book', component: BookComponent,
children: [
{ path: 'book-list', component: BookListComponent },
{ path: 'book-form', component: BookFormComponent }
]
}
]);
In my AuthorComponent for example I have a method to delete an author that call the service :
deleteBadge = (event): void => {
// Call delete service
this._badgeService.delete(event).subscribe(
result => {
// Good
},
error => {
// Error
}
My question is how can I call that method from my route child (author list or form component) knowing that I can't call it like a normal child component using event.
PS: I put method (and many other) in the parent because I need to access to it in both child components and so to avoid redundancy.
Standard practice is to use a shared service for Component Interaction. However, if you still want to avoid using a shared service, you can use the Injector API.
In your child component, AuthorListComponent for example, do the following:
import { Injector } from '#angular/core';
import {AuthorComponent} from "./author.component";
// ....
constructor(private injector:Injector){
let parentComponent = this.injector.get(AuthorComponent);
parentComponent.deleteBadge('String passed from AuthorListComponent');
}
Here is a link to working demo.
Use a communication Service which unites several communication observables.
An example can be found in the official Angular docs: https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service

(Vue.js) Same component with different routes

I would like to use the same component for different routes in a Vue.js application.
I currently have something like this:
main.js
const routes = [
{ path: '/route-1', name: 'route-1', component: MyComponent },
{ path: '/route-2', name: 'route-2', component: MyComponent },
{ path: '/route-3', name: 'route-3', component: MyComponent },
]
const router = new VueRouter({
routes
})
myComponent.vue
<ul>
<li><router-link to="/route-1">Route 1</router-link></li>
<li><router-link to="/route-2">Route 2</router-link></li>
<li><router-link to="/route-3">Route 3</router-link></li>
</ul>
When I type the route manually in the browser, everything is working well, but when I try to navigate between the routes using some of these router-generated-links, nothing happens. The route changes but the content is still the same. Any idea how I can solve this?
Thanks!
This is expected behaviour as Vue is trying to be optimal and reuse existing components. The behaviour you want to achieve used to be solved with a setting called canReuse, but that has been deprecated. The current recommended solution is to set a unique :key property on your <router-view> like so:
<router-view :key="$route.path"></router-view>
Check out this JSFiddle example.
You can use watch property, so your component will not waste time to reloading:
index.js
You might have something like this
const routes = [
{
path: '/users/:id',
component: Vue.component('user', require('./comp/user.vue').default)
}
]
user.vue
created(){
// will fire on component first init
this.init_component();
},
watch: {
// will fire on route changes
//'$route.params.id': function(val, oldVal){ // Same
'$route.path': function(val, oldVal){
console.log(this.$route.params.id);
this.init_component();
}
},
methods: {
init_component: function(){
// do anything you need
this.load_user_data_with_ajax();
},
}
Just to make a note. If anybody is working with SSR template, things are a bit different. #mzgajner's answer does indeed recreate the component but will not trigger the asyncData again.
For that to happen, modify entry-client.js like this.
OLD:
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
NEW:
const activated = matched.filter((c, i) => {
/*
In my case I only needed this for 1 component
*/
diffed = ((prevMatched[i] !== c) || c.name == 'p-page-property-map')
return diffed
})

vue-router replace parent view with subRoute

I'm using the vue-router and have a question regarding subRoutes. I would like to set up my routes so the main routes are listings and the subRoutes are things like edit/add/etc.
I want the subRoute components to replace the <router-view> of the parent route. The way I understand the documentation and from what I've tested, it looks like I should define another <router-view> in the parent components template for the subRoute to render into but then the user-list would remain visible.
Example routes:
'/users': {
name: 'user-list',
component(resolve) {
require(['./components/users.vue'], resolve)
},
subRoutes: {
'/add': {
name: 'add-user',
component(resolve) {
require(['./components/users_add.vue'], resolve)
}
}
}
}
Main router view:
<!-- main router view -->
<div id="app">
<router-view></router-view>
</div>
User list:
<template>
<a v-link="{ name: 'add-user' }">Add</a>
<ul>
<li>{{ name }}</li>
</ul>
</template>
Add user:
<template>
<div>
<a v-link="{ name: 'user-list' }">back</a>
<input type="text" v-model="name">
</div>
</template>
When I click on "Add", I want to be filled with the add-user template. Is this possible?
Also, can I establish a parent-child relationship between the user-list and add-user components? I would like to be able to pass props (list of users) to the add component and dispatch events back up to the user-list.
Thanks!
It sounds like those edit/add routes should not be subRoutes, simple as that.
just because the path makes it seem like nesting doesn't mean you have to actually nest them.
'/users': {
name: 'user-list',
component(resolve) {
require(['./components/users.vue'], resolve)
}
},
'users/add': {
name: 'add-user',
component(resolve) {
require(['./components/users_add.vue'], resolve)
}
}
So I played around with this for quite a bit and finally figured out how this can be achieved. The trick is to make the listing a subRoute too and have the root-level route just offer a <router-view> container for all child components to render into. Then, move the "list loading" stuff to the parent component and pass the list to the list-component as a prop. You can then tell the parent to reload the list via events.
That's it for now, it works like a charm. Only drawback is that I have another component that I didn't really need. I'll follow up with an example when I find the time.
edit
Added an example as requested. Please note: this code is from 2016 - it may not work with current Vue versions. They changed the event system so parent/child communication works differently. It is now considered bad practice to communicate in both directions (at least the way I'm doing it here). Also, these days I would solve this differently and give each route it's own module in the store.
That said, here's the example I would have added back in 2016:
Let's stick with the users example - we have a page with which you can list/add/edit users.
Here's the route definition:
'/users': {
name: 'users',
component(resolve) {
// this is what I meant with "root-level" route, it acts as a parent to the sub routes
require(['./pages/users.vue'], resolve)
},
subRoutes: {
'/': {
name: 'user-list',
component(resolve) {
require(['./pages/users/list.vue'], resolve)
}
},
'/add': {
name: 'user-add',
component(resolve) {
require(['./pages/users/add.vue'], resolve)
}
},
// etc.
}
},
users.vue
<template>
<div>
<router-view :users="users"></router-view>
</div>
</template>
<script>
/**
* This component acts as a parent and sort of state-storage for
* all user child components. It's the route that's loaded at /users.
* the list component is the actual component that is shown though, but since
* that's a sibling of the other child components, we need this.
*/
export default {
events: {
/**
* Update users when child says so
*/
updateUsers() {
this.loadUsers();
}
},
data() {
return {
users: []
}
},
route: {
data() {
// load users from server
if (!this.users.length) {
this.loadUsers();
}
}
},
methods: {
loadUsers() {
// load the users from an API or something
this.users = response.data;
},
}
}
</script>
./pages/users/list.vue
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
<script>
export default {
props: ['users'],
// the rest of the component
}
</script>

Categories

Resources