Update: It might be the jQuery's trigger() do some extra works in testings, I opened a issue on github.
=====
I'm following learnQuery to build my simple jQuery. Now working on DOM event , implement on()
and off()
function. They provided some testings, I can't pass some of them.
Here is my code: (And you can clone this branch ,run 06.event_listeners/runner.html
to run the testing)
"use strict";
function isEmpty(str) {
return (!str || 0 === str.length);
}
// listener use to bind to DOM element, call corresponding functions when event firing.
function geneEventListener(event) {
console.log('gene');
let type = Object.keys(this.handlers).find(type=>type===event.type);
if (!type) return;
let functions = this.handlers[type];
functions.forEach(f=>f.apply(this,event));
}
// cache elements which bound event listener
let Cache = function () {
this.elements = [];
this.uid = 1;
};
Cache.prototype = {
constructor:Cache,
init:function (element) {
if(!element.uid) element.uid = this.uid++;
if(!element.handlers) element.handlers = {};
if(!element.lqListener) element.lqListener = geneEventListener.bind(element);
this.elements.push(element);
},
removeElement:function (uid) {
this.elements.splice(this.elements.findIndex(e=>e.uid===uid),1);
},
removeType:function (uid,type) {
if(this.get(uid)) delete this.get(uid).handlers[type];
},
removeCallback:function (uid, type, callback) {
if(this.get(uid) && this.get(uid).handlers[type]) {
let functions = this.get(uid).handlers[type];
functions.splice(functions.findIndex(callback),1)
}
},
// return element or undefined
get:function (uid) {
return this.elements.find(e=>e.uid===uid);
},
};
/*
* One type could have many event listeners, One element could have many event types of listeners
* So use element.handlers = {'click':[listener1, listener2, ...], 'hover':[...], ...}
* */
let eventListener = (function() {
let cache = new Cache();
function add (element, type, callback){
cache.init(element);
element.addEventListener(type,element.lqListener);
if(!element.handlers[type]){
element.handlers[type] = [];
}
(element.handlers[type]).push(callback);
}
// remove a type of event listeners, should remove the callback array and remove DOM's event listener
function removeType (element, type) {
element.removeEventListener(type,element.lqListener);
cache.removeType(element.uid,type);
}
// remove a event listener, just remove it from the callback array
function removeCallback(element, type, callback) {
cache.removeCallback(element.uid,type,callback);
}
// bind a callback.
function on(element,type,callback) {
if(!(element||type||callback)) throw new Error('Invalid arguments');
add(element,type,callback);
}
function off(element,type,callback) {
if(!(element instanceof HTMLElement)) throw new Error('Invaild element, need a instance of HMTLElement');
let handlers = cache.get(element.uid).handlers;
if(isEmpty(type)&&!callback){
for(let type in handlers){
removeType(element,type);
}
}
console.log('off')
if(!isEmpty(type)&&!callback) removeType(element,type);
if(!isEmpty(type) && (typeof callback === 'function')) removeCallback(element,type,callback);
}
return {
on,
off
}
})();
I use chrome debugger to follow element.handlers
's value, it seems fine, working great when add and remove callback.
And the testing have some console.log()
in event's callback function, oddly enough, these console.log()
do not log in console, and I try to set a breakpoint in callback, it also do not work.
I have little javascript experience, If anyone can tell me how to debug and where is the bug, thank you very much! And why console.log()
cannot work in callback. It should work, since they wrote it in testing, I think.
Here is the testing code:
/*global affix*/
/*global eventListener*/
describe('EventListeners', function() {
'use strict';
var $selectedElement, selectedElement, methods;
beforeEach(function() {
affix('.learn-query-testing #toddler .hidden.toy+h1[class="title"]+span[class="subtitle"]+span[class="subtitle"]+input[name="toyName"][value="cuddle bunny"]+input[class="creature"][value="unicorn"]+.hidden+.infinum[value="awesome cool"]');
methods = {
showLove: function(e) {
console.log('<3 JavaScript <3');
},
giveLove: function(e) {
console.log('==> JavaScript ==>');
return '==> JavaScript ==>';
}
};
spyOn(methods, 'showLove');
spyOn(methods, 'giveLove');
$selectedElement = $('#toddler');
selectedElement = $selectedElement[0];
});
it('should be able to add a click event to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove).toHaveBeenCalled();
});
it('should be able to add the same event+callback two times to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should be able to add the same callback for two different events to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
console.log('3')
$selectedElement.trigger('click');
$selectedElement.trigger('hover');
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should be able to add two different callbacks for same event to an HTML element', function() {
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
$selectedElement.trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
expect(methods.giveLove.calls.count()).toEqual(1);
});
it('should be able to remove one event handler of an HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.off(selectedElement, 'click', methods.showLove);
console.log('5')
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(0);
expect(methods.giveLove.calls.count()).toEqual(1);
});
it('should be able to remove all click events of a HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
eventListener.off(selectedElement, 'click');
console.log('6')
$selectedElement.trigger('hover');
$selectedElement.trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
expect(methods.giveLove).not.toHaveBeenCalled();
});
it('should be able to remove all events of a HTML element', function() {
$selectedElement.off();
eventListener.on(selectedElement, 'click', methods.showLove);
eventListener.on(selectedElement, 'click', methods.giveLove);
eventListener.on(selectedElement, 'hover', methods.showLove);
eventListener.off(selectedElement);
var eventHover = new Event('hover');
var eventClick = new Event('click');
selectedElement.dispatchEvent(eventClick);
selectedElement.dispatchEvent(eventHover);
expect(methods.showLove).not.toHaveBeenCalled();
expect(methods.giveLove).not.toHaveBeenCalled();
});
it('should trigger a click event on a HTML element', function() {
$selectedElement.off();
$selectedElement.on('click', methods.showLove);
eventListener.trigger(selectedElement, 'click');
expect(methods.showLove.calls.count()).toBe(1);
});
it('should delegate an event to elements with a given css class name', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
$('.title').trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should not delegate an event to elements without a given css class name', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
$('.subtitle').trigger('click');
$('.title').trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should delegate an event to elements that are added to the DOM to after delegate call', function() {
eventListener.delegate(selectedElement, 'new-element-class', 'click', methods.showLove);
var newElement = document.createElement('div');
newElement.className = 'new-element-class';
$selectedElement.append(newElement);
$(newElement).trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should trigger delegated event handler when clicked on an element inside a targeted element', function() {
eventListener.delegate(selectedElement, 'title', 'click', methods.showLove);
var newElement = document.createElement('div');
newElement.className = 'new-element-class';
$selectedElement.append(newElement);
$('.title').append(newElement);
$(newElement).trigger('click');
expect(methods.showLove.calls.count()).toEqual(1);
});
it('should not trigger delegated event handler if clicked on container of delegator', function() {
var $targetElement = $('<p class="target"></p>');
$selectedElement.append($targetElement);
eventListener.delegate(selectedElement, 'target', 'click', methods.showLove);
$selectedElement.click();
expect(methods.showLove.calls.count()).toEqual(0);
});
it('should trigger delegated event handler multiple times if event happens on multiple elements', function() {
eventListener.delegate(selectedElement, 'subtitle', 'click', methods.showLove);
$('.subtitle').trigger('click');
expect(methods.showLove.calls.count()).toEqual(2);
});
it('should not trigger method registered on element A when event id triggered on element B', function() {
var elementA = document.createElement('div');
var elementB = document.createElement('div');
$selectedElement.append(elementA);
$selectedElement.append(elementB);
eventListener.on(elementA, 'click', methods.showLove);
eventListener.on(elementB, 'click', methods.giveLove);
$(elementA).trigger('click');
expect(methods.showLove).toHaveBeenCalled();
expect(methods.giveLove).not.toHaveBeenCalled();
});
});
The problem lies in that there is no event called hover
.
Just a combination of mouseenter
and mouseleave
.
You can see all event types listed here.
When calling element.addEventListener(type, element.lqListener)
with type value hover
, it just does not work.
You can see more info from this question Is it possible to use jQuery .on and hover?.
You are very close to meeting your own requirement. The only issue that could locate at code, here, is that Function.prototype.apply()
expects an Array
at second parameter
Syntax
func.apply(thisArg, [argsArray])
Substitute
// pass `event` as element to array literal as second parameter to `.apply()`
functions.forEach(f => f.apply(this, [event]));
for
functions.forEach(f => f.apply(this, event));
Also, substituted function name _Cache
for Cache
, as Cache
is a globally defined function
The Cache interface provides a storage mechanism for
Request
/Response
object pairs that are cached, for example as part of theServiceWorker
life cycle.
"use strict";
function isEmpty(str) {
return (!str || 0 === str.length);
}
// listener use to bind to DOM element, call corresponding functions when event firing.
function geneEventListener(event) {
console.log('gene');
let type = Object.keys(this.handlers).find(type => type === event.type);
if (!type) return;
let functions = this.handlers[type];
functions.forEach(f => f.apply(this, [event]));
}
// cache elements which bound event listener
let _Cache = function() {
this.elements = [];
this.uid = 1;
};
_Cache.prototype = {
constructor: _Cache,
init: function(element) {
if (!element.uid) element.uid = this.uid++;
if (!element.handlers) element.handlers = {};
if (!element.lqListener) element.lqListener = geneEventListener.bind(element);
this.elements.push(element);
},
removeElement: function(uid) {
this.elements.splice(this.elements.findIndex(e => e.uid === uid), 1);
},
removeType: function(uid, type) {
if (this.get(uid)) delete this.get(uid).handlers[type];
},
removeCallback: function(uid, type, callback) {
if (this.get(uid) && this.get(uid).handlers[type]) {
let functions = this.get(uid).handlers[type];
functions.splice(functions.findIndex(callback), 1)
}
},
// return element or undefined
get: function(uid) {
return this.elements.find(e => e.uid === uid);
},
};
/*
* One type could have many event listeners, One element could have many event types of listeners
* So use element.handlers = {'click':[listener1, listener2, ...], 'hover':[...], ...}
* */
let eventListener = (function() {
let cache = new _Cache();
function add(element, type, callback) {
cache.init(element);
element.addEventListener(type, element.lqListener);
if (!element.handlers[type]) {
element.handlers[type] = [];
}
(element.handlers[type]).push(callback);
}
// remove a type of event listeners, should remove the callback array and remove DOM's event listener
function removeType(element, type) {
element.removeEventListener(type, element.lqListener);
cache.removeType(element.uid, type);
}
// remove a event listener, just remove it from the callback array
function removeCallback(element, type, callback) {
cache.removeCallback(element.uid, type, callback);
}
// bind a callback.
function on(element, type, callback) {
if (!(element || type || callback)) throw new Error('Invalid arguments');
add(element, type, callback);
}
function off(element, type, callback) {
if (!(element instanceof HTMLElement)) throw new Error('Invaild element, need a instance of HMTLElement');
let handlers = cache.get(element.uid).handlers;
if (isEmpty(type) && !callback) {
for (let type in handlers) {
removeType(element, type);
}
}
console.log('off')
if (!isEmpty(type) && !callback) removeType(element, type);
if (!isEmpty(type) && (typeof callback === 'function')) removeCallback(element, type, callback);
}
return {
on,
off
}
})();
onload = () => {
eventListener.on(document.querySelector("div"), "click", function(event) {
console.log(event.type);
eventListener.off(event.target, "click");
});
}
<div>click</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With