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