Programatically assign handler for native event in Vue JS? - vue.js

I am trying to leverage a Vue mixin to add behavior when a native event happens. Using a mixin will allow me to share that across several components. Specifically, when a field component (or button, or checkbox, etc.) has focus, and the Escape key is pressed, the field loses focus.
A similar Stack Overflow question seemed to indicate I could listen for native events (see code comment about multiple events).
However, the Vue Documentation for programmatically adding an event listener using $on says that it will
Listen for a custom event on the current vm...
(Emphasis added)
Unsure if the custom event remark is absolute or based on the context, I have been experimenting. I have been trying to listen for the native keyup event (using the Vue alias keyup.esc) but have had no success. So I am wondering if it is indeed limited to custom events, and if so, why?
You can see my experiment in a code sandbox. The custom event works, the native does not.
The mixin looks like so:
// escape.mixin.js
export default {
created() {
// Custom event
this.$on("custom-event", function() {
console.log("Custom event handled by mixin");
});
// Native event
this.$on(["keyup.esc", "click"], function() {
alert("Native event handled!");
});
}
};
The main point of all this is to be able to add the behavior to a set of components by adding to how the event is handled, without overriding behavior that might also exist on the component. The secondary goal is to provide the behavior by simply adding the mixin, and not having to do component level wiring of events.
So a component script would look something like this:
// VText component
import escapeMixin from "./escape.mixin";
export default {
name: "VText",
mixins: [escapeMixin],
methods: {
onFocus() {
console.log("Has Focus");
this.$emit("custom-event");
}
}
};
Also, I was trying to avoid attaching the listener to the <input> element directly with vanilla JS because the Vue documentation suggested that letting Vue handle this was a good idea:
[When using v-on...] When a ViewModel is destroyed, all event listeners are automatically removed. You don’t need to worry about cleaning it up yourself.
Solution
skirtle's solution in the comment below did the trick. You can see it working in a code sandbox.
Or here's the relevant mixin:
export default {
mounted() {
this.$el.addEventListener("keyup", escapeBlur);
},
beforeDestroy() {
this.$el.removeEventListener("keyup", escapeBlur);
}
};
function escapeBlur(e) {
if (e.keyCode === 27) {
e.target.blur();
console.log("Lost focus");
}
}

Related

"nativeOn" vs. "on" in the render function & JSX

In the Vue documentation at https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth there is an explanation for “on” and “nativeOn”:
// Event handlers are nested under `on`, though
// modifiers such as in `v-on:keyup.enter` are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},
We are trying to listen to an “input” event from a custom component we created. We noticed that the event did not get detected in the on property, so we tried nativeOn and we were surprised to find that this worked. We were surprised because the docs say nativeOn:
Allows you to listen to
// native events, rather than events emitted from
// the component using vm.$emit
In this case, we are using an event emitted from a (custom) component using vm.$emit.
Here is a snippet from our code demonstrating the above:
on: {
input: (event) => {
console.log('hi'); // We did not receive "hi" in the console
}
},
nativeOn: {
input: (event) => {
console.log('hi2'); // We did receive "hi2" in the console
}
Any clarification on why we would need nativeOn to listen to “input” events from a custom component, or on when to use nativeOn vs. on and the differences therein would be most appreciated. Thanks in advance!

Closing bootstrap vue modal doesn't unbind event

I have a bootstrap vue modal on a view page. When save is clicked, the save function emits an event. Works fine. When i close the modal and open it again then click on save, the save function is handled as expected emitting the function, however, it emits it twice (once for each time the modal was opened and closed. If i open and close the modal 5 times then click save, it calls the save function once but emits the function 5 times. I'm not sure how i can unbind the event when the modal closes using either typescript, vue, or bootstrap (any way other than jQuery :). Can anyone advise?
save() {
EventBus.$emit(MyEvents.RequestItemDetails);
}
// EventBus.ts
export const EventBus = new Vue();
export enum MyEvents{
RequestItemDetails = "request-item-details"
}
You've provided very little code for us to know what the problem actually is, but I'll take a guess.
If you're using a global event bus and you subscribe to an event on that bus from within a component, you need to make sure you unsubscribe from that event when the component is destroyed, otherwise your event handler function will be called multiple times because it gets registered multiple times on the bus.
For example:
import bus from './bus.js'
export default {
created() {
bus.$on('request-item-details', this.onRequestItemDetails)
},
destroyed() {
bus.$off('request-item-details', this.onRequestItemDetails)
},
methods: {
onRequestItemDetails() {
// Handle event
}
}
}
Your reply helped me find the solution. In my close method, all i needed to do was add "EventBus.$off('request-item-details')". That took care of it. Guilty of Overthinking again.
Thanks!

Calling method of another router-view's component / Vue.js

I have two <router-view/>s: main and sidebar. Each of them is supplied with a component (EditorMain.vue and EditorSidebar.vue).
EditorMain has a method exportData(). I want to call this method from EditorSidebar on button click.
What is a good way of tackling it?
I do use vuex, but i don't wanna keep this data reactive since the method requires too much computational power.
I could use global events bus, but it doesn't feel right to use it together with vuex (right?)
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
Is there any good practice for this? Or did i make some mistakes with the way app is set up? Did is overlooked something in documentation?
After two more years of my adventure with Vue I feel confident enough to answer my own question. It boils down to communication between router views. I've presented two possible solutions, I'll address them separately:
Events bus
Use global events bus (but it doesn't feel right to use it together with vuex)
Well, it may not feel right and it is surely not a first thing you have to think about, but it is perfectly fine use-case for event-bus. The advantage of this solution would be that the components are coupled only by the event name.
Router-view event listeners
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
This way of solving this problem is also fine, buy it couples components together. Coupling happens in the component containing <router-view/> where all the listeners are set.
Big number of listeners could be addressed by passing an object with event: handler mapping pairs to v-on directive; like so:
<router-view v-on="listeners"/>
...
data () {
return {
listeners: {
'event-one': () => console.log('Event one fired!'),
'event-two': () => console.log('The second event works as well!')
}
}
You could create a plugin for handling exports:
import Vue from 'vue'
ExportPlugin.install = function (Vue, options) {
const _data = new Map()
Object.defineProperty(Vue.prototype, '$exporter', {
value: {
setData: (svg) => {
_data.set('svg', svg)
},
exportData: () => {
const svg = _data.get('svg')
// do data export...
}
}
})
}
Vue.use(ExportPlugin)
Using like:
// EditorMain component
methods: {
setData (data) {
this.$exporter.setData(data)
}
}
// EditorSidebar
<button #click="$exporter.exportData">Export</button>

Custom element based on bootstrap-toggle not updating attribute binding

I have created a custom element based on bootstrap-toggle that looks as follows:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle();
}
}
toggle.html:
<template>
<require from="bootstrap-toggle/css/bootstrap2-toggle.min.css"></require>
<require from="bootstrap-toggle"></require>
<input ref="input" data-toggle="toggle" type="checkbox" checked.bind="checked">
</template>
The problem ist that the binding for the checked attribute is never updated when the switch is toggled via the UI. I am aware of the common pitfalls when using Aurelia with jQuery based components as described here. However, in my understanding this should not apply to bootstrap-toggle, as this component triggers a change event on the input element on toggle. I have verified that this change event bubbles up to my custom component.
The workaround I currently use is this:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle().on('change', (event) => this.checked = event.target.checked);
}
}
However, I do not understand why this should be necessary.
I have created a test project based on Aurelia's navigation-skeleton that can be downloaded here.
I would appreciate some help in understanding this!
The event listener used by jquery uses the capture phase and presumably does preventDefault or something among those lines that prevents the event from bubbling back up.
The change event dispatched by this line: if (!silent) this.$element.change() does not actually propagate to the bubble phase. Aurelia uses the bubble phase and thus never receives the event.
You can see the difference if you dispatch one manually in your browser console like so:
document.querySelector("[data-toggle]").dispatchEvent(new CustomEvent("change"));
This will result in the appropriate listeners being invoked and updates the bound property.
What this line does: .on('change', (event) => this.checked = event.target.checked); is also add a listener on the capture phase which then of course works. But then you might as well remove the checked.bind='checked' because that effectively does nothing.
There isn't necessarily any straight-forward fix for this, these CSS frameworks tend to just have very intrusive javascript. I would generally recommend against including their scripts and look for (or implement) them natively in Aurelia. It's very easy to do so and just works a lot better.

How can I capture click event on custom directive on Vue.js?

I am trying to learn Vue.js and came to an practice example where I need to implement a custom directive whice works lice 'v-on'.
This means that i need to capture the click event on my custom directive and call a method.
The template i was thinking of.
<template>
<h1 v-my-on:click="alertMe">Click</h1>
</template>
The problem is i don't know how to capture the click event in the custom directive. Excuse the clumsy code below.
<script>
export default {
methods: {
alertMe() {
alert('The Alert!');
}
},
directives: {
'my-on': {
bind(el, binding, vnode) {
console.log('bind');
el.addEventListener('click',()=>{
console.log('bind');
vnode.context.$emit('click');
});
},
}
}
}
</script>
Can anyone help me understand how this works? I didn't manage to find any example of something similar.
After some more searching i came to this solution:
<template>
<h1 v-my-on:click="alertMe">Click me!</h1>
</template>
<script>
export default {
methods: {
alertMe() {
alert('The Alert!');
}
},
directives: {
'my-on': {
// Add Event Listener on mounted.
bind(el, binding) {
el.addEventListener(binding.arg, binding.value);
},
// Remove Event Listener on destroy.
unbind(el, binding) {
el.removeEventListener(binding.arg, binding.value);
}
}
}
}
</script>
The solution you found is, as far as I can tell, the very best solution for what you are looking for. However, for those who don't know much about Vue.JS I thought I'd give a quick explanation. I'd also suggest you check out the official Vue documentation for Custom Directives or my Medium article on the concepts.
This is the code that Vlad came to and I would support:
<template>
<h1 v-my-on:click="alertMe">Click me!</h1>
</template>
<script>
export default {
methods: {
alertMe() {
alert('The Alert!');
}
},
directives: {
'my-on': {
bind(el, binding) {
let type = binding.arg;
let myFunction = binding.value;
el.addEventListener(type, myFunction);
}
}
}
}
</script>
In short, Vue Directives are called on the lifecyle of the element they are attached to, based on the directive object definition. In the example the function defined is called "bind" so the directive will call that function when the element is bound into the DOM.
This function receives the element it's attached to "el" and the different content of the directive usage in the template "binding". In the binding usage in the template, the value after the colon ":" is the "arg" which in this example is the string literal "click". The value inside of the quotes '""' is the "value" which in this case is the object reference to the function "alertMe".
The vars that are defined by getting binding.arg and binding.value (with their respective content) can then be used to create an event listener contained inside of the element "el" that the directive is used on (el is modifiable). So, when the element is created and bound, this new event listener is created on the "click" event defined by "arg" and it will call the "alertMe" function defined by "value".
Because the modification is contained inside the element, you don't have to worry about cleaning up on unbind, because the listener will be destroyed when the element is destroyed.
And that is a basic description of what is happening in the suggested code. To see more about directives and how to use them, follow the suggested links. Hopefully that helps!
You need to register a listener for the event being emitted within your directive.
// emit a custom event
// binding.expression is alertMe
vnode.context.$emit(binding.expression);
// listen for the event
export default {
created(){
this.$on('alertMe', event => {
this.alertMe()
})
},
....
}
This is not calling the method alertMe, rather passing alertMe through to the directive as the binding expression:
<h1 v-my-on:click="alertMe">Click</h1>
#Vlad has an excellent solution!
May I also add an important point: if you wanna pass parameters to your callback, it will confuse you by the way Vue handles your expression. In short, for custom directives, whatever in between quotation marks gets evaluated and the resulted value is passed in (hence, you can get it via binding.value (duh!), while for built-in directives, at least for v-on, the contents between quotation marks get evaluated later on, when event is fired.
Maybe this is best demonstrated with a comparison between custom directive and the built-in v-on directive. suppose you have a "my-on" directive written exactly as what #Vlad does, and you use it side by side with v-on:
built-in:
<h1 v-on:click="myAlert('haha')"> Click me!</h1>
It works as expected, when button is clicked, alert window pops up.
customized:
<h1 v-my-on:click="myAlert('haha')">Click me!</h1>
As soon as button is displayed, the alert window pops up, and when you click on it, the event is fired but nothing visible happens. This is because "myAlert('haha')" is evaluated as soon as binding(?), hence the alert window, and its value gets passed to your directive(undefined or whatever), cuz its value is not a function, nothing seems to happen.
now, the workaround is to have whatever in between the quotation marks returns a function upon evaluation, eg v-my-on:click="() => {myAlert('haha')}"
Hope it helps.
References:
https://stackoverflow.com/a/61734142/1356473
https://github.com/vuejs/vue/issues/5588
As #Vlad said it worked for me:
el.addEventListener('click',()=>{
console.log('bind');
vnode.context.$emit('click');
Here's my directive:
Vue.directive('showMenu', {
bind: function (el, binding, vnode) {
el.addEventListener('click', () => {
console.log('bind')
setTimeout(() => {
this.$emit('toggleDrawer')
}, 1000)
})
}
})
Thanks dude!
Seems like addEventListener works only for native events
To catch events fired with Vue inside the directive use $on
newdirective: {
bind(el, key, vnode){
vnode.componentInstance.$on('event-fired-from-component', someFunction)
},
....
}
You can put this code either inside your component or mixin under directives section like this
directives: {...}
And then connect it to the component you want to receive this event from
<some-component
v-newdirective
></some-component>