Prevent child elements for receiving click event not working - vue.js

I am trying to make a modal component and dismiss it when I click outside of the component. Here is my current setup:
Auth component with click event set on a div element:
<template> <div>
<transition name="modal">
<div class="modal-mask" #click="$parent.$emit('close')">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">Default Header</slot>
</div>
<div class="model-body">
<slot name="body">Default Body</slot>
</div>
<div class="modal-footer">
<slot name="footer">Default Footer</slot>
</div>
</div>
</div>
</div>
</transition> </div> </template>
SignIn component that injects necessary information:
<template>
<div>
<Auth />
</div>
</template>
Home component that uses the SignIn component:
<template>
<div class="home">
<SignIn v-if="showModal" #close="showModal = false" />
</div>
</template>
Right now when I click outside the modal it behaves ok, the close event is called.
But it is also called when I click inside the modal.
Not I tried to use #click.self , but now it doesn't work anymore even when clicking outside the modal.
<div class="modal-mask" #click.self="$parent.$emit('close')">
I am currently learning VueJs, but I don't understand how this works. I thought self will prevent propagating click event to child elements and thats it.
Anyone has an idea what is going on ?
PS: I am using this setup, because I want to have a SignIn and SignUp using the Auth component.

Either <div class="modal-wrapper"> or <div class="modal-container"> needs #click.prevent.stop
<template>
<div>
<transition name="modal">
<div class="modal-mask" #click="$parent.$emit('close')">
<div class="modal-wrapper">
<div class="modal-container" #click.prevent.stop>
<div class="modal-header">
<slot name="header">Default Header</slot>
</div>
<div class="model-body">
<slot name="body">Default Body</slot>
</div>
<div class="modal-footer">
<slot name="footer">Default Footer</slot>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>

With this code you don't have to worry about click event's propagation #click.stop, for the style purpose I am using bootstrap.css but you can write your own style.
Here is the reusable component BsModal.vue
<template lang="pug">
div(v-if="showModal")
.modal.fade.d-block(tabindex='-1', role='dialog', :class="{'show': addShowClassToModal}")
.modal-dialog(role='document')
.modal-content.border-0
.modal-header.border-0
h5.modal-title
slot(name="title")
button.close(type='button', data-dismiss='modal', aria-label='Close', #click="hideModal")
span ×
.modal-body.p-0
slot
.modal-backdrop.fade(:class="{ 'show': addShowClassToModalBackdrop }")
</template>
<script>
export default {
name: 'BsModal',
props: {
showModal: {
default: false,
type: Boolean,
},
},
data() {
return {
addShowClassToModal: false,
addShowClassToModalBackdrop: false,
};
},
mounted() {
this.toggleBodyClass('addClass', 'modal-open');
setTimeout(() => {
this.addShowClassToModalBackdrop = true;
}, 100);
setTimeout(() => {
this.addShowClassToModal = true;
}, 400);
},
destroyed() {
this.toggleBodyClass('removeClass', 'modal-open');
},
methods: {
hideModal() {
setTimeout(() => {
this.addShowClassToModal = false;
}, 100);
setTimeout(() => {
this.addShowClassToModalBackdrop = false;
this.$emit('hide-modal', false);
}, 400);
},
toggleBodyClass(addRemoveClass, className) {
const elBody = document.body;
if (addRemoveClass === 'addClass') {
elBody.classList.add(className);
} else {
elBody.classList.remove(className);
}
},
},
};
</script>
And use it wherever you need by importing it:
<template lang="pug">
div
button(#click="showModal = true")
| Show Modal
bs-modal(
v-if="showModal",
:show-modal="showModal",
#hide-modal="showModal = false"
).modal
template(slot="title") Modal Title
// Modal Body content here
</template>
<script>
import BsModal from '~/components/BsModal.vue';
export default {
name: 'your component',
components: { BsModal },
data() {
return {
showModal: false,
};
},
};
</script>
If you don't like pug template language then you can convert PUG to HTML here: https://pughtml.com/

Related

There is no div b in page dom, why?

<template>
<div class="test">
<template v-if="arr && arr.length">
<div class="a">rendered in dom</div>
<div class="b">
<span>not render</span>
<el-button>but this child component is created, we can see it in vue-devtool</el-button>
</div>
</template>
<div class="c" v-else>test</div>
<el-dialog append-to-body :modal="false" :visible="visible" #close="visible = false">1</el-dialog>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
arr: [],
visible: true
};
},
mounted() {
this.arr = [1, 2];
}
};
</script>
There are many solutions for this, e.g:
add key, change a different tag, and so on, but why ?
Most important, if delete the dialog, it could run with expectations!

VueJs - pass slot from child component to parent

I am having difficulty to make use of named slot from children in parent component.
So I am making tabs component with main Tabs and child Tab components.
My Tabs:
<template>
<div class="tabsMainContainer">
<div class="tabsContainer">
<ul class="tabsHeader">
<li
v-for="(tab, index) in tabs"
:key="tab.title"
:class="{tabActive: (index == selectedIndex)}"
#click="selectTab(index)"
>
<slot name="icon"/>
{{ tab.title }}
</li>
</ul>
</div>
<div class="tabsContent"><slot/></div>
</div>
</template>
I get tabs object with this.$children
and then my Tab component:
<template>
<div v-show="isActive" class="tab">
<slot name="icon"/>
<slot/>
</div>
</template>
And use of that:
<Tabs class="tabsComponent">
<Tab title="first">
<template v-slot:icon>Icon</template>
Content
</Tab>
<Tab title="second">Content second</Tab>
</Tabs >
while Content as such works fine, it appears in default slot, the icon slot also appears in default slot in main Tab component. How can I get icon slot from children, and display it in icon slot of main Tab component?
I also had a same kind of requirement. What I did was adding slot inside the main component's slot with the same name
In your Tab component
<template>
<div v-show="isActive" class="tab">
<template v-slot:icon>
<slot name="icon" />
</template>
</div>
</template>
First of all, I use google translate. Sorry, I may have spelled it wrong.
https://stackoverflow.com/a/64568036
I got an example from #ingrid-oberbüchler.
My solution
Layout.vue
<Tabs class="tabsComponent">
<Tab title="first">
<template v-slot:icon>Icon</template>
Content
</Tab>
<Tab title="second">Content second</Tab>
</Tabs>
Tabs.vue
<template>
<div class="title">
{{ tabsProvider.tabs[tabsProvider.selectedIndex].props.title }}
</div>
<div class="content">
<slot/>
</div>
<div class="icon">
<component :is="tabsProvider.tabs[tabsProvider.selectedIndex].children.icon"/>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
tabsProvider: {
selectedIndex: 0,
tabs: [],
count: 0,
},
};
},
provide() {
return {
$tabsProvider: this.tabsProvider,
};
},
created() {
this.tabsProvider.tabs = this.$slots.default().filter((child) => child.type.name === "Tab");
},
}
</script>
Tab.vue
<template>
<div v-show="isActive">
<slot />
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "Tab",
props: {
title: {
type: String,
},
},
inject: ["$tabsProvider"],
data() {
return {
index: 0,
isActive: false,
};
},
beforeMount() {
this.index = this.$tabsProvider.count;
this.$tabsProvider.count++;
this.isActive = this.index === this.$tabsProvider.selectedIndex;
},
watch: {
"$tabsProvider.selectedIndex": {
handler(val, oldVal) {
this.isActive = this.index === this.$tabsProvider.selectedIndex;
},
deep: true,
},
},
});
</script>
Simplest example
let's admit x component is parent
<div class="x">x component</div>
let's admit y component is child
<div class="y">y component</div>
How to be process?
<Ycomponent>
<slot name="example1"/>
<slot name="example2"/>
<slot name="example3"/>
</Ycomponent>
<Xcomponent>
<Ycomponent>
<button slot="example1">button</button>
<span slot="example2">Span</span>
<img slot="example3" src="/"/>
</Ycomponent>
</Xcomponent>
I hope I could help.

Is it possible to call a function in one vue component from another vue component?

LogoutModal.vue
<template>
<div class="modal LogoutModal" v-bind:class="{'is-active':confirmLogout}">
<div class="modal-background"></div>
<div class="modal-content">
</div>
<button class="modal-close is-large" aria-label="close" #click="closeModal"></button>
</div>
</template>
<script>
export default {
name: "LogoutModal",
data() {
return {
confirmLogout: false
};
},
methods: {
closeModal: function() {
this.confirmLogout = false;
},
showModal: function() {
this.confirmLogout = true;
}
}
};
</script>
Navigation.vue
<template>
<aside class="menu">
<ul class="menu-list">
<li>
<a #click="showModal">Logout</a>
</li>
</ul>
<LogoutModal />
</aside>
</template>
<script>
import LogoutModal from "#/components/LogoutModal.vue";
export default {
name: "Navigation",
components: {
LogoutModal
}
};
</script>
I want to call the showModal function when i click on the Logout link. How can i achive that?
Or is that possible to change the variable in LogoutModal.vue from Navigation.vue. I have a variable called confirmLogout in LogoutModal.vue it has to be updated from Navigation.vue. How can I do that?
You should have confirmLogout in the navigation component, then pass that into your modal, you will also need a way to close the modal so in your modal you should this.$emit('close') when your user has signaled they want to close it.
<LogoutModal :open="confirmLogout" #close="confirmLogout=false" />
In your logout modal add a prop
<script>
export default {
name: "LogoutModal",
props: ['open'],
...
then in the template
<div class="modal LogoutModal" v-bind:class="{'is-active':open}">
You could define a dynamic property in LogoutModal, that sets it's state.
Here's more about that: https://v2.vuejs.org/v2/guide/components-props.html

Vue Eventbus: handler.apply is not a function

I want to reload one of my components when an other component changes (for example I send a put with axios)
I tried it with eventbus but I got this error:
handler.apply is not a function
In the component where I want to trigger:
EventBus.$emit('compose-reload', Math.random()*100);
Where I want to be triggered:
<template>
<div class="">
<div class="">
<div class="columns">
<div class="column is-one-fifth">
<div class="box">
<Menu></Menu>
</div>
</div>
<div class="column is-four-fifth">
<div class="box">
<router-view :key="key"></router-view>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Menu from './includes/Menu'
import EventBus from '../../../event-bus';
export default {
components: {
Menu,
},
data() {
return {
key: null
}
},
mounted(){
EventBus.$on('compose-reload', this.key);
},
created(){
this.key = Math.random()*100
}
}
</script>
EventBus.$on expects a handler function as a second argument but the variable this.key is passed in, hence the error.
You should change this :
mounted(){
EventBus.$on('compose-reload', this.key);
}
To this :
mounted(){
EventBus.$on('compose-reload', key => {
this.key = key;
});
}

Conditional rendering template on Vue?

I have a simple situation that I want to render cart-badge based on window width value but it always show the last one even though windowWidth does change the value. Please advise me what to do !
<template v-if="windowWidth>=1024">
<div class="nav-user-account"> {{windowWidth}}
<div class="nav-cart nav-cart-box">
<span class="text hidden-sm">Cart</span>
<span class="cart-number" id="nav-cart-num">{{cartItemCount}}</span>
</div>
</div>
</template>
<template v-if="windowWidth<1024">
<a href="#" style="color:#ff6a00" class="icon-cart">
{{windowWidth}}
<i class="fa fa-shopping-cart fa-2x" aria-hidden="true"></i>
<span class="cart-num">2</span>
</a>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {
windowWidth: window.innerWidth,
}
},
computed: {
...mapGetters('cart', {
cartItemCount: 'getShoppingCartItemsCount'
})
},
mounted() {
this.$nextTick(() => {
window.addEventListener('resize', () => {
this.windowWidth= window.innerWidth
});
})
},
}
</script>
UPDATED: as Tony19 pointed out, i need a master template outside of these 2 template.
<template>
<div>
<template v-if="windowWidth>=1024">...</template>
<template v-else></template>
</div>
</template>