Clicking on active router-link - vue.js

In my Vue 2 app, I have a menu bar whose menu items use router-link. One of these menu items is 'Customers', which takes the user to a customer editor. Now, if the user clicks on this same 'Customers' menu item while they're in the customer editor, nothing happens. I presume this is because of this behaviour mentioned in the docs: "In HTML5 history mode, router-link will intercept the click event so that the browser doesn't try to reload the page."
Now, that's perfectly understandable and for the majority of the time it would be the behaviour I want. But actually here I want the component to be reloaded, to go back to a clean slate. Or perhaps more accurately, for some event to occur which I can handle within the customer editor to set things how I want them to be. I assumed this would be the case, in fact, but when I set up a watch, thus
watch: {
'$route' (to, from) {
console.log("Route changed");
}
},
I don't see anything logged to the console. It may be that some event is occurring which I'm not handling and that Vue is simply reusing the component. But how can I stop it from doing so?

According to this issue, you can add a #click.native binding to the current router-link and reinitialize your component data in there.
But a quick and dirty solution would be to append a unique parameter to your route, like a date. Check this fiddle

The intended method of accomplishing this seems to be to implement a beforeRouteUpdate function to reset the properties of the route component. See this issue on vue-router's GitHub: Routing the same component, the component is not reload, but be reused? #1490.
This is expected behaviour, Vue re-uses components where possible.
You can use the beforeRouteUpdate hook to react to a route switch that
uses the same component.

Related

Nuxt-link delay transition to new page?

This is a follow up to #1458. I'm looking for some direction on how Nuxt expects this to be handled.
I have a menu. When I click on a nuxt-link in the menu, I want to have time to close the menu before the page transition happens. The thing is, I only want that to happen when you click on the nuxt-link in the menu, not every time I go to a certain route (as the previous issue described using a middlewear on the route).
So there are a few different ways to do this, and I'm curious what the "Nuxt" way is?
The way we currently do this, disable the nuxt-link and capture the click, then do a router.push().
<nuxt-link :to="path" event="disabled" #click.native="delayLoad"/>
// Methods
delayLoad(event) {
this.$store.commit("CLOSE_MENU")
setTimeout(()=>{
this.$router.push(event.target.pathname)
}, 2000)
}
Is this a good idea? I just always have an aversion to hijacking nuxt-link and browser navigation like this. It seems janky.
The other ideas we played with were using a query param on the nuxt-link, and then using that in a middlewear to delay the page transition. That seemed worse to me, because now my URL's have a query param in them that is used for an animation, seems like that is abusing query params. This also triggers the page loading progress bar, which isn't really the intent, it's to have a sequenced animation happen, then page load.
It seems to me that perhaps nuxt-link should have a delay prop, or perhaps the page transition config should allow for a delay (like it does with duration)?
I wanted to do this as well and came up with the following solution. Using the new slots api you can more elegantly customise the nuxt-link behaviour:
<nuxt-link v-slot="{ route, href }" :to="path" custom>
<a :href="href" #click.prevent="$emit('navigate', route)">
<slot></slot>
</a>
</nuxt-link>
This will make the link emit a navigate event with the route as a param. You then listen for this event wherever you include your menu component, like this:
<template>
<transition
name="fade"
#after-leave="maybeNavigate"
>
<MainMenu
v-if="menuIsVisible"
#navigate="navigate"
/>
</transition>
</template>
<script>
export default {
data: () => ({
menuIsVisible: false,
navigateToOnMainMenuClose: null,
}),
methods: {
navigate(route) {
this.navigateToOnMainMenuClose = route
this.menuIsVisible = false
},
maybeNavigate() {
if (this.navigateToOnMainMenuClose) {
this.$router.push(this.navigateToOnMainMenuClose)
this.navigateToOnMainMenuClose = null
}
},
},
}
</script>
Whenever you click a nav link in the menu, the route will be stored and the menu will close. After the menu out animation has finished, maybeNavigate() will push the stored route, if there is one. This removes the need for a setTimeout and if you manage to click multiple links in quick succession only the last one will be stored and navigated to.
Since nuxt-link is essentially a wrapped version of vue's router-link, if you look at the documentation for that there is an event property that accepts string or string[], looking at it's source code here: https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L86
you can see it will register a listener for disabled in this instance. It may make more sense to pass an empty array so that no event listeners are registered, but that's at the cost of readability.
Otherwise, #click.native is the suggested way to handle custom click handlers for router-link (see: https://github.com/vuejs/vue-router/issues/800#issuecomment-254623582).
The only other concerns I can think of are what happens if you click 2 different links in rapid succession and what happens if you click more than once. May just want to add a variable to track if a link has been clicked to prevent firing setTimeout multiple times, which could navigate you from page A to B and then to C as all timeouts will fire if not canceled. Or maybe you want to only navigate to the 'last' link clicked, so if another link is clicked, you would cancel the earlier setTimeout. This is realistically an edge case that probably won't be an issue, but worth exploring.
Otherwise, IMO, looks good to me. This seems like the simplest way to implement this without having to create a custom component / plugin. I'm no expert, but is likely how I would implement this functionality as well. It would be nice to see a delay option though since I can see myself using that functionality as well with vuetify.
Another potential method would be to do your store commit in beforeTransition: https://nuxtjs.org/api/configuration-transition/
Though I'm not sure that there is access to the store there, so you might have to write a custom plugin for that as well. Again, seems more complicated than it's worth for a simple delayed animation. Simple, working code is sometimes the best solution, even if it's not the most extensible option.
See also: How can I transition between two nuxt pages, while first waiting on a child component transition/animation to finish?
for another way of handling this.

How to show correct nested child tab when page is reloaded or refreshed in Vue

I am working on implementing nested tab view using custom tab component in Vue. When I refresh or reload the page for nested child tab, no active tab is shown. My question is how to show the active parent tab when page is refreshed/reloaded for the correct active child tab. Here I am providing the code to understand better.
https://codesandbox.io/s/morning-leaf-zxlt7?file=/src/components/TabView.vue
To clarify more, Suppose, I am browsing here to Operation 2 Under the Operation Tab
Now, when I refresh the page, I got this-
I need to click the parent tab (Operation Tab) to see the active Child Tab (Operation 2)
Please, help me to solve this problem. Thank you.
If i'm understanding correctly, your goal here is to keep the selected tabs open after reloading/refreshing the page. In that case you need to store the state of that component by creating new items in localStorage manually by calling localStorage.setItem('key', 'value') and then grabbing that values back in the mounted() lifecycle hook of your component with localStorage.getItem('key', 'value'). Alternatively you can use use Vuex with vuex-persist plugin that will handle saving and loading the state of your app for you.
After your edit, it seems that you want the app to go to a specific route after a refresh. Perhaps you could implement vue-router, and save the most recent route in localstorage, then re-route the user to said most recent route when the app is refreshed.
You would need: implement vue-router so that the open tabs are saved into the route, use hooks or mixin to save most recent route to localstorage, use created() hooks in App.Vue to load the most recent route (if any) and use programmatic navigation to push user to the loaded most recent route.

force component to rebuild on route change or vuex state change

I want to get into VueJs / Vuex and created a small todo app. There are three links for "all todos", "pending ones" and "completed ones". It's the same route but with a different filter query.
When changing the route the component will not update, because it seems that updated query is not forcing an component update. But the computed event gets triggered.
I created a small example showing my current problem
https://codesandbox.io/s/6zx2p0m20r
If you click around on the todo links there will be no component update. If you head over to "another route" and head back, the component was updated (because of a completely different route).
How can I force to update the component on a query update within the same base route?
You can add the beforeRouteUpdate navigation guard to reinitialize your data:
beforeRouteUpdate is called when the route that renders this component has changed,
but this component is reused in the new route.
For example, for a route with dynamic params /foo/:id, when we
navigate between /foo/1 and /foo/2, the same Foo component instance
will be reused, and this hook will be called when that happens.
(source)
beforeRouteUpdate(to, from, next) {
this.currentTodoFilter = to.query.filter
next()
}
You can also remove your updateTodoFilter method this way.

How to add a callback to a Vue root that triggers when child components update

I have a Semantic UI sidebar that uses the Semantic UI visibility module to highlight items in the sidebar menu as they're scrolled past in the page. I need to refresh this visibility config every time the page length changes. This happens a lot when my Vue page initially loads and then occasionally as the user adds bits to the page.
I'd like to just be able to add an updated callback on the root Vue element and have it trigger every time any descendant component updates but this doesn't seem to be a thing. Is there a way to do this tidily? I'd like not to have to litter a bunch of components with this updated callback.
you can use vue event bus to trigger events from different components.
First, initialize Vue.prototype.$bus = new Vue(); in your main.js file.
then use it to send events:
this.$bus.$emit('throw', 'Hi')
then let your main component listen:
this.$bus.$on('throw', ($event) => {
console.log($event) //shows 'Hi'
})

Route binding not updating sub component

I have a a hard time understanding how components and routes works together. In the documentation, they only talk about one level of components. In case there is multiple level, it does not look like it is working.
I made this http://jsfiddle.net/uvqpracr/7/
and when you click on init(1) it initialize the counter with 1 and when you click on init(5) it initialize the counter with 5. In the route component, I declare v-bind:init-counter="$route.params.initCounter so when I am in counter-container, writing {{init-counter}} works, but in the subcomponent counter, event if I wrote v-bind:init-counter="initCounter" it does not work.
In this documentation, I can read:
One thing to note when using routes with params is that when the user navigates from /user/foo to /user/bar, the same component instance will be reused. Since both routes render the same component, this is more efficient than destroying the old instance and then creating a new one. However, this also means that the lifecycle hooks of the component will not be called.
I wonder if this is the reason why what I am trying to do does not work. If so I really wonder how I should do it in a simple way.
First of all, yes, the components (counter-container and its child counter) are created just once.
See the log at this demo JSFiddle. No matter how many times you click the links, the created()s are only called once each (see the console).
and when you click on init(1) it initialize the counter with 1 and when you click on init(5) it initialize the counter with 5
Not quite. I mean, the clicks don't always initialize the counter variable.
Actually, when you click them, the route changes and then initCounter (not counter, not total) changes.
At the first click, because the components haven't been created before, then the value of initCounter will be used to initialize the counter (and total).
But in subsequent clicks, even though the initCounter does change even for nested components, it won't affect the counter/total variables because they have already been created.
Check the demo JSFiddle. I added the displaying of counter: {{ counter }} / initCounter: {{ initCounter }}, so you'll see initCounter changes in the counter component as well.
Updating every time
So, you now know that initCounter will be used to set counter/total only once, only when the components are first created.
If you want to update them whenever initCounter changes, the solution is to watch the route (using watch: { '$route' (to, from) { /* react here */ } }) or, more specifically, watch initCounter.
Check this other demo JSFiddle. This one uses watch and updates counter/total whenever initCounter is updated.