Vue directive not triggering method - vue.js

I have a custom vue directive.
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
document.addEventListener(clickHandler, (event) => {
const clickedInsideDropdown = el.contains(event.target);
if (!clickedInsideDropdown && el.classList.contains(openClass)) {
vnode.context.$emit(binding.expression);
}
});
}
});
I then initialize it with the dropdown template:
<template>
<div class="dropdown" :class="{ '-is-open': open }" v-click-outside="close">
<span #click="toggle">
<slot name="toggle"></slot>
</span>
<slot name="menu"></slot>
</div>
</template>
The supporting logic is functioning as expected as well:
<script>
export default {
data: function () {
return {
open: false
}
},
methods: {
close: function () {
this.open = false;
console.log('close');
},
toggle: function () {
this.open = !this.open;
console.log('toggle');
}
}
}
</script>
The Problem
The event should fire when the current dropdown _is open and none of the items inside of it are clicked - which is does (console logging confirms this). However, the $emit is not triggering the close method for some reason.
The event is being emitted in the Vue devtools as expected.
Vue version 2.5.3

Credits to Linus Borg who answered my question for me on the forum. Was just understanding the purpose of events incorrectly.
Events are usually used to communicate from a child component to a parent component, so triggering an event ‘close’ in a componet will not run a method of that name in that component.
If you want that, you have to actually register a listener to that event:
created () {
this.$on('close', this.close /*the name of the method to call */)
}
However, this isn’t really necessary in your case. you are already passing the close method to the directive, so you can run it directly:
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
document.addEventListener(clickHandler, (event) => {
const clickedInsideDropdown = el.contains(event.target);
if (!clickedInsideDropdown && el.classList.contains(openClass)) {
binding.value()
// alternartively, you could also call the method directly on the instance, no need for an event:
vnode.context.[expression]()
// but that wouldn't really be elegant, agreed?
}
});
}
});

Related

Vuejs $emit doesnt work in some part of a function, and in other part works

I am using vuejs3 and trying to emit event from a child component.
child Component
<input type="button" v-if="edition_mode" #click="cancel()" class="btn btn-primary" value="Annuler">
[...]
cancel(){
if(this.new_sav){
this.$emit('test')
}else{
console.log('else')
this.$emit('test')
}
},
Parent Component
<div v-if="creation_form">
<h4>Ajout Nouveau Sav</h4>
<sav-form
:initial_data="null"
:li_product="li_product"
:new_sav="true"
:customer_id="data.customer.id"
#action="form_action"
#test="test()"/>
</div>
[...]
test(){
console.log('test emit works')
}
When cancel() is executed, in the if case $emit() works correctly, but in the else case, only 'else' is printed and $emit is not executed. What I am doing wrong here ?
I also have several buttons in child component, in the same div, that all call differents function but some function 'can' emit event and other can't.
I am not sure what issue you are facing but it is working fine in the below code snippet. Please have a look and let me know if any further clarification/discussion required.
Demo :
Vue.component('child', {
template: '<div><button #click="cancel()">Trigger emit event from child!</button></div>',
data() {
return {
isValid: true
}
},
methods: {
cancel: function() {
this.isValid = !this.isValid;
if (this.isValid) {
console.log('valid');
this.$emit('test');
} else {
console.log('not valid');
this.$emit('test')
}
}
}
});
var vm = new Vue({
el: '#app',
methods: {
getTestEvent() {
console.log('Emit event from child triggered!');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<child #test="getTestEvent"></child>
</div>
I was facing the same issue.
I use Vuex to store authenticated status of the user and my mistake was to use v-if="isAuth" attribute in parent component and the child component set isAuth to false through the store so this.$emit() doesn't work anymore.
Parent Component:
<login-form #onLogin="onLoginHandler" v-if="!isAuth" />
Child Component:
methods: {
example() {
[...]
const response = await axios.post("/login", data);
if (response.status === 200) {
// under the hood isAuth is set to True;
this.$store.dispatch("SET_AUTH", true);
}
// Doesn't work anymore because the parent component v-if is now False.
this.$emit("onLogin", response.data);
}
}

Bind a class in one Vue component's template to a Window event emitted from another Vue component

To be able to emit events and listen for these events in all instances I do this:
window.Event = new Vue();
Then inside a Vue component's template I emit the event "isSelected", like so,
Vue.component('artikel',{
template: '<li style="list-style-type:none;"><input #click="isSelected" type="checkbox" class="kryssruta" /><slot></slot></li>',
methods:{
isSelected(){
Event.$emit('isSelected');
}
}
});
Now in another component, I listen for the event (I stripped out everything but the button):
Vue.component('modal', {
template: `
<button class="button" :class="{grey:toggle}">Lägg till</button>
`,
data(){
return{
toggle: true
}
},
created(){
Event.$on('isSelected', function() {
alert('works!');
this.toggle = false;
})
},
Inside the created() function I listen for the event, and I can confirm that it is recieved with the alert function.
My problem is that I can't figure out how to use the event to toggle the button's class. Anyone?
EDITED:
Thanks to the comment from skirtle, it now works with an arrow function, like so:
created(){
Event.$on('isSelected', () => this.toggle=!this.toggle);
},
But as I understand it arrow functions are not supported by IE, so I would also like to figure out how to use the suggestion about .bind(this) from Joshua Minkler, but I can't get that to work... I get "toggle is not defined".
created(){
Event.$on('isSelected', function() {
toggle.bind(this) = true;
})

Is there any solution for tricking vue's lifecycle hook order of execution?

Destroyed hook is called later than i need.
I tried to use beforeDestroy instead of destroy, mounted hook instead of created. The destroy hook of previous components is always called after the created hook of the components that replaces it.
App.vue
<div id="app">
<component :is="currentComponent"></component>
<button #click="toggleComponent">Toggle component</button>
</div>
</template>
<script>
import A from './components/A.vue';
import B from './components/B.vue';
export default {
components: {
A,
B
},
data: function(){
return {
currentComponent: 'A'
}
},
methods: {
toggleComponent() {
this.currentComponent = this.currentComponent === 'A' ? 'B' : 'A';
}
}
}
</script>
A.vue
<script>
export default {
created: function() {
shortcut.add('Enter', () => {
console.log('Enter pressed from A');
})
},
destroyed: function() {
shortcut.remove('Enter');
}
}
</script>
B.vue
<script>
export default {
created: function() {
shortcut.add('Enter', () => {
console.log('Enter pressed from B');
})
},
destroyed: function() {
shortcut.remove('Enter');
}
}
</script>
Result:
// Click Enter
Enter pressed from A
// now click on toggle component button
// Click Enter again
Enter pressed from A
Expected after the second enter to show me Enter pressed from B.
Please don't show me diagrams with vue's lifecycle, i'm already aware of that, I just need the workaround for this specific case.
Dumb answers like use setTimeout are not accepted.
EDIT: Made some changes to code and description
If you are using vue-router you can use router guards in the component (as well as in the router file) where you have beforeRouteLeave obviously only works where there is a change in route, see here:
https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards

VueJS Custom directive + emit event

I need to $emit an event from a custom directive.
Is it possible?
directive.js:
vnode.context.$emit("myEvent") // nothing append
vnode.child.$emit("myEvent") // error
vnode.parent.$emit("myEvent") // error
component.vue:
<div v-directive.modifier="binding" #myEvent="method()"></div>
Do you know if it's possible or if there is any trick?
Thanks
A <div> is not a VueComponent, which means it doesn't have an $emit method.
So to make your Vue custom directive emit an event, you will have to do some checking first:
If the directive was used in a Vue custom component, then call $emit() of that component's instance
If the directive was used in a regular DOM element (...because there's no $emit()...), then dispatch a native DOM event using .dispatchEvent.
Luckily, Vue's v-on listeners respond to native custom events.
That should be all. Demo implementation below.
Vue.component('my-comp', {
template: `<input value="click me and check the console" size="40">`
});
Vue.directive('my-directive', {
bind: function (el, binding, vnode) {
// say you want to execute your logic when the element is clicked
el.addEventListener('click', function (e) {
var eventName = 'my-event';
var eventData = {myData: 'stuff - ' + binding.expression}
if (vnode.componentInstance) {
vnode.componentInstance.$emit(eventName, {detail: eventData}); // use {detail:} to be uniform
} else {
vnode.elm.dispatchEvent(new CustomEvent(eventName, {detail: eventData}));
}
})
}
})
new Vue({
el: '#app',
methods: {
handleStuff(e) { console.log('my-event received', e.detail); }
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<div id="app">
<div v-my-directive.modifier="'native element'" #my-event="handleStuff">CLICK ME AND CHECK THE CONSOLE</div>
<hr>
<my-comp v-my-directive.modifier="'custom component'" #my-event="handleStuff"></my-comp>
</div>

How to use a global event bus in vue.js 1.0.x?

I am trying to create a event bus with an empty new Vue instance. The app is large enough to be split into multiple files for components. As an example my app is structured as :
main.js
import Vue from vue;
window.bus = new Vue();
Vue.component('update-user', require('./components/update-user');
Vue.component('users-list', require('./components/users-list');
Vue.component('edit-user', require('./components/edit-user');
Vue.component('user-address', require('./components/user-address');
new Vue({
el:'body',
ready(){
}
});
components/update-user.js
export default{
template: require('./update-user.template.html'),
ready(){
bus.$emit('test-event', 'This is a test event from update-user');
}
}
components/users-list.js
export default{
template:require('./users-list.template.html'),
ready(){
bus.$on('test-event', (msg) => { console.log('The event message is: '+msg)});
//outputs The event message is: This is a test event
}
components/edit-user.js
export default{
template:require('./edit-user.template.html'),
ready(){
bus.$on('test-event', (msg) => {console.log('Event message: '+msg)});
//doesn't output anything
console.log(bus) //output shows vue instance with _events containing 'test-event'
}
}
components/user-address.js
export default{
template:require('./user-address.template.html'),
ready(){
bus.$on('test-event', () => {console.log('Event message: ' +msg)});
//doesn't output anything
console.log(bus) //output shows vue instance with _events containing 'test-event'
}
}
index.html
...
<body>
<update-user>
<users-list></users-list>
<edit-user>
<user-address></user-address>
</edit-user>
</update-user>
</body>
...
My question is that why does bus.$on work in the first child component only? Even if I remove the listener from <users-list>, none of the other components are able to listen to the event i.e console.log() with bus.$on doesn't work in any component below/after <users-list> i.e. the immediate child component.
Am I missing something or where am I doing wrong?
How to get this working so that any child component at any depth can listen to an event emitted from even the root component or any where higher up in the hierarchy and vice-versa?
I figured it out and got it working. Posting here to be of help to someone else who hits this question.
Actually there's nothing wrong with the implementation I have mentioned above in the question. I was trying to listen to the event in a component which was not yet rendered (v-if condition was false) when the event was fired. So a second later (after the event was fired) when the component was rendered it could not listen for the event - this is intended behavior in Vue (I got a reply on laracasts forum).
However, I finally implemented it slightly differently (based on a suggestion from Cody Mercer as below:
import Vue from vue;
var bus = new Vue({});
Object.defineProperty(Vue.prototype, $bus, {
get(){
return this.$root.bus;
}
});
Vue.component('update-user', require('./components/update-user');
Vue.component('users-list', require('./components/users-list');
Vue.component('edit-user', require('./components/edit-user');
Vue.component('user-address', require('./components/user-address');
new Vue({
el:'body',
ready(){
},
data:{
bus:bus
}
});
Now to access the event bus from any component I can use this.$bus as
this.$bus.$emit('custom-event', {message:'This is a custom event'});
And I can listen for this event from any other component like
this.$bus.$on('custom-event', event => {
console.log(event.message);
//or I can assign the message to component's data property
this.message = event.message;
//if this event is intended to be handled in other components as well
//then as we normally do we need to return true from here
return true;
});
Event propagation stops when a listener is triggered. If you want the event to continue on, just return true from your listener!
https://vuejs.org/api/#vm-dispatch
bus.$on('test-event', () => {
console.log('Event message: ' +msg);
return true;
});
$emit dispatches the event within the scope of the instance — it doesn't propagate to parents/children. $broadcast will propagate to child components. As mentioned in #Jeff's answer, the intermediate component event callbacks have to return true to allow the event to continue cascading to [their] children.
var child = Vue.extend({
template: '#child-template',
data: function (){
return {
notified: false
}
},
events: {
'global.event': function ( ){
this.notified = true;
return true;
}
}
});
var child_of_child = Vue.extend({
data: function (){
return {
notified: false
}
},
template: '#child-of-child-template',
events: {
'global.event': function ( ){
this.notified = true;
}
}
});
Vue.component( 'child', child );
Vue.component( 'child-of-child', child_of_child );
var parent = new Vue({
el: '#wrapper',
data: {
notified: false
},
methods: {
broadcast: function (){
this.$broadcast( 'global.event' );
},
emit: function (){
this.$emit( 'global.event' );
}
},
events: {
'global.event': function (){
this.notified = true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="wrapper">
<h2>Parent notified: {{ notified }}</h2>
<child></child>
<button #click="broadcast">$broadcast</button>
<button #click="emit">$emit</button>
</div>
<template id="child-template">
<h5>Child Component notified: {{ notified }}</h5>
<child-of-child></child-of-child>
</template>
<template id="child-of-child-template">
<h5>Child of Child Component notified: {{ notified }}</h5>
</template>