Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Selfmade jQuery cannot handle event properly

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();
  });
});
like image 597
Ezio Shiki Avatar asked Aug 13 '17 12:08

Ezio Shiki


2 Answers

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?.

like image 80
aristotll Avatar answered Nov 15 '22 08:11

aristotll


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 the ServiceWorker 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>
like image 27
guest271314 Avatar answered Nov 15 '22 10:11

guest271314