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

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;
})

Related

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

Using Event Names stored in a variable in VueJs

In VueJS, a child component can emit an event, for example:
this.$emit('toggle-button')
In the parent, we can listen to this event as follows:
<my-component v-on:toggle-button="doSomething"></my-component>
This works great. But I have a requirement where the event name (in this case, toggle-button) is stored in a variable or Vuex store. So, I don't have the exact event name but a variable or store with the name of the event.
In such a case what would be the syntax for referring that that event name in the on-click directive?
For example, let say we have:
let eventName = 'toggle-button'
How can I use this variable (eventName) instead of the exact event name (toggle-button) in the following:
<my-component v-on:toggle-button="doSomething"></my-component>
You could use $on(EVENT_NAME, CALLBACK) in this case:
// <my-component ref="foo" />
this.$refs.foo.$on(eventName, doSomething)
Vue.component('my-component', {
template: `<button #click="$emit('click', $event)">Click</button>`
});
new Vue({
el: '#app',
mounted() {
const eventName = 'click';
this.$refs.foo.$on(eventName, this.doSomething);
},
methods: {
doSomething() {
alert('clicked');
}
}
})
<script src="https://unpkg.com/vue#2.5.17"></script>
<div id="app">
<my-component ref="foo" />
</div>
If you render your component using separate render function, this is easy:
export default {
props: {
eventName: {
type: String,
},
},
render: function (createElement) {
return createElement('my-component', {
on: {
[this.eventName]: (event) => {
console.log('Received event!')
}
}
})
}
}
Render functions also come with ability to dynamically specify a name for the component, if that is also required for your app

Vue directive not triggering method

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?
}
});
}
});

Can a vue component know if a listener is listening?

Say I have a modal dialogue as a Vue component. Sometimes I want OK and Cancel. Sometimes, I just want OK. The cleanest way I can think to do this would be for my component to only display Cancel when it's caller is listening for cancel events. Can this be done?
jsfiddle
markup
<div id="vue-root">
<confirm-modal v-on:ok="handleOk"></confirm-modal>
<confirm-modal v-on:ok="handleOk" v-on:cancel="handleCancel"></confirm-modal>
</div>
code
Vue.component('confirm-modal',{
template : `
<div class="confirm-modal">
Are you sure<br>
<button v-on:click="$emit('ok',{})">OK</button>
<button v-if="'HOW DO I TEST IF CANCEL WILL BE CAPTURED???'" v-on:click="$emit('cancel',{})">Cancel</button
</div>
`,
})
vm = new Vue({
el : '#vue-root',
methods : {
handleOk : function(){
alert('OK already');
},
handleCancel : function(){
}
}
})
First you can emit an event without value and the parent will catch it. You dont need this empty Object.
What i understood from your question is this:
If you to track in confirm-modal component if the cancel button is clicked ?
Then do this.
Vue.component('confirm-modal',{
template : `
<div class="confirm-modal">
Are you sure<br>
<button v-on:click="$emit('ok',{})">OK</button>
<button v-if="isCaptured" v-on:click="cancelClick">Cancel</button
</div>
`,
data: function () {
return {
isCaptured: false,
};
},
methods: {
cancelClick: function() {
this.isCaptured = true;
// or this.$emit('cancel'); then pass prop from parent
},
}
})
vm = new Vue({
el : '#vue-root',
methods : {
handleOk : function(){
alert('OK already');
},
handleCancel : function(){
}
}
})
The easy way to do this, you can pass props from parent to child component when you want to show ok and cancel button.
Here you have some more about props
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-with-Props

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>