VueJS Custom directive + emit event - vue.js

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>

Related

Bind click from instance instead of html tag

In vue is possible to bind button click directly from vue instance?
I have this button:
<el-button #click="alert_me" class="gf-button" type="primary" style="margin-left: 16px;">Button</el-button>
I wan't to remove #click="alert_me" and do like i would normally do with jquery but with vue.
Is it possible?
My Vue Instance:
new Vue({
el: "#app",
data: {
},
methods: {
alert_me() {
alert('Hello from vue!');
}
},
});
Thanks
If you need to attach a click event listener programmatically, it is possible with the classic javascript api:
<template>
<el-button class="gf-button" type="primary">Button</el-button>
</template>
<script>
export default {
mounted () {
// jquery would also work if it's installed.
document.getElementByClassName('gf-button').addEventListener('click', this.alert_me)
},
methods: {
alert_me() {
console.log('alert')
}
}
}
</script>
You could avoid the manual element query from the document with the Vue $refs object.
<template>
<el-button ref="myButton" class="gf-button" type="primary">Button</el-button>
</template>
<script>
export default {
mounted () {
this.$refs.myButton.addEventListener('click', this.alert_me)
},
methods: {
alert_me() {
console.log('alert')
}
}
}
</script>
But if you need that event as soon as the Vue component is created, I wouldn't recommend doing this. It kinda oversee the shadow dom optimisation of Vue.
The #click="" syntax provided is the best way to attach a click listener to an html element.
You can make use of addEventListener and call it in mounted life cycle.
mounted() {
document.querySelector('#element').addEventListener('click', event =>
{
//handle click
}
)
}

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

Vuejs vue-nav-tabs change title of tabs [duplicate]

Let's say I have a main Vue instance that has child components. Is there a way of calling a method belonging to one of these components from outside the Vue instance entirely?
Here is an example:
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
$('#external-button').click(function()
{
vm['my-component'].increaseCount(); // This doesn't work
});
<script src="http://vuejs.org/js/vue.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="app">
<my-component></my-component>
<br>
<button id="external-button">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 5px;">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
So when I click the internal button, the increaseCount() method is bound to its click event so it gets called. There is no way to bind the event to the external button, whose click event I am listening for with jQuery, so I'll need some other way to call increaseCount.
EDIT
It seems this works:
vm.$children[0].increaseCount();
However, this is not a good solution because I am referencing the component by its index in the children array, and with many components this is unlikely to stay constant and the code is less readable.
In the end I opted for using Vue's ref directive. This allows a component to be referenced from the parent for direct access.
E.g.
Have a component registered on my parent instance:
var vm = new Vue({
el: '#app',
components: { 'my-component': myComponent }
});
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Now, elsewhere I can access the component externally
<script>
vm.$refs.foo.doSomething(); //assuming my component has a doSomething() method
</script>
See this fiddle for an example: https://jsfiddle.net/0zefx8o6/
(old example using Vue 1: https://jsfiddle.net/6v7y6msr/)
Edit for Vue3 - Composition API
The child-component has to return the function in setup you want to use in the parent-component otherwise the function is not available to the parent.
Note: <sript setup> doc is not affacted, because it provides all the functions and variables to the template by default.
You can set ref for child components then in parent can call via $refs:
Add ref to child component:
<my-component ref="childref"></my-component>
Add click event to parent:
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component ref="childref"></my-component>
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 2px;" ref="childref">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
For Vue2 this applies:
var bus = new Vue()
// in component A's method
bus.$emit('id-selected', 1)
// in component B's created hook
bus.$on('id-selected', function (id) {
// ...
})
See here for the Vue docs.
And here is more detail on how to set up this event bus exactly.
If you'd like more info on when to use properties, events and/ or centralized state management see this article.
See below comment of Thomas regarding Vue 3.
You can use Vue event system
vm.$broadcast('event-name', args)
and
vm.$on('event-name', function())
Here is the fiddle:
http://jsfiddle.net/hfalucas/wc1gg5v4/59/
A slightly different (simpler) version of the accepted answer:
Have a component registered on the parent instance:
export default {
components: { 'my-component': myComponent }
}
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Access the component method:
<script>
this.$refs.foo.doSomething();
</script>
Say you have a child_method() in the child component:
export default {
methods: {
child_method () {
console.log('I got clicked')
}
}
}
Now you want to execute the child_method from parent component:
<template>
<div>
<button #click="exec">Execute child component</button>
<child-cmp ref="child"></child_cmp> <!-- note the ref="child" here -->
</div>
</template>
export default {
methods: {
exec () { //accessing the child component instance through $refs
this.$refs.child.child_method() //execute the method belongs to the child component
}
}
}
If you want to execute a parent component method from child component:
this.$parent.name_of_method()
NOTE: It is not recommended to access the child and parent component like this.
Instead as best practice use Props & Events for parent-child communication.
If you want communication between components surely use vuex or event bus
Please read this very helpful article
This is a simple way to access a component's methods from other component
// This is external shared (reusable) component, so you can call its methods from other components
export default {
name: 'SharedBase',
methods: {
fetchLocalData: function(module, page){
// .....fetches some data
return { jsonData }
}
}
}
// This is your component where you can call SharedBased component's method(s)
import SharedBase from '[your path to component]';
var sections = [];
export default {
name: 'History',
created: function(){
this.sections = SharedBase.methods['fetchLocalData']('intro', 'history');
}
}
Using Vue 3:
const app = createApp({})
// register an options object
app.component('my-component', {
/* ... */
})
....
// retrieve a registered component
const MyComponent = app.component('my-component')
MyComponent.methods.greet();
https://v3.vuejs.org/api/application-api.html#component
Here is a simple one
this.$children[indexOfComponent].childsMethodName();
I am not sure is it the right way but this one works for me.
First import the component which contains the method you want to call in your component
import myComponent from './MyComponent'
and then call any method of MyCompenent
myComponent.methods.doSomething()
Declare your function in a component like this:
export default {
mounted () {
this.$root.$on('component1', () => {
// do your logic here :D
});
}
};
and call it from any page like this:
this.$root.$emit("component1");
If you're using Vue 3 with <script setup> sugar, note that internal bindings of a component are closed (not visible from outside the component) and you must use defineExpose(see docs) to make them visible from outside. Something like this:
<script setup lang="ts">
const method1 = () => { ... };
const method2 = () => { ... };
defineExpose({
method1,
method2,
});
</script>
Since
Components using are closed by default
Sometimes you want to keep these things contained within your component. Depending on DOM state (the elements you're listening on must exist in DOM when your Vue component is instantiated), you can listen to events on elements outside of your component from within your Vue component. Let's say there is an element outside of your component, and when the user clicks it, you want your component to respond.
In html you have:
Launch the component
...
<my-component></my-component>
In your Vue component:
methods() {
doSomething() {
// do something
}
},
created() {
document.getElementById('outsideLink').addEventListener('click', evt =>
{
this.doSomething();
});
}
I have used a very simple solution. I have included a HTML element, that calls the method, in my Vue Component that I select, using Vanilla JS, and I trigger click!
In the Vue Component, I have included something like the following:
<span data-id="btnReload" #click="fetchTaskList()"><i class="fa fa-refresh"></i></span>
That I use using Vanilla JS:
const btnReload = document.querySelector('[data-id="btnReload"]');
btnReload.click();

Vue.js bind to DOM custom event with dots in name (like bootstrap events)

Using Vue 2.1.10
I can bind to DOM events with v-on directive. For example:
v-on:click
To bind to DOM click.
But I can't figure how to bind to an event that has dots in the name. such as "show.bs.modal" from bootstrap.
Currently, I use a workaround binding in the created hook with Regular DOM Methods, but I really would like to use the declarative syntax for that. How can this be achieved? thanks
EDIT:
The question is about allowed syntax: how can I do something like:
Vue.component('comp',{
template:'<div v-on:show.bs.modal="sunrise"></div',
methods:{
sunrise:function(e){
}
}
})
I was facing the very same problem when working on old projects.
Luckily I found the answer here: vue2 doc
<!-- object syntax (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
This works on Bootstrap 5.1.1 with Vue 2.16.14:
<div class="modal" v-on="{ 'hide.bs.modal': handleModalClose }">
...
</div>
I think dots are not supported in v-on but you could create a custom directive to create an event listener for that event.
Not sure if there is something easier but something like in the demo below or this fiddle should work.
The demo creates a new event with dots in name but that should also work with bootstrap events (not tested). Please let me know if it's not working with bootstrap and I'll have a look.
Unbinding only works if you're using v-if. If you're removing that element with Javascript directly. The event will still be there.
var helloEvent = new Event('demo.event.hello');
document.addEventListener('demo.event.hello', function(e) {
// this is just for testing event dispatching!
console.log('main event listener');
}, false);
const bindCustomEvent = {
getName: function(binding) {
return binding.arg + '.' +
Object.keys(binding.modifiers).map(key => key).join('.');
},
bind: function(el, binding, vnode) {
const eventName = bindCustomEvent.getName(binding);
console.log(el, eventName);
document.addEventListener(eventName, binding.value);
},
unbind: function(el, binding) {
const eventName = bindCustomEvent.getName(binding);
console.log('unbinding', eventName);
document.removeEventListener(eventName, binding.value);
}
};
Vue.directive('bindCustomEvent', bindCustomEvent);
new Vue({
el: '#app',
data() {
return {
enabled: true,
eventMsg: ''
};
},
methods: {
sunrise: function(e) {
console.log('received event');
this.eventMsg = 'received event';
},
testEvent: function() {
document.dispatchEvent(helloEvent);
},
toggle: function() {
console.log('toggle', this.enabled);
this.enabled = !this.enabled;
if (!this.enabled) {
this.eventMsg = '';
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<div id="app">
<div v-bind-custom-event:demo.event.hello="sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>
<!--
The following markup is not working
<div v-on="demo.event.hello:sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>-->
<button #click="testEvent()">
Change
</button>
<button #click="toggle">
<span v-if="enabled">disable custom event</span>
<span v-else>enable custom event</span>
</button>
</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>