I'm using the Vuejs official router, and am currently working on giving different routes different animations, like in the example here: https://router.vuejs.org/en/advanced/transitions.html (see 'Route-Based Dynamic Transition')
My problem is that some transitions need a specific transition durations, and others don't (those I want to specify in the CSS).
I've made a variable that holds the duration to pass to the router, (same as this.transitionName in the router) but I was wondering if there was a way to set this variable to 'auto', for the routes that don't need a duration?
I don't know enough about your setup, so I'll only address modifying the transition duration.
As you can tell, the transitions are using css to manage the transitions.
I've taken the example in the doc you linked (https://github.com/vuejs/vue-router/blob/dev/examples/transitions/app.js) and added the functionality to override the duration.
const Parent = {
data() {
return {
transitionName: "slide-left",
transitionDuration: 2.5
};
},
computed: {
transitionStyle() {
return {
'transition-duration': this.transitionDuration +'s'
}
}
},
beforeRouteUpdate(to, from, next) {
const toDepth = to.path.split("/").length;
const fromDepth = from.path.split("/").length;
///////////////
// this is where you set the transition duration
// replace with your own implementation of setting duration
this.transitionDuration = Math.random()*1.5+0.5;
///////////////
this.transitionName = toDepth < fromDepth ? "slide-right" : "slide-left";
next();
},
template: `
<div class="parent">
<h2>Parent</h2>
{{transitionStyle}}
<transition :name="transitionName">
<router-view class="child-view" :style="transitionStyle"></router-view>
</transition>
</div>
`
};
This will override the duration by in-lining the style
You would still need to find a way to get the intended duration within beforeRouteUpdate, but I'll leave that up to you.
you can have a look at a working pen here: https://codepen.io/scorch/pen/LOPpYd
Note that the effect is only applied to the parent component, so animation on home(/) will not be affected
Related
I am trying to create multiple markers in Vue using VueMapbox. Currently the map displays correctly but there is only one marker. I think there is something wrong either with my v-for statement or perhaps in the forEach statement. I am trying to place a marker on each location but only the first location is added.
Here is the code for my vue component:
<template>
<MglMap
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
>
<MglMarker v-for="coordinate in coordinates" :key="coordinate" :coordinates="coordinates">
<MglPopup>
<VCard>
<div>{{ country }}</div>
<div>{{ cases }}</div>
</VCard>
</MglPopup>
</MglMarker>
</MglMap>
</template>
<script>
import Mapbox from "mapbox-gl";
import { MglMap, MglPopup, MglMarker } from "vue-mapbox"
export default {
name: 'Map',
components: {
MglMap,
MglPopup,
MglMarker,
},
data() {
return {
accessToken: 'pk.accesstoken...blahblahblah',
mapStyle: 'mapbox://styles/mapbox/dark-v10',
coordinates: [],
country: '',
cases: 0,
}
},
created() {
this.mapbox = Mapbox;
this.getData();
},
methods: {
getData: function () {
fetch('https://coronavirus-tracker-api.herokuapp.com/v2/locations')
.then(response => response.json())
.then(data => {
const locations = data.locations;
locations.forEach(stat => {
const country = stat.country;
this.country = country;
const cases = stat.latest.confirmed;
this.cases = cases;
const coordinates = [stat.coordinates.longitude, stat.coordinates.latitude]
this.coordinates = coordinates;
})
})
}
}
}
</script>
You're currently doing a v-for on coordinates. It should be on locations.
If locations don't have all the required props a MglMarker needs, transform them in the forEach but that's all you should do in that forEach (if you need it at all). Don't use it to populate this.country, this.cases or this.coordinates. You only want to set those when a marker is clicked (if, and only if, you have any functionality listening to changes on those Vue instance properties).
There might be more details which need to be fixed but, without a minimal reproducible example it's very difficult to spot them. Note: you'll need to create a mapbox public token with readonly permissions for your example to work.
To summarize: Move the functionality from your forEach into a function called showMarker or activateMarker. Call that function whenever a marker is clicked or, if that's what you want, call it on one of the locations to make it the currently active one.
What your code does now is: it makes all markers the currently active one, therefore only the last one iterated will be currently active.
Here's what your MglMarker iterator might look like:
<MglMarker v-for="(l, key) in locations"
:key="key"
:coordinates="l.coordinates"
#click="activateMarker(l)"
>
<MglPopup>
VCard>
<div>{{ l.country }}</div>
<div>{{ l.latest.confirmed }}</div>
</VCard>
</MglPopup>
</MglMarker>
In activateMarker method you can dispatch any action to let the rest of the app know about your selection. Or you can close any other open popups, for example (although that can be handled easier with :closeOnClick="true" on each MglPopup).
If I’m on a page with the URL 'http://localhost:8080/item' and I’m clicking on the same link on this page, then the page does not reload.
I need to make that if I click on the same link, the page will reload.
My link:
<nuxt-link :to="/item">
Any insight will be welcome. Thanks!
Use key, something like:
<router-view :key="$route.params.yourCustomParam"/>
Also you can use something like:
<router-link :to="{ params: { yourCustomParam: Data.now } }" replace>link</router-link>
Remember to is passed router.push() and it accept an object also. Doing that, it is more declarative and controllable. I'm using this to decide if the page of component should be rerendered since they will based on id params obtained from URL entry, and my child component can still using nesting .
I recently tried to solve a similar issue and to overcome this I used Vuex with :key (ref).
Firstly, in your store you need a state property such as:
export const state = () => ({
componentUpdates: {
item: 0,
//can add more as needed
}
})
In general, you could use only one property across the app if you prefer it that way. Just remember that later on, the key value needs to be unique - that is in the case if you used this property for two or more components within one page, for example. In this case, you could do something like this :key="$store.getters.getComponentUpdates.item+'uniqueString'"
then a getter:
export const getters = {
getComponentUpdates(state) {
return state.updateComponent;
}
}
finally a mutatation:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update]++
}
}
Now we can utilise the reactive :key wherever needed.
But first in your nuxt-link lets add an event to trigger the mutation, note the usage of #click.native to trigger the click event:
<nuxt-link #click.native="$store.commit('updateComponent', { update: 'item'})" :to="/item">
Now in the item page, for example. Let's imagine there is a component that needs to be updated. In this case we would add :key to it:
<my-item :key="$store.getters.getComponentUpdates.item" />
That is it. As you can see this solution utilises the benefits of nuxt-link but also allows us to selectively update only parts of our page that need updates (we could update the entire page this way as well if needed).
In case if you needed to trigger the logic from mounted or initial load in general, then you could use computed property and :key to your div container, right inside the <template> of your page.
Add :key to the div:
<template>
<div :key="$store.getters.getComponentUpdates.item"></div>
</template>
Create computed property:
computed: {
updateItemPage() {
//run your initial instructions here as if you were doing it in mounted then return the getter
this.initialLoadMethod()
return this.$store.getters.getComponentUpdates.item
}
}
The final touch, which is not crucial but can be implemented in order to reset the state property:
export const mutations = {
updateComponent(state, payload) {
return state.componentUpdates[payload.update] >= 10
? state.componentUpdates[payload.update] = 0
: state.componentUpdates[payload.update]++
}
}
My Vue app has a dynamic tabs mechanism.
Users can create as many tabs as the want on the fly, each tab having its own state (eg "Pages").
I am using the <keep-alive> component to cache the different pages.
<keep-alive include="page">
<router-view :key="$route.params.id" />
</keep-alive>
But users can also "close" individual tab. As pages tend to store a lot of datas, I would like to delete the according page component from the cache, as the user close the tab.
How can I programmatically destroy a cached component inside keep-alive ?
You can call this.$destroy() before user close the tab and delete all of data and event binding in that one.
If you don't mind losing the state when a tab is added/removed, then you can try these:
Use v-if and turn off the keep-alive component and turn it back on in
nextTick
Use v-bind on the include list, and remove "page" and add it
back in nextTick
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
cachedViews is the array of the route component name
First when create a tab, cachedViews push the cached route name, when you switch the opened tab, the current route is cached.
Second when close the tab, cachedViews pop the cached route name, the route
component will destroyed.
There is no built-in function in keep-alive which allows you to clear a specific component from the cache.
However, you can clear the cache from the VNode directly inside the component you want to destroy by calling this function :
import Vue, { VNode } from 'vue'
interface KeepAlive extends Vue {
cache: { [key: string]: VNode }
keys: string[]
}
export default Vue.extend({
name: 'PageToDestroy',
...
methods: {
// Make sure you are not on this page anymore before calling it
clearPageFromKeepAlive() {
const myKey = this.$vnode.key as string
const keepAlive = this.$vnode.parent?.componentInstance as KeepAlive
delete keepAlive.cache[myKey]
keepAlive.keys = keepAlive.keys.filter((k) => k !== myKey)
this.$destroy()
}
},
})
For me, it doesn't cause any memory leaks and the component is not in the Vue.js devtools anymore.
based on the answer of #feasin, here is the setup I am using
template
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
script
import { ref, inject, watch } from "vue";
export default {
components: { CustomRouterLink },
setup() {
const cachedViewsDefault = ["Page1", "Page1", "Page3"];
var cachedViews = ref([]);
const auth = inject("auth");
// check whether user is logged in (REACTIVE!)
const isSignedIn = auth.isSignedIn;
// set the initial cache state
if (isSignedIn.value) {
cachedViews.value = cachedViewsDefault;
}
// clear the cache state
watch(isSignedIn, () => {
if (!isSignedIn.value) {
cachedViews.value = [];
} else {
cachedViews.value = cachedViewsDefault;
}
});
return {
cachedViews,
};
},
};
First I set the initial cached views value based on whether the user is logged in or not.. After the user logs-out I simply set the array value to an empty array.
When the user logs back in - I push the default array keys back into the array.
This example of course does not provide the login/logout functionality, it is only meant as a POC to to the solution proposed by the #feasin (which seems like a good approach to me)
Edit 19.01.2022
I now understand the shortcomings of such approach. It does not allow to gradually destroy a certain component. Given that we have a component named Item and it's path is Item/{id} - there is currently no native way (in Vuejs3) to remove, let's say a cached item with Id = 2. Follow up on this issue on the Github: https://github.com/vuejs/rfcs/discussions/283
Edit 20-21.01.2022
Note that you have to use the computed function for inclusion list. Otherwise the component will not ever be unmounted.
Here is the fiddle with the problem: https://jsfiddle.net/7f2d4c0t/4/
Here's fiddle with the fix: https://jsfiddle.net/mvj2z3pL/
return {
cachedViews: computed(() => Array.from(cachedViews.value)),
}
I'm trying to simplify my problem:
Let us say I have a modal component
//example.php
<modal>
<div>A big div</div>
</modal>
Before the modal is shown I need to calculate the height for the proper animation. Inside the modal Vue it looks like this:
//Modal.vue
...
<transition
:name="transition"
#before-enter="beforeTransitionEnter"
#after-leave="afterTransitionLeave"
>
<div
v-if="visibility.modal"
ref="modal"
class="v--modal v--modal-box"
:style="modalStyle"
>
<slot/>
</div>
</transition>
I know that with this.$slots.default I get the node of the slot.
But I'm not sure how I can create a div, add the node to the div so that I can then calculate the height of it?
Edit:
Is it possible to call your own render function so I can use it like in the docs?
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
Like
guessSlotsHeight(){
let modalDiv = document.createElement('div')
modalDiv.className = 'v--modal v--modal-box'
//something like
let slotDiv = this.render('div', this.$slots.default)
modalDiv.appendChild(slotDiv);
},
Your logic is too complex. It's easiest than you think.
Look, animation is the thing which happens to DOM. Therefore, DOM is available when animation starts. So, you can access any DOM element using Vue ref (recommended) or native javascript syntax in mounted hook. And there you can take the element height or any other property you want and store in in your data (for example).
Possible solution example #1
Vue transition before-enter event function has the target element as an argument by default. So, you can get the modal height in your beforeTransitionEnter function:
Modal.vue
data() {
return {
modalHeight: 0,
visibility: {
modal: false
}
}
},
methods: {
beforeTransitionEnter(element) {
this.modalHeight = element.offsetHeight;
}
}
Possible solution example #2
For this example there is one important detail. I see in your code example that you display the next div conditionally:
`<div v-if="visibility.modal" class="v--modal v--modal-box">`
Be careful with that, as I don't know what is the init value of visibility.modal.
I would use watch feature to know content height every time once visibility.modal gets true in this particular case.
Look at the example. It's based on the code which you provided and it's a possible solution in your case.
Modal.vue
data() {
return {
isMounted: false,
modalHeight: 0,
visibility: {
modal: false
}
}
},
mounted() {
this.isMounted = true;
},
watch: {
visibility: {
handler(value) {
if (value.modal && this.isMounted) {
this.modalHeight = this.$refs.modal.clientHeight;
}
},
deep: true
}
}
As you can see - you will always have actual modalHeight value and you can use it whenever you want within the Modal.vue component.
Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>