i'm trying to implement a text selection listener to display a toolbar for some custom options
<script>
export default {
name: "home",
created() {
document.onselectionchange = function() {
this.showMenu();
};
},
data() {
return {
...
};
},
methods: {
showMenu() {
console.log("show menu");
}
}
</script>
<style scoped>
</style>
but it still display that can't call showMenu of undefined, so i tried in this way:
created() {
vm = this;
document.onselectionchange = function() {
vm.showMenu();
};
},
so, nothing changed =(
i need to use this selectionchange because its the only listener that i can add that will handle desktop and mobile together, other method i should implement a touchup, touchdown and its not working for devices
Functions declared the classic way do have their own this. You can fix that by either explicitly binding this using Function.prototype.bind() or by using an ES6 arrow function (which does not have an own this, preserving the outer one).
The second problem is that if you have more than one of those components you've shown, each will re-assign (and thus, overwrite) the listener if you attach it using the assignment document.onselectionchange =. This would result in only the last select element working as you expect because it's the last one assigned.
To fix that, I suggest you use addEventListener() instead:
document.addEventListener('selectionchange', function() {
this.showMenu();
}.bind(this));
or
document.addEventListener('selectionchange', () => {
this.showMenu();
});
A third solution stores a reference to this and uses that in a closure:
const self = this;
document.addEventListener('selectionchange', function() {
self.showMenu();
});
Related
I have just come accross with an issue related to event listening in Vue directives.
I have a component which holds following code inside:
function setHeaderWrapperHeight() { ... }
function scrollEventHandler() { ... }
export default {
...
directives: {
fox: {
inserted(el, binding, vnode) {
setHeaderWrapperHeight(el);
el.classList.add('header__unfixed');
window.addEventListener(
'scroll',
scrollEventListener.bind(null, el, binding.arg)
);
window.addEventListener(
'resize',
setHeaderWrapperHeight.bind(null, el)
);
},
unbind(el, binding) {
console.log('Unbound');
window.removeEventListener('scroll', scrollEventListener);
window.removeEventListener('resize', setHeaderWrapperHeight);
}
}
}
...
}
And this component is re-rendered everytime I change router path, I achieved this behaviour by assigning current route path to :key prop so whenever path changes it gets re-rendered. But the propblem is though event listeners are not being removed/destroyed causing terrible performance issues. So how do I remove event listeners?
Calling bind on a function creates a new function. The listeners aren't being removed because the function you're passing to removeEventListener is not the same function you passed to addEventListener.
Communicating between hooks in directives is not particularly easy. The official documentation recommends using the element's dataset, though that seems clumsy in this case:
https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
You could just store the listeners on the element directly as properties so that they're available in the unbind hook.
The code below takes a slightly different approach. It uses an array to hold all of the elements that are currently bound to the directive. The listener on window is only ever registered once, no matter how many times the directive is used. If the directive isn't currently being used then that listener is removed:
let foxElements = []
function onClick () {
console.log('click triggered')
for (const entry of foxElements) {
clickHandler(entry.el, entry.arg)
}
}
function clickHandler (el, arg) {
console.log('clicked', el, arg)
}
new Vue({
el: '#app',
data () {
return {
items: [0]
}
},
directives: {
fox: {
inserted (el, binding) {
console.log('inserted')
if (foxElements.length === 0) {
console.log('adding window listener')
window.addEventListener('click', onClick)
}
foxElements.push({
el,
arg: binding.arg
})
},
unbind (el, binding) {
console.log('unbind')
foxElements = foxElements.filter(element => element.el !== el)
if (foxElements.length === 0) {
console.log('removing window listener')
window.removeEventListener('click', onClick)
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<button #click="items.push(Math.floor(Math.random() * 1000))">Add</button>
<hr>
<button
v-for="(item, index) in items"
v-fox:example
#click="items.splice(index, 1)"
>Remove {{ item }}</button>
</div>
However, all of this assumes that a directive is even the right way to go. If you can just do this at the component level then it may get a lot simpler because you have the component instance available to store things. Just remember that calling bind creates a new function, so you'll need to keep a reference to that function somewhere so you can pass it to removeEventListener.
Just for the records, and to help whoever passes through here as there is already an accepted answer, what one could do in this case (on Vue 3 at least, not tested on Vue 2) is to use binding.dir (which is a reference to the directive's own object) to host the function for adding the event listener on the directive object and take it back later when there's a need to remove this listener.
One simple example (not related to the original question) for binding one focus event:
export default {
...
directives: {
fox: {
handleFocus: () => { /* a placeholder to rewrite later */ },
mounted(el, binding) {
binding.dir.handleFocus = () => { /* do whatever */ }
el.addEventListener('focus', binding.dir.handleFocus);
},
beforeUnmount(el, binding) {
el.removeEventListener('focus', binding.dir.handleFocus);
}
}
}
...
}
A practical example of what I'm doing with this, in my case, is to have a focus/blur notifier for any input or textarea tag. I made a Gist here of this, it is on a project built on Vue 3 with TypeScript.
I'm trying to transform my code into a more plugin type of code, so everything will be separated, in case I change class names in the future.
For some reason, in my code, I get Cannot read property 'dropdown' of undefined.
My guess is, the function Navigation.bindEvents() runs before I set the config, so It can't find it... But I don't know how to solve it.
Here's my Navigation.js file:
let Navigation = {
config: {},
init(config) {
this.config = config;
this.bindEvents();
},
bindEvents() {
$(this.config.trigger).on('click', this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns);
},
toggleNavigation(event) {
// Store the current visible state
var visible = $(this).siblings(this.config.trigger).hasClass('visible');
// Hide all the drop downs
this.hideAllDropdowns();
// If the stored state is visible, hide it... Vice-versa.
$(this).siblings(this.config.content).toggleClass('visible', !visible);
event.preventDefault();
event.stopPropagation();
},
hideAllDropdowns() {
$(this.config.dropdown + ' ' + this.config.content).removeClass('visible');
}
}
export default Navigation;
And here's my app.js file which I run all the init functions.
window.$ = window.jQuery = require('jquery');
import Navigation from './layout/navigation.js';
Navigation.init({
dropdown: '.dropdown',
trigger: '.dropdown-trigger',
content: '.dropdown-content'
});
I guess you got problem with the scope $(document).on('click', this.hideAllDropdowns);
Let's try
bindEvents() {
$(this.config.trigger).on('click', this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns.bind(this));
},
UPDATE:
bindEvents() {
$(this.config.trigger).bind('click', {self:this}, this.toggleNavigation);
$(document).on('click', this.hideAllDropdowns.bind(this));
},
And replace all this.config by event.data.self inside toggleNavigation function
this in the context of toggleNavigation refers to the clicked element.
That is why you can do $(this).siblings(...) to get the sibling elements.
You need to have a reference to the Navigation object. Perhaps you can use the on syntax that allows you to pass extra data $(this.config.trigger).on('click', this, this.toggleNavigation);
Then rewrite the handler
toggleNavigation(event) {
//get the navigation reference
var nav = event.data;
// Store the current visible state
var visible = $(this).siblings(nav.config.trigger).hasClass('visible');
// Hide all the drop downs
nav.hideAllDropdowns();
// If the stored state is visible, hide it... Vice-versa.
$(this).siblings(nav.config.content).toggleClass('visible', !visible);
event.preventDefault();
event.stopPropagation();
},
The behavior of this is one of the hardest things to understand in JavaScript. Here this is obviously dynamic, which means that its value depends on where your method has been called...
let module = {
config() {
console.log(`config(): 'this' is 'module' ---> ${Object.is(this, module)}`);
console.log(`config(): 'this' is 'document' ---> ${Object.is(this, document)}`);
},
init() {
console.log(`init(): 'this' is 'module' ---> ${Object.is(this, module)}`);
console.log(`init(): 'this' is 'document' ---> ${Object.is(this, document)}`);
module.config();
}
};
$(document).ready(module.init);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
I have a situation using BS3 modal events and my app functionality is wrapped in object with exposed methods (reveal module pattern). I have event received from BS and I what my $this to point to the Event object instead of the App object.
I tried jquery this context with the jQuery proxy, which seems to be the best thing, but for some reason the things didn't worked for me
var globalAppDef = (function() {
function modalFilters() {
$('#filtersMore')
.on('show.bs.modal', (event) => {
const sourceElement = $(event.relatedTarget);
$(sourceElement.data().filters).removeClass('hidden');
})
/*
* Transfer the proper #this of the event outside the {globalAppDef} Object
*/
.on('hidden.bs.modal', $.proxy((event) => {
$(this).find(".form-list-items-1").addClass('hidden');
$(this).find(".form-list-items-1").addClass('hidden');
}, this));
}
return modalFilters: modalFilters
}
});
var globalApp = new globalAppDef();
globalApp.initialize();
$(document).ready(function () {globalApp.modalFilters()});
What I what to achieve is on the second hidden.bs.modal $this to point to my Modal, which is $('#filtersMore') element.
actually it it was the Arrow function the reason for that this stayed in the Object context.
That way worked:
.on('hidden.bs.modal', $.proxy(function (event) {
$(this).find(".form-list-items-1, .form-list-items-2").addClass('hidden');
}, $('#filtersMore')));
I'm trying to create a jQuery control using the widget factory. The idea is that I turn a button into a jQuery button, give it an icon, and register the click event for that button such that when invoked, it displays a calendar control on a textbox, whose id is passed in as an option to the widget method:
$.widget("calendarButton", {
options: {
textFieldId: ''
},
_create: function () {
this.element.button(
{
icons: {
primary: "ui-icon-calendar"
}
}).click(function () {
if (this.options.textFieldId != '') {
$(this.options.textFieldId).datetimepicker('show');
return false;
}
});
}
});
The problem with this however, is that this.options is undefined when the click handler is invoked; which makes sense since the method has a different scope. So I tried to see if there is a way to define a "static" variable which then can be accessed inside the callback method. I found this answer that explained how to create variables inside a wrapper function like this:
(function ($) {
var $options = this.options;
$.widget("calendarButton", {
options: {
textFieldId: ''
},
_create: function () {
this.element.button(
{
icons: {
primary: "ui-icon-calendar"
}
}).click(function () {
if ($options.textFieldId != '') {
$($options.textFieldId).datetimepicker('show');
return false;
}
});
}
});
})(jQuery);
But it still reports that $options is undefined. Is there a way to achieve this? I'm trying to avoid requiring the callback function be passed in since it'll be pretty much the same for all instances. Any help is appreciated.
After playing with it for a few hours, I finally came across the jQuery Proxy method which is exactly what I was looking for. I changed the code a little bit to look like this:
$.widget("calendarButton", {
options: {
textFieldId: ''
},
_create: function () {
this.element.button(
{
icons: {
primary: "ui-icon-calendar"
}
}).on("click", $.proxy(this._clickHandler, this));
},
_clickHandler: function () {
if (this.options.textFieldId != '') {
$(this.options.textFieldId).datetimepicker('show');
}
}
});
Notice that instead of implementing the click callback directly, I'm essentially creating a delegate that points to my private _clickHandler function, which itself runs on the same context as the $.widget() method (since the second argument of $.proxy(this._clickHandler, this) returns $.widget()'s context) hence availablity of the options variable inside the method.
I'm trying to create a simple gallery with prototype.js and script.aculo.us. To handle left and right arrow I made this code, but it doesn't work
Gallery.Arrow = Class.create(document.createElement('a'), {
initialize: function(listener) {
this.on('click', listener);
this.addClassName('xjsl-arrow');
}
});
this.on is undefined. I tryed Class.create($(document.createElement('a')), ..., or even Element.extend(this) in the initialize function, but nothing works.
If I tryed Event.Handler(this, 'click', listener) to, but the error come from element.attachEvent inside prototype.js library.
Is it possible to create a class based on HTML element ?
Try building the Class based on the Element.Methods namespace like this
Gallery.Arrow = Class.create(Element.Methods, {
initialize: function(element,listener) {
this.on(element,'click', listener);
this.addClassName(element,'xjsl-arrow');
}
});
jsfiddle example http://jsfiddle.net/rPLa8/