I'm working with Lit Element and I'm trying add an event listener on 'Click' that will a variable state that will set the dropdown to be expand or not. But once the drop down is 'closed' I want to remove that event to avoid unnecessary event calls on 'Click.
Adding the event works great but I cannot remove it.
Here is the idea:
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("_tenantsExpanded")) {
document.removeEventListener("click", (ev) => this._eventLogic(ev, this));
if (this._tenantsExpanded)
document.addEventListener("click", (ev) => this._eventLogic(ev, this));
}
}
The fct logic:
private _eventLogic(e: MouseEvent, component: this) {
const targets = e.composedPath() as Element[];
if (!targets.some((target) => target.className?.includes("tenant"))) {
component._tenantsExpanded = false;
}
}
Code in my render's function:
${this._tenantsExpanded
? html` <div class="tenants-content">${this._tenantsContent()}</div> `
: html``}
Important note: I want the click event to be listened on all the window, not just the component itself. The same for removing the event.
PS: I don't know why e.currentTaget.className doesn't give me the actual className, but results to an undefined.
When you use removeEventListener you have to pass a reference to the same function you used when adding the listener.
In this example the function is stored in fn.
(You might have to change the this reference here, it depends a bit on your whole component).
const fn = (ev) => this._eventLogic(ev, this);
document.addEventListener("click", fn);
document.removeEventListener("click", fn);
Related
As you may already know, mouseenter and mouseleave events are NOT triggered if the mouse doesn't move, which means that if you scroll over an element without moving the mouse, hover effects are ignored.
This answer describes the strategy to overcome this:
1: Add a scroll listener to the window.
2: In the handler, call document.elementsFromPoint.
3: Manually call the actual mouseover handler for those elements.
4: Manually call the actual mouseleave handler for elements no longer being hovered.
We also must take into account that registering a listener for each component we want to detect the hover of, is a waste of resource.
My idea is to create a singleton object and subscribe to it from each component.
I'm fairly new to react so I will try to write pseudo-react-code to describe my idea:
// A global singleton object that registers the listeners ONCE.
class Singleton {
subscribedElements = {}
subscribe(element, isHovered, setIsHovered) {
subscribedElements[element] = {
isHovered: isHovered,
setIsHovered: setIsHovered
}
}
unsubscribe(element) { ..... }
constructor() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('mousemove', this.handleMove);
}
handleMove() {
//omitted
}
handleScroll() {
hoveredElements = document.elementsFromPoint(mousePosition)
// Iterate every element that is subscribed and check if they are in the list of elements that are hovered.
subscribedElements.map( e => {
if (hoveredElements.contains(e)) {
subscribedElements[e].setIsHovered(true)
} else {
subscribedElements[e].setIsHovered(false)
}
})
}
}
// a hook to be used in all elements that want to detect if they are hovered
function useHovered(element) {
[isHovered, setIsHovered] = useState(false);
Singleton.subscribe(element, isHovered, setIsHovered);
return isHovered;
}
// All components that want to check if they are being hovered would do this
function MyComponent(props) {
isHovered = useHovered(this);
return <div>{ isHovered ? 'hovered' : 'not hovered'</div>
}
Now, what is the most efficient and clean way to do something like this?
I have read about useContext but I'm not sure how to apply it to this solution.
I'm not sure how to create this singleton. Does it have to be a component? Can it be done in the new functional way?
The Twitter Bootstrap modal dialog has a set of events that can be used with callbacks.
Here is an example in jQuery:
$(modalID).on('hidden.bs.modal', function (e) {
console.log("hidden.bs.modal");
});
However, when I transcribe this method to JS the event 'hidden.bs.modal' does not work. Using 'click' does work:
document.querySelector(modalID).addEventListener('hidden.bs.modal', function(e) {
console.log("hidden.bs.modal");
}, false);
Are these Bootstrap events only useable with jQuery? Why?
Thanks,
Doug
The reasoning behind this is because Twitter Bootstrap uses that.$element.trigger('hidden.bs.modal')(line 997) to trigger it's events. In other words it uses .trigger.
Now jQuery keeps track of each element's event handlers (all .on or .bind or .click etc) using ._data. This is because there isn't any other way to get the event handlers that are bound (using .addEventListener) to an element. So the trigger method actually just get's the set event listener(s)/handler(s) from ._data(element, 'events') & ._data(element, 'handle') as an array and runs each of them.
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
}
(line 4548)
This means that no matter what context is, if an event is bound via .addEventListener it will not run using .trigger. Here's an example. On load only jquery will be logged (triggered by .trigger). If you click the a element though, both will run.
$('a')[0].addEventListener('click', function(){console.log('addlistener');}, false);
$('a').on('click', function(){
console.log('jquery');
});
$('a').trigger('click');
DEMO
Alternatively, you can trigger an event on an element in javascript using fireEvent(ie) & dispatchEvent(non-ie). I don't necessarily understand or know the reasoning of jQuery's .trigger not doing this, but they may or may not have one. After a little more research I've found that they don't do this because some older browsers only supported 1 event handler per event.
In general we haven't tried to implement functionality that only works on some browsers (and some events) but not all, since someone will immediately file a bug that it doesn't work right.
Although I do not recommend it, you can get around this with a minimal amount of changes to bootstraps code. You would just have to make sure that the function below is attached first (or you will have listeners firing twice).
$(modalID).on('hidden.bs.modal', function (e, triggered) {
var event; // The custom event that will be created
if(!triggered){
return false;
}
e.preventDefault();
e.stopImmediatePropagation();
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent("hidden.bs.modal", true, true);
} else {
event = document.createEventObject();
event.eventType = "hidden.bs.modal";
}
event.eventName = "hidden.bs.modal";
if (document.createEvent) {
this.dispatchEvent(event);
} else {
this.fireEvent("on" + event.eventType, event);
}
});
Finally change the Twitter Bootstrap line from above to:
that.$element.trigger('hidden.bs.modal', true)
This is so you know its being triggered and not the event that you're firing yourself after. Please keep in mind I have not tried this code with the modal. Although it does work fine on the click demo below, it may or may not work as expected with the modal.
DEMO
Native Javascript Solution. Here is my way of doing it without JQuery.
//my code ----------------------------------------
export const ModalHiddenEventListener = (el, fn, owner) => {
const opts = {
attributeFilter: ['style']
},
mo = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === 'attributes'
&& mutation.attributeName ==='style'
&& mutation.target.getAttribute('style') === 'display: none;') {
mo.disconnect();
fn({
owner: owner,
element: mutation.target
});
}
}
});
mo.observe(el, opts);
};
//your code with Bootstrap modal id='modal'-------
const el = document.getElementById('modal'),
onHide = e => {
console.log(`hidden.bs.modal`);
};
ModalHiddenEventListener(el, onHide, this);
Compatibility:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#Browser_compatibility
I am having issues dynamically adding and removing eventListeners. I want to be able to add an event listener to all child nodes of an element. Then at a later time remove them, and add them back.
Note: I am aware of EventListener options.once however this doesn't exactly solve my case.
Here is some sample code of what I am trying to do:
var cont;
window.onload = function() {
cont = document.querySelector(".container");
[...cont.children].forEach(c => {
c.addEventListener("click", clicked.bind(c))
});
}
function clicked() {
console.log(`removing event listener from ${this}`);
this.removeEventListener("click", clicked); //Not actually removing - why?
}
Thanks everyone!
The problem is that .bind creates a new function. The function passed to addEventListener can only be removed if that exact same function is passed to removeEventListener. A bound function is not the same as the original unbound function, so removeEventListener won't work if you pass it the unbound function.
In your situation, one possibility would be to use a Map indexed by the HTML elements, whose value is the bound listener for that element, so that they can then be removed later:
const listenerMap = new Map();
const cont = document.querySelector(".container");
[...cont.children].forEach(c => {
const listener = clicked.bind(c);
listenerMap.set(c, listener);
c.addEventListener("click", listener);
});
function clicked() {
console.log(`removing event listener from ${this}`);
this.removeEventListener("click", listenerMap.get(this));
}
<div class="container">
<div>one</div>
<div>two</div>
<div>three</div>
</div>
I have a module like this:
let Search = {
settings: {
inputField: document.getElementById('search_field')
},
init: function() {
this.bindAction();
},
bindAction: function() {
this.settings.inputField.addEventListener("onkeyup", function(e) {
let value = this.settings.inputField.value;
console.log(value);
e.preventDefault();
})
}
};
export default Search;
And I import it into my main app like so:
import Search from './components/Search';
Search.init();
But the onkeyup event doesn't fire.
What am I doing wrong?
There is no such event name as onkeyup, so the listener doesn't fire. The event's name is keyup:
this.settings.inputField.addEventListener("keyup", function(e) {
You can use on when you're assigning a listener by assigning to a listener property using dot notation, for example:
this.settings.inputField.onkeyup = function(e) {
When using addEventListener, never prefix the event name with on - when assigning to a property, always prefix the event name with on.
The other problem is that your calling context is wrong for the listener - inside the listener, this will refer to the element, not to the Search object. Use an arrow function instead, so that the this of the parent block will be inherited:
this.settings.inputField.addEventListener("keyup", (e) => {
Currently dojo uses on method to connect event to handler.
btn = new Button();
btn.on('click', function () {console.log('do something');});
this will call the attached function when the button gets clicked.
however, according to the documents, removing existing handlers should be done in the following way
handler = btn.on('click', function () {console.log('do something');});
handler.remove();
this is not the way I want to remove event handler.
I do not store the handler reference anywhere. But I want to add a new 'click' event by doing
btn.on('click', function () {console.log('do something different');});
so that it replaces the existing 'click' event handler and add a new one.
Is there any way to achieve what I want?
Thanks!
That's not possible, the framework tells you to do it in the way by creating a reference to the event handler. This is similar to how other frameworks like jQuery work.
jQuery has of course a mechanism to remove all event handlers by using the off() function, but that's not available in Dojo either. Like Chris Hayes suggested in the comments, you can implement such a feature by yourself, either by wrapping it inside another module, or by using aspects on the dojo/on module.
For example, you can wrap it inside a new module:
// Saving the event handlers
var on2 = function(dom, event, callback) {
on2.handlers = [];
if (on2.handlers[event] === undefined) {
on2.handlers[event] = [];
}
var handler = on(dom, event, callback);
on2.handlers[event].push({
node: dom,
handler: handler
});
return handler;
};
// Off functionality
lang.mixin(on2, on, {
off: function(dom, event) {
if (this.handlers[event] !== undefined) {
array.forEach(this.handlers[event], function(handler) {
if (handler.node === dom) {
handler.handler.remove();
}
});
}
}
});
And then you can use it:
on2(dom.byId("test"), "click", function() {
console.log("test 1 2 3"); // Old event handler
});
on2.off(dom.byId("test"), "click"); // Remove old event handlers
on2(dom.byId("test"), "click", function() {
console.log("test 4 5 6"); // New event handler
});
This should work fine, as you can see in this fiddle: http://jsfiddle.net/X7H3F/
btn = new Button();
btn.attr('id','myButton');
query("#myButton").on('click', function () {console.log('do something');});
Do the same thing when you want to replace your handler. Like,
query("#myButton").on('click', function () {console.log('do something different');});
Hope that helps :)