Vue-router reload components - javascript

I have a few routes that each load 3 components. Two of the components are the same on all routes.
When I move between those routes I want to pass new data and on some init event of the component I want to fill the data of that component so it reflects on the UI.
Also I want to retrigger bootstrap animations of components being loaded.
How would I go about doing that.
Because right now, I don't know where in the component lifecycle would I fetch the data and rerender the component with this new data.
Concretly in myapps/1 and /newapp/ I have a main view component and a sidebar component. In the /newapp/ URL I want all icons on the sidebar to be red (nothing on the main view has been filled) and the main view should be empty.
On the myapps/1 I want to load the data from the server into the main view and if it's all filled I want icons on the sidebar to be green.
What now happens I load myapps/1, click on the second item in the sidebar and the main view changes to the second view. Then I router.push("/newapp/); and sidebar stays on the second item and second main view.
So router.push("/myapps/"); doesn't reload my sidebar and my main view.
EDIT:
Here you see my routes, sidebars and default are the same.
const routes = [
{
path: "/myapps/newproject",
components: {
default: ProjectMain,
"breadcrumbs": BreadcrumbsNewProject,
"sidebar": SidebarProject,
}
},
{
path: "/myapps/:id",
components: {
default: ProjectMain,
"breadcrumbs": BreadcrumbsCurrentProject,
"sidebar": SidebarProject,
}
},
{
path: "/myapps/newversion/:id",
components: {
default: ProjectMain,
"breadcrumbs": BreadcrumbsNewVersion,
"sidebar": SidebarProject,
}
}
];
This is my ProjectMain.vue
<template>
<div class="wrapper wrapper-content">
<component :is="currentProjectMain"></component>
</div>
</template>
This is my router-view in index.html:
<router-view name="sidebar"></router-view>
And I have another one in index.html, the default one:
<router-view></router-view>
When I click on some item on Sidebar i emit event to the ProjectMain to switch out the component. So like this:
In Sidebar:
eventBus.$emit("currentProjectMainChanged", "appOverview");
or
eventBus.$emit("currentProjectMainChanged", "appSettings");
And than in ProjectMain:
eventBus.$on("currentProjectMainChanged", function(data) {
if(data === "appOverview") {
self.currentProjectMain = CurrentProjectAppOverview;
}else if(data === "appSettings") {
self.currentProjectMain = CurrentProjectSettings;
}
});
If I got to "/myapps/:id". It loads the sidebar and ProjectMain and I get a little animation of the sidebar and the ProjectMain with this bootstrap classes:
<div class="animated fadeInUp"> and both components got through the entire lifecycle.
By default appOverview is selected in sidebar and CurentProjectAppOverview.vue is loaded as a component in ProjectMain.
Than I click on appSettings in the sidebar and class is added to that item in the sidebar to mark it as selected and in the ProjectMain CurrentProjectSettings.vue is loaded as a component.
But then in the breadcrumbs I have a button to go to "/myapps/newversion/:id"
Here is the problem. When I click to go to "/myapps/newversion/:id" (router.push("/myapps/newversion/" + id);) the second item on the sidebar remains selected (appSettings) and in ProjectMain CurrentProjectSettings.vue remains loaded, and I don't get the bootstrap animation of the sidebar and ProjectMain, and the components don't go through their lifecycle.
What I want here when I click the button to go to "/myapps/newversion/:id" is my bootstrap animation (<div class="animated fadeInUp">), I want the Sidebar and ProjectMain to go through their entire lifecycle. And I want the default item to be selected on the sidebar (so appOverview) and default component to be loaded in ProjectMain (so CurentProjectAppOverview.vue).
There are colored buttons for the each item in the sidebar. If I go to "/myapps/newversion/:id" from "/myapps/:id", I want to load the data from the server into CurrentProjectAppOverview.vue and if all data is filled I want the button on the sidebar to be green and if not it should be red.
So In short when moving between this two routes I want to be able to load data and fill the fields I want, and I want bootstrap animation and default views to be selected and loaded and they should go through their entire lifecycle. Now router just reuses them as they are.
So something like "ReloadComponent: true", or destroy and recreate component.
I could duplicate SidebarProject.vue and ProjectMain.vue and rename them and in each route load basically a copy but that would mean I have to write the same code in different .vue files.
Before there was an option to set YouComponent.route.canReuse to false, which would do what I want, I think, but it is removed in the newest version.

I was facing the same problem. I will quote a found answer, which is really simple and great.
The simplest solution is to add a :key attribute to :
<router-view :key="$route.fullPath"></router-view>
This is because Vue Router does not notice any change if the same component is being addressed. With the key, any change to the path will trigger a reload of the component with the new data.

Partially thanx to this: https://forum.vuejs.org/t/vue-router-reload-component/4429
I've come up with a solution that works for me:
First I added a new function to jQuery which is used to retrigger bootstrap animation when the component loads.
$.fn.redraw = function(){
return $(this).each(function(){
var redraw = this.offsetHeight;
});
};
In component:
export default{
created:function(){
this.onCreated();
},
watch: {
'$route' (to, from) {
//on route change re run: onCreated
this.onCreated();
//and trigger animations again
$("#breadcrumbsCurrentProject").find(".animated").removeClass("fadeIn").redraw().addClass("fadeIn");
}
}
}
I call the onCreated(); method when the component loads and when the route reloads or the ID of the same route changes but the component stays the same I just call the method again. This takes care of the new data.
As for re triggering bootstrap animations I use redraw(); function I add to jQuery right on the start of the app. and this re triggers the animations on URL change:
$("#breadcrumbsCurrentProject").find(".animated").removeClass("fadeIn").redraw().addClass("fadeIn");

It looks like you are using vue-router.
I don't have your code, but I can show you how I handled this. See named-views.
Here is my router-map from my bundle.js file:
const router = new VueRouter({
routes: [
{ path: '/',
components: {
default: dashboard,
altside: altSide,
toolbar: toolbar
},
},
{ path: '/create',
components: {
default: create,
altside: altSide,
toolbar: toolbar
},
},
{ path: '/event/:eventId',
name: 'eventDashboard',
components: {
default: eventDashboard,
altside: altSide,
toolbar: eventToolbar,
},
children: [
{ path: '/', name: 'event', component: event },
{ path: 'tickets', name: 'tickets', component: tickets},
{ path: 'edit', name: 'edit', component: edit },
{ path: 'attendees', name: 'attendees', component: attendees},
{ path: 'orders', name: 'orders', component: orders},
{ path: 'uploadImage', name: 'upload', component: upload},
{ path: 'orderDetail', name: 'orderDetail', component: orderDetail},
{ path: 'checkin', name: 'checkin', component: checkin},
],
}
]
})
I have named-views of "default", "altside" and "toolbar" I am assigning a component to the named view for each path.
The second half of this is in your parent component where you assign the name
<router-view name="toolbar"></router-View>
Here is my parent template:
<template>
<router-view class="view altside" name="altside"></router-view>
<router-view class="view toolbar" name="toolbar"></router-view>
<router-view class="view default"></router-view>
</template>
So, what I've done is I've told the parent template to look at the named-view of the router-view. And based upon the path, pull the component I've designated in the router-map.
For my '/' path toolbar == the toolbar component but in the '/create' path toolbar = eventToolbar component.
The default component is dynamic, which allows me to use child components without swapping out the toolbar or altside components.

This is what I came up with:
import ProjectMain from './templates/ProjectMain.vue';
import SidebarProject from './templates/SidebarProject.vue';
//now shallow copy the objects so the new object are treated as seperatete .vue files with: $.extend({}, OriginalObject);
//this is so I don't have to create basically the same .vue files but vue-router will reload those files because they are different
const ProjectMainA = $.extend({}, ProjectMain);
const ProjectMainB = $.extend({}, ProjectMain);
const ProjectMainC = $.extend({}, ProjectMain);
const SidebarProjectA = $.extend({}, SidebarProject);
const SidebarProjectB = $.extend({}, SidebarProject);
const SidebarProjectC = $.extend({}, SidebarProject);
const routes = [
{
path: "/myapps/newproject",
components: {
default: ProjectMainA,
"breadcrumbs": BreadcrumbsNewProject,
"sidebar": SidebarProjectA
}
},
{
path: "/myapps/:id",
components: {
default: ProjectMainB,
"breadcrumbs": BreadcrumbsCurrentProject,
"sidebar": SidebarProjectB
}
},
{
path: "/myapps/newversion/:id",
components: {
default: ProjectMainC,
"breadcrumbs": BreadcrumbsNewVersion,
"sidebar": SidebarProjectC
}
}
];
This basically tricks the vue-router into thinking he's loading different .vue components, but they are actually the same. So I get everything I want: reloading of the component, animation, life cycle, and I don't have to write multiple similar or the same components.
What do you think, I'm just not sure if this is the best practice.

Related

Vue.js on render populate content dynamically via vue.router params

AS title sates, I don't so much need a solution but I don't understand why I'm getting the undesired result;
running v2 vue.js
I have a vue component in a single component file.
Basically the vue should render data (currently being imported from "excerciseModules" this is in JSON format).
IT's dynamic so based on the url path it determines what to pull out of the json and then load it in the page, but the rendering is being done prior to this, and I'm unsure why. I've created other views that conceptually do the samething and they work fine. I dont understand why this is different.
I chose the way so I didn't have to create a ton of routes but could handle the logic in one view component (this one below).
Quesiton is why is the data loading empty (it's loading using the empty "TrainingModules" on first load, and thereafter it loads "old" data.
Example url path is "https...../module1" = page loads empty
NEXT
url path is "https..../module 2" = page loads module 1
NEXT
url path is "https..../module 1" = page loads module 2
//My route
{
path: '/excercises/:type',
name: 'excercises',
props: {
},
component: () => import( /* webpackChunkName: "about" */ '../views/training/Excercises.vue')
}
<template>
<div class="relatedTraining">
<div class="white section">
<div class="row">
<div class="col s12 l3" v-for="(item, index) in trainingModules" :key="index">
<div class="card">
<div class="card-content">
<span class="card-title"> {{ item.title }}</span>
<p>{{ item.excercise }}</p>
</div>
<div class="card-action">
<router-link class="" to="/Grip">Start</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
console.log('script');
let trainingModules; //when initialized this is empty, but I would expect it to not be when the vue is rendered due to the beforeMount() in the component options. What gives?
/* eslint-disable */
let init = (params) => {
console.log('init');
console.log(trainingModules);
trainingModules = excerciseModules[params.type];
//return trainingModules
}
import { getRandom, randomImage } from '../../js/functions';
import { excerciseModules } from '../excercises/excercises_content.js'; //placeholder for JSON
export default {
name: 'excercises',
components: {
},
props: {
},
methods: {
getRandom,
randomImage,
init
},
data() {
return {
trainingModules,
}
},
beforeMount(){
console.log('before mount');
init(this.$route.params);
},
updated(){
console.log('updated');
},
mounted() {
console.log('mounted');
//console.log(trainingModules);
}
}
</script>
I can't tell you why your code is not working because it is an incomplete example but I can walk you through a minimal working example that does what you are trying to accomplish.
The first thing you want to do, is to ensure your vue-router is configured correctly.
export default new Router({
mode: "history",
routes: [
{
path: "/",
component: Hello
},
{
path: "/dynamic/:type",
component: DynamicParam,
props: true
}
]
});
Here I have a route configured that has a dynamic route matching with a parameter, often called a slug, with the name type. By using the : before the slug in the path, I tell vue-router that I want it to be a route parameter. I also set props: true because that enables the slug value to be provided to my DynamicParam component as a prop. This is very convenient.
My DynamicParam component looks like this:
<template>
<div>
<ul>
<li v-for="t in things" :key="t">{{ t }}</li>
</ul>
</div>
</template>
<script>
const collectionOfThings = {
a: ["a1", "a2", "a3"],
b: ["b1", "b2"],
c: [],
};
export default {
props: ["type"],
data() {
return {
things: [],
};
},
watch: {
type: {
handler(t) {
this.things = collectionOfThings[t];
},
immediate: true,
},
},
};
</script>
As you can see, I have a prop that matches the name of the slug available on this component. Whenever the 'slug' in the url changes, so will my prop. In order to react to those changes, I setup a watcher to call some bit of code. This is where you can make your fetch/axios/xhr call to get real data. But since you are temporarily loading data from a JSON file, I'm doing something similar to you here. I assign this data to a data value on the component whenever the watcher detects a change (or the first time because I have immediate: true set.
I created a codesandbox with a working demo of this: https://codesandbox.io/s/vue-routing-example-forked-zesye
PS: You'll find people are more receptive and eager to help when a minimal example question is created to isolate the problematic code. You can read more about that here: https://stackoverflow.com/help/minimal-reproducible-example

Angular 2 Page / Component not reloading

Using the Nav on the website and going from
http://localhost:4200/mens
to
http://localhost:4200/mens/shirts
Works fine but when clicking on another category from with shirts like Hats the component/page doesn't reload and i'm unsure why as i'm not getting a error and it work fine when you click in the first category but within that category if you click on another it doesn't.
const routes: Routes = [
{
path : '',
component: WebsiteComponent,
children: [
{
path : '',
component: HomePageComponent,
},
{
path : 'mens',
component: MensPageComponent
},
{
path : 'mens/:category',
component: MensPageComponent
}
]
}
];
How are you getting your params in the men's component?
Here is a stackblitz that works https://stackblitz.com/edit/angular-buahdy
It uses route.params observable that emits when the route changes, if you are using a snapshot it will not change as it is not an observable.
category$ = this.route.params.pipe(map(params => params.category));
constructor(private route: ActivatedRoute) { }
and in the template show the category with the async pipe
Category {{ category$ | async }}
When you go from mens/shoes to mens/shirts the men's component does not reload, only the category param has changed. Subscribing to the param with route.params is how you trigger an update in your component.
When using the children property in the route. The component will need a router-outlet for those children routes to be rendered.
WebsiteComponent template:
<!-- some code -->
<!-- where children routes are rendered -->
<router-outlet></router-outlet>

Vue Dynamic Layouts mounting router-view component twice

I setup my Vue project to use dynamic layouts - that is, layouts that persist from page to page, assuming the layout for the new page is the same as the last page. My problem is that when I go to a route with a different layout, the router-link component gets created and destroyed, then created again, which is causing me some issues. My setup is as follows:
App.vue
<template>
<component :is="layout">
<router-view :layout.sync="layout" />
</component>
</template>
<script>
import LayoutPortal from '#/layouts/LayoutPortal';
import LayoutOffline from '#/layouts/LayoutOffline';
import LayoutDefault from '#/layouts/LayoutDefault';
export default {
name: 'App',
components: {
LayoutPortal,
LayoutOffline,
LayoutDefault,
},
...
Some router-view Component
<template>
...
</template>
<script>
import LayoutDefault from '#/layouts/LayoutDefault';
export default {
...
created() {
this.$emit('update:layout', LayoutDefault);
},
}
</script>
Layout Default
<template>
<div class="wrapper">
<slot />
</div>
</template>
<script>
export default {
name: 'layout-default',
};
</script>
tldr;
If you setup your project using dynamic layouts, following any of a number of tutorials out there online, when you navigate to a route with a different layout than the last page, the new router-view component gets created, destroyed, then created again. This causes issues like doubling up on mounted() calls and more.
I ultimately went with nested (child) routes (https://router.vuejs.org/guide/essentials/nested-routes.html):
{
path: '/portal',
component: LayoutPortal,
beforeEnter(to, from, next) {
if (!store.getters.auth) {
next('/login');
return;
}
next();
},
children: [
{
path: 'home',
name: 'portal-home',
component: PortalHome,
},
{
path: 'about',
name: 'portal-about',
component: PortalAbout,
},
...
In this way I can load up the layout as the parent route, separate beforeEnter logic into separate route groups, and avoid the problem where going to a page with a new layout loads a component twice.

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.

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