Vue Transition on Element When Route Changes - vue.js

Context:
I have a navigation component which is present on every page but there is a logo element in that component which I am removing based on what route the user is at. I want to add a transition effect to this element when it disappears/appears and I have tried to do so using Vue transitions as you can see down below.
Problem is:
Element only fades in when this.header goes from false to true - whenever it goes from true to false no animation happens.
You can look at the problem for yourself in this code sandbox
Sidenote:
The CSS is not the problem. I know this because the desired effect works perfectly well if I instead trigger it using a button. The problem seems to have something to do with the nature of using a router change to trigger the animation. Do any of you have any idea why this would be the case?
<template>
<div class="headerNav">
<transition name="fade">
<div class="logo" v-if="!this.logo"></div>
</transition>
</div>
</template>
<script>
export default {
name: 'Navbar',
components: {
postFilter,
},
data() {
return {
logo: null,
}
},
mounted() {
//changing around the header depending on the page we are on so that we can use one header for all pages
if (this.$route.name == 'Library' || this.$route.name == 'Profile') {
this.logo = false
} else {
this.logo = true
}
}
</script>
The CSS (this should not be a problem but I included it anyway)
.fade-enter-active {
transition: all .3s ease;
}
.fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.fade-enter, .fade-leave-to {
transform: translateY(10px); opacity: 0;
}

Based on the comments and codesandboxes, here is a working version: https://codesandbox.io/s/olz3jrk829
Two main changes:
Instead of
<transition name="fade">
<div class="logo" v-if="page"></div>
</transition>
<transition name="fade">
<div class="logo two" v-if="!page"></div>
</transition>
Combine both div in one transition. Vue needs keys to determine each section:
<transition name="fade">
<div v-if="page" class="logo" key="1">12</div>
<div v-else class="logo two" key="2">34</div>
</transition>
Use the computed function instead of mounted:
computed: {
page() {
if (this.$route.name == 'otherpage') {
return false
} else {
return true
}
}
}
Most importantly, you reuse your navigation in each component (homeand otherpage in your example), so the leave transition doesn't get triggered from mounting.
The right way would be to remove the navigation component from the home and otherpage component, so it gets used only once in App.vue, which let all other components share one navigation instance.
Here is your original question with the changes:
<template>
<div class="headerNav">
<transition name="fade">
<div v-if="logo" class="logo" key="1"></div>
<div v-else class="logo two" key="2"></div>
</transition>
</div>
</template>
<script>
export default {
name: 'Navbar',
components: {
postFilter,
},
data() {
return {}
},
computed: {
logo() {
if (this.$route.name == 'Library' || this.$route.name == 'Profile') {
return false
} else {
return true
}
}
}
</script>

Related

Why transition does not work in my Vue components?

I am building a Vue app. I have two components that are related to each other. One of them is called loginRegister.vue and the code of that is here:
loginRegister.vue:
<template>
<BaseModal idBtn = "regis" btnOneText="Cancel" :btnTwoText="button2Text['register']" v-show="showModal1" #close="showModal1 = false" #submitForm="registerUser(regLogPara['register'])" :popoverVue = "showToolTip.register">
<!-- the material that must be shown in modal comes here. it is better to use bootstrap "card" classes for compatible design -->
<!-- ################ -->
<!-- transition part -->
<!-- ################ -->
<transition name="fade">
<div v-if="modalAlert==='form'" key="item1">
<h4 class="card-title text-center my-3">Create Your Account</h4>
</div>
<!-- showing success message -->
<div v-else-if="modalAlert==='success'" key="item2">
<p>you succeed</p>
</div>
<!-- showing error message -->
<div v-else key="item3">
<p>there is an error!</p>
</div>
</transition>
</BaseModal>
</template>
<script>
import BaseButton from './BaseButton.vue';
import BaseInput from './BaseInput.vue'
import BaseModal from './BaseModal.vue';
import { onMounted, reactive, ref } from 'vue';
export default {
name: "loginRegister",
components: {
BaseModal,
BaseButton,
BaseInput
},
setup(props) {
const showModal = ref(false);
const showModal1 = ref(false);
const modalAlert = ref("form");
const registerUser = async (checkPara) => {
/* This function is responsible for sending form data to backend and getting the result from backend */
if (checkPara == "reset") {
/* for showing form again if there is a back-end error */
console.log("reset form");
modalAlert.value = "form";
button2Text["login"] = "Submit";
button2Text["register"] = "Submit";
regLogPara["login"] = "login";
regLogPara["register"] = "register";
} else {
/* for submiting form */
modalAlert.value = "error";
button2Text[checkPara] = "Try again";
regLogPara[checkPara] = "reset";
console.log("register function");
}
}
return {
registerUser,
showModal,
showModal1,
validation,
blurInput,
loginValid,
registerValid,
finalCheck,
showToolTip,
store,
modalAlert,
button2Text,
regLogPara
}
}
};
</script>
The other one called BaseModal.vue and the code of that is here:
BaseModal.vue:
<template>
<div class="modal-overlay container-fluid p-0">
<div class="row align-items-center justify-content-center" #click.self="$emit('close')">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<!-- text and other html comes here in the slot -->
<!-- ############## -->
<!-- this is the slot part -->
<!-- ############## -->
<slot></slot>
<div class="card-footer d-flex justify-content-around align-items-center">
<base-button
large
v-if="btnOneText"
kind = "btn-secondary"
:textBtn = "btnOneText"
#click="$emit('close')"
>
</base-button>
<base-button
large
v-if="btnTwoText"
kind = "btn-secondary"
:textBtn = "btnTwoText"
#click="popoverFunc"
#blur="popoverDisable"
:id="idBtn"
>
</base-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import BaseButton from './BaseButton.vue';
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js";
export default {
// $emit('submitForm'),
name: "BaseModal",
props: {
btnOneText: {
type: String
},
btnTwoText: {
type: String
},
popoverVue: {
type: Boolean
},
idBtn: {
type: String
}
},
computed: {
popOverData: function () {
return this.popoverVue;
},
popVar: function() {
return new bootstrap.Popover(document.getElementById(this.idBtn), {
trigger: "manual",
title: "Notice:",
content: "Please fill all fields in the correct way to submit your information!",
customClass: "myPopover",
placement: "top"
})
}
},
data() {
return {
showPopOver: false
}
},
emits: ["close", "submitForm"],
components: {
BaseButton
},
watch: {
popOverData: function (newState, oldState) {
if(newState === true) {
this.showPopOver = true;
this.popoverFinal();
} else {
this.showPopOver = false;
this.popoverFinal();
}
}
},
methods: {
popoverFunc: function() {
this.$emit('submitForm');
if (this.popOverData) {
this.showPopOver = true;
this.popoverFinal();
}
},
popoverDisable: function() {
console.log("popoverDisable");
this.showPopOver = false;
this.popoverFinal();
},
popoverFinal: function() {
console.log(this.showPopOver);
/* this function is responsible for showing and hiding the popover according to "showPopOver" data */
if (this.showPopOver) {
this.popVar.show();
} else {
this.popVar.hide();
}
}
}
};
</script>
<style scoped src="../assets/css/compoStyles/baseModal.css"></style>
There are some codes that are not related to this question and also I tried to simplify the codes and clarify the parts that are related to transition and slot in my components code.
Although the codes may seem long or complicated, the goal that I want to reach is simple. I want to submit a register form in a modal component. Actually the user in my app clicks on register button and then the BaseModal.vue component is shown. In that case the register form (that for simplicity I substitute that with a h4 tag) is the default thing that user could see. After submitting the form according that the process is successful or there is an error, I want to show a message to the user and change the text of button from submit to try again if there is an error. After that when the user clicks try again button the form (h4 tag) must be fade in again. So I tried the v-if/v-else-if/v-else structure of Vue in my loginRegister.vue component. The code that I used in transition part is similar to the code that Vue documentation is used, But the transition does not work correctly. In my local development environment, the h4 tag disappears smoothly and then no message is shown, after clicking try again the h4 tag fade in again. Also in the console I could see this warning:
[Vue warn]: <transition> can only be used on a single element or component. Use <transition-group> for lists.
at <BaseTransition mode=undefined appear=false persisted=false ... >
at <Transition name="fade" >
at <BaseModal idBtn="regis" btnOneText="Cancel" btnTwoText="Try again" ... >
at <LoginRegister>
at <App>
But I don't think that is related to my issue, because I did not use transition on multi elements in my app. So could anyone please help me that what is wrong in my codes?
It seems like you want baseModal.vue to be inside loginRegister.vue and the baseModal to have some kind of transition whenever some state changes? Try changing <transition></transition> into <transition-group></transition-group>

Render a child component in the global app component

I wrote a dialog component (global) to show modal dialogs with overlays like popup forms.
Right now the dialog gets rendered inside the component where it is used. This leads to overlapping content, if there is something with position relative in the html code afterwards.
I want it to be rendered in the root App component at the very end so I can force the dialog to be always ontop of every other content.
This is my not working solution:
I tried to use named slots, hoping, that they work backwards in the component tree too. Unfortunately they don't seem to do that.
Anybody a solution how to do it?
My next idea would be to render with an extra component that is stored in the app component and register the dialogs in the global state. But that solution would be super complicated and looks kinda dirty.
The dialog component:
<template v-slot:dialogs>
<div class="dialog" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if(!this.persistent) {
this.$emit('close')
}
}
}
}
</script>
The template of the app component:
<template>
<div class="application">
<div class="background">
<div class="satellite"></div>
<div class="car car-lr" :style="{ transform: `translateY(${car.x}px)`, left: adjustedLRLeft + '%' }" v-for="car in carsLR"></div>
</div>
<div class="content">
<login v-if="!$store.state.user"/>
<template v-else>
<main-menu :show-menu="showMainMenu" #close="showMainMenu = false"/>
<router-view/>
</template>
<notifications/>
<div class="dialogs"><slot name="dialogs"></slot></div>
</div>
</div>
</template>
Another possibility is to use portals. These provide a way to move any element to any place in the dom. Checkout the following library: https://github.com/LinusBorg/portal-vue
You can just place the dialog component directly in the app component and handle the dialog logic/which dialog to display in that component?
In case you want to trigger these dialogs from other places in your app, this would would be a good use case for vuex! That, combined with dynamic webpack imports is how I handle this.
With the help of the guys from the vuetify2 project, I found the solution. The dialog component gets an ref="dialogContent" attribute and the magic happens inside the beforeMount function.
<template>
<div class="dialog" ref="dialogContent" :class="{'dialog--open': show, 'dialog--fullscreen': fullscreen }">
<transition name="dialogfade" duration="300">
<div class="dialog__overlay" v-if="show && !fullscreen" :key="'overlay'" #click="close"></div>
</transition>
<transition name="dialogzoom" duration="300">
<div class="dialog__content" :style="{'max-width': maxWidth}" v-if="show" :key="'content'">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "MyDialog",
props: {
show: {
type: Boolean,
default: false
},
persistent: {
type: Boolean,
default: true
},
fullscreen: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '600px'
}
},
data: () => ({}),
methods: {
close() {
if (!this.persistent) {
this.$emit('close')
}
}
},
beforeMount() {
this.$nextTick(() => {
const target = document.getElementById('dialogs');
target.appendChild(
this.$refs.dialogContent
)
})
},
}
</script>

VueJS: #click.native.stop = "" possible?

I have several nested components on the page with parents component having #click.native implementation. Therefore when I click on the area occupied by a child component (living inside parent), both click actions executed (parent and all nested children) for example
<products>
<product-details>
<slide-show>
<media-manager>
<modal-dialog>
<product-details>
<slide-show>
<media-manager>
<modal-dialog>
</products>
So I have a list of multiple products, and when I click on "canvas" belonging to modal dialog - I also get #click.native fired on product-details to which modal-dialog belongs. Would be nice to have something like #click.native.stop="code", is this possible?
Right now I have to do this:
#click.native="clickHandler"
and then
methods: {
clickHandler(e) {
e.stopPropagation();
console.log(e);
}
code
<template>
<div class="media-manager">
<div v-if="!getMedia">
<h1>When you're ready please upload a new image</h1>
<a href="#"
class="btn btn--diagonal btn--orange"
#click="upload=true">Upload Here</a>
</div>
<img :src="getMedia.media_url"
#click="upload=true"
v-if="getMedia">
<br>
<a class="arrow-btn"
#click="upload=true"
v-if="getMedia">Add more images</a>
<!-- use the modal component, pass in the prop -->
<ModalDialog
v-if="upload"
#click.native="clickHandler"
#close="upload=false">
<h3 slot="header">Upload Images</h3>
<p slot="body">Hello World</p>
</ModalDialog>
</div>
</template>
<script>
import ModalDialog from '#/components/common/ModalDialog';
export default {
components: {
ModalDialog,
},
props: {
files: {
default: () => [],
type: Array,
},
},
data() {
return {
upload: false,
}
},
computed: {
/**
* Obtain single image from the media array
*/
getMedia() {
const [
media,
] = this.files;
return media;
},
},
methods: {
clickHandler(e) {
e.stopPropagation();
console.log(e);
}
}
};
</script>
<style lang="scss" scoped>
.media-manager img {
max-width: 100%;
height: auto;
}
a {
cursor: pointer;
}
</style>
Did you check the manual? https://v2.vuejs.org/v2/guide/events.html
There is #click.stop="" or #click.stop.prevent=""
So you don't need to use this
methods: {
clickHandler(e) {
e.stopPropagation();
console.log(e);
}
}
In the Vue, modifiers can be chained. So, you are free to use modifiers like this:
#click.native.prevent or #click.stop.prevent
<my-component #click.native.prevent="doSomething"></my-component>
Check events
I had the same problem. I fixed the issue by using following:
<MyComponent #click.native.prevent="myFunction(params)" />

Move elements passed into a component using a slot

I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>

Vuejs: Using keep-alive (Or something similar) on a slot

I've got a combination of tree hierarchy and tabs in vue. So far I've gotten the unfolding more or less working.
I need to remove things from the dom entirely when they're closed, because the data I'm talking about is large enough to bring a browser to its knees if it's all just left in the dom as display:none.
Take a look at this example:
Vue.component('tabs', {
template: '#tabs',
data(){
return {
tabs: [],
expanded:true,
defaultExpanded:true,
activeTab: null,
hasChildren:false,
};
},
methods: {
toggle() {
this.expanded = !this.expanded;
},
activate(tab) {
if (this.activeTab) {
this.activeTab.active = false;
}
tab.active = true;
this.activeTab = tab;
},
},
mounted(){
for (i = 0; i < this.$slots.default.length; i++) {
let t = this.$slots.default[i];
if (t.componentOptions && t.componentOptions.tag == 'tab') {
this.tabs.push(t.componentInstance);
}
}
if (this.tabs.length) {
this.activeTab = this.tabs[0];
this.activeTab.active = true;
}
this.expanded = this.defaultExpanded;
},
});
Vue.component('tab', {
template: '#tab',
data() {
return {
active: false,
};
},
props: ['label'],
});
app = new Vue({
'el': '#inst',
});
<!-- templates -->
<script type="text/x-template" id="tabs">
<div #click.stop="toggle">
<h1><slot name="h" /></h1>
<div v-show="expanded" class="children">
<ul><li v-for="tab in tabs" #click.stop="activate(tab)">{{tab.label}}</li></ul>
<div style="border:1px solid #F00"><slot /></div>
</div>
</script>
<script type="text/x-template" id="tab">
<strong v-show="active"><slot /></strong>
</script>
<!-- data -->
<tabs id="inst">
<div slot="h">Woot</div>
<tab label="label">
<tabs>
<div slot="h">Weet</div>
<tab label="sub">Weetley</tab>
</tabs>
</tab>
<tab label="label2">Woot3</tab>
</tabs>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
This works fine, but if I change the v-show to v-if for performance, it loses state, tab buttons stop showing - basically lots of stuff breaks.
The problem is that as soon as I add v-if to the tab template's slot the entire component is removed when it's closed. This means the parent component's tabs list is a completely different bunch of objects than the ones that show up when it's opened a second time.
This means I can't click on a label to open a tab, since the tabs will be different instances by the time I get to them, and all the tabs will default to closed every time I close and open the parent.
What I really need is something like <keep-alive> - where I could tell vue to keep the components alive in memory without rendering them to the dom. But when I add that the entire thing stops working. It seems like it really doesn't work on slots, only on individual components.
So. tl;dr: How do I maintain the state of mixed trees and tabs while using v-if to keep the dom light?
Building on Bert Evans' codepen, I created a component that is just a slot. I made a keep-alive-wrapped dynamic component that is the slot-component when active and a blank component when not. Now there is no v-if and state is preserved in the children when you close and re-open the parent.
console.clear();
Vue.component('keepableSlot', {
template: '#keepable-slot'
});
Vue.component('tabs', {
template: '#tabs',
data() {
return {
tabs: [],
expanded: true,
activeTab: null,
};
},
methods: {
addTab(tab) {
this.tabs.push(tab)
},
toggle() {
this.expanded = !this.expanded;
},
activate(tab) {
if (this.activeTab) {
this.activeTab.active = false;
}
tab.active = true;
this.activeTab = tab;
},
},
watch: {
expanded(newValue) {
console.log(this.$el, "expanded=", newValue);
}
}
});
Vue.component('tab', {
props: ["label"],
template: '#tab',
data() {
return {
active: false
}
},
created() {
this.$parent.$parent.addTab(this)
}
});
app = new Vue({
'el': '#inst',
});
.clickable-tab {
background-color: cyan;
border-radius: 5px;
margin: 2px 0;
padding: 5px;
}
.toggler {
background-color: lightgray;
border-radius: 5px;
margin: 2px 0;
padding: 5px;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<script type="text/x-template" id="tabs">
<div>
<h1 class="toggler" #click.stop="toggle">
<slot name="h"></slot>
(expanded={{expanded}})
</h1>
<keep-alive>
<component :is="expanded && 'keepableSlot'">
<div class="children">
<ul>
<li class="clickable-tab" v-for="tab in tabs" #click.stop="activate(tab)">{{tab.label}}</li>
</ul>
<div>
<slot></slot>
</div>
</div>
</component>
</keep-alive>
</div>
</script>
<script type="text/x-template" id="keepable-slot">
<div>
<slot></slot>
</div>
</script>
<script type="text/x-template" id="tab">
<strong>
<component :is="active && 'keepableSlot'"><slot></slot></component>
</div>
</script>
<!-- data -->
<tabs id="inst">
<div slot="h">Woot</div>
<tab label="label">
<tabs>
<div slot="h">Weet</div>
<tab label="sub">Weetley</tab>
</tabs>
</tab>
<tab label="label2">Woot3</tab>
</tabs>