how to define eventEmitter properly in javascript - javascript

I need to define a helper object in which I need a function that will execute on each "orientationchange" event of window.
My code as below and it is not in correct form. Can you please help me how can I define onRotate properly so that I can use it globally.
<script type="text/javascript">
'use strict';
var GlobalHelper = (function () {
var me = {
onRotate: onRotate // this is where I am struggling
}
function onRotate() {
window.addEventListener("orientationchange", function (event) {
console.log(event.target.screen.orientation.angle);
});
}
return me;
})();
GlobalHelper.onRotate = function (e) {
console.log(e);
}
</script>

I found an answer to my own question. It was actually pretty easy.
<script type="text/javascript">
'use strict';
var GlobalHelper = (function () {
var me = {
onRotate: function (e) { }
}
function _init() {
window.addEventListener("orientationchange", function (event) {
me.onRotate(event);
});
}
_init();
return me;
})();
GlobalHelper.onRotate = function (e) {
console.log(e);
}
</script>

Another option is to add custom programmable method for creating listeners and bonded functions. You can test it by Running code snippet and pressing keyboard keys while focused on results window.
// Set app object
const bindFn = e => {
// Set object to hold your functions
let fns = {};
// Set event listener to run all functions in fns object
window.addEventListener(e, event => {
for(const f in fns) fns[f](event);
});
// Return method to set new functions
// You can extend it with another function for
// deleting or altering, as you wish...
return {
add: (name, fn) => {
if(!fns[name]) fns[name] = fn;
else console.log(`Function with name ${name} already exist, skipping...`);
}
};
};
// Set binder with proper event
// here we do orientation change
const OrCh = bindFn("orientationchange");
// Add some function to execute on this event
OrCh.add('log', event => console.log(event.target.screen.orientation.angle));
//
// Test with another event that easy to trigger on stackoverflow
const KeyP = bindFn("keypress");
// Add logger
KeyP.add('log', event => console.log(`key pressed: ${event.code}`));
// Add something else
KeyP.add('logAlt', event => console.log(`Alternative function: ${event.code}`));

Related

How can I remove an event listener no matter how the callback is defined

For years I ran into problems trying to remove an event listener in JavaScript. Often I would have to create an independent function as the handler. But that is just sloppy and, especially with the addition of arrow functions, just a pain.
I am not after a ONCE solution. This needs to work in all situations no matter HOW the callback is defined. And this needs to be raw JS so anyone can use it.
The following code works fine since the function clickHandler is a unique function and can be used by both addEventListener and removeEventListener:
This example has been updated to show what I have run into in the past
const btnTest = document.getElementById('test');
let rel = null;
function clickHandler() {
console.info('Clicked on test');
}
function add() {
if (rel === null) {
rel = btnTest.addEventListener('click', clickHandler);
}
}
function remove() {
btnTest.removeEventListener('click', clickHandler);
}
[...document.querySelectorAll('[cmd]')].forEach(
el => {
const cmd = el.getAttribute('cmd');
if (typeof window[cmd] === 'function') {
el.addEventListener('click', window[cmd]);
}
}
);
<button cmd="add">Add</button>
<button cmd="remove">Remove</button>
<button id="test">Test</button>
You used to be able to do it with arguments.callee:
var el = document.querySelector('#myButton');
el.addEventListener('click', function () {
console.log('clicked');
el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>
But using an arrow function does not work:
var el = document.querySelector('#myButton');
el.addEventListener('click', () => {
console.log('clicked');
el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>
Is there a better way??
UPDATE
As stated by #Jonas Wilms this way will work:
var el = document.querySelector('#myButton');
el.addEventListener('click', function handler() {
console.log('clicked');
el.removeEventListener('click', handler); //<-- will work
});
<button id="myButton">Click</button>
Unless you need to using binding:
var obj = {
setup() {
var el = document.querySelector('#myButton');
el.addEventListener('click', (function handler() {
console.log('clicked', Object.keys(this));
el.removeEventListener('click', handler); //<-- will work
}).bind(this));
}
}
obj.setup();
<button id="myButton">Click</button>
The problem is that there are too many ways to provide an event handler to the addEventListener function and your code might break if the way you pass in the function changes in a refactor.
You can NOT use an arrow function or any anonymous function directly and expect to be able to remove the listener.
To remove a listener requires you pass the EXACT SAME ARGUMENTS to removeEventListener as you passed to addEventListener but when you use an anonymous function or an arrow function you do not have access to that function so it's impossible for you to pass it into removeEventListener
works
const anonFunc = () => { console.log("hello"); }
someElem.addEventListener('click', anonFunc);
someElem.removeEventListener('click', anonFunc); // same arguments
does not work
someElem.addEventListener('click', () => { console.log("hello"); });
someElem.removeEventListener('click', ???) // you don't have a reference
// to the anon function so you
// can't pass the correct arguments
// to remove the listener
your choices are
don't use anonymous or arrow functions
use a wrappers that will track the arguments for you
One example is #Intervalia closure. He tracks the function and other arguments you passed in and returns a function you can use the remove the listener.
One solution I often use which often fits my needs is a class that tracks all the listeners and remove them all. Instead of a closure it returns an id but it also allows just removing all listeners which I find useful when I build up something now and want to tear it down something later
function ListenerManager() {
let listeners = {};
let nextId = 1;
// Returns an id for the listener. This is easier IMO than
// the normal remove listener which requires the same arguments as addListener
this.on = (elem, ...args) => {
(elem.addEventListener || elem.on || elem.addListener).call(elem, ...args);
const id = nextId++;
listeners[id] = {
elem: elem,
args: args,
};
if (args.length < 2) {
throw new Error('too few args');
}
return id;
};
this.remove = (id) => {
const listener = listeners[id];
if (listener) {
delete listener[id];
const elem = listener.elem;
(elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
}
};
this.removeAll = () => {
const old = listeners;
listeners = {};
Object.keys(old).forEach((id) => {
const listener = old[id];
if (listener.args < 2) {
throw new Error('too few args');
}
const elem = listener.elem;
(elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
});
};
}
Usage would be something like
const lm = new ListenerManager();
lm.on(saveElem, 'click', handleSave);
lm.on(newElem, 'click', handleNew);
lm.on(plusElem, 'ciick', handlePlusOne);
const id = lm.on(rangeElem, 'input', handleRangeChange);
lm.remove(id); // remove the input event on rangeElem
lm.removeAll(); // remove events on all elements managed by this ListenerManager
note the code above is ES6 and would have to be changed to support really old browsers but the ideas are the same.
Just use a named function expression:
var el = document.querySelector('#myButton');
el.addEventListener('click', function handler() {
console.log('clicked');
el.removeEventListener('click', handler); //<-- will work
});
For sure that can be wrapped in a function:
function once(selector, evt, callback) {
var el = document.querySelector(selector);
el.addEventListener(evt, function handler() {
callback();
el.removeEventListener(evt, handler); //<-- will work
});
}
once("#myButton", "clicl", () => {
// do stuff
});
There is an easy solution using closures.
By moving the code to both addEventListener and removeEventListener into a single function you can accomplish the task easily:
function ael(el, evt, cb, options) {
console.log('Adding', evt, 'event listener for', el.outerHTML);
el.addEventListener(evt, cb, options);
return function() {
console.log('Removing', evt, 'event listener for', el.outerHTML);
el.removeEventListener(evt, cb, options);
}
}
const btnTest = document.getElementById('test');
let rel = null;
function add() {
if (rel === null) {
rel = ael(btnTest, 'click', () => {
console.info('Clicked on test');
});
}
}
function remove() {
if (typeof rel === 'function') {
rel();
rel = null;
}
}
function removeAll() {
rels.forEach(rel => rel());
}
const rels = [...document.querySelectorAll('[cmd]')].reduce(
(rels, el) => {
const cmd = el.getAttribute('cmd');
if (typeof window[cmd] === 'function') {
rels.push(ael(el, 'click', window[cmd]));
}
return rels;
}, []
);
<button cmd="add">Add</button>
<button cmd="remove">Remove</button>
<button id="test">Test</button>
<hr/>
<button cmd="removeAll">Remove All</button>
The function ael above allows the element, the event type and the callback to all be saved in the closure scope of the function. When you call ael it calls addEventListener and then returns a function that will call removeEventListener. Later in your code you call that returned function and it will successfully remove the event listener without worrying about how the callback function was created.
Here is an es6 version:
const ael6 = (el, evt, cb, options) => (el.addEventListener(evt, cb, options), () => el.removeEventListener(evt, cb, options));
You can use the once option of EventTarget.addEventListener():
Note: supported by all browsers but IE.
var el = document.querySelector('#myButton');
el.addEventListener('click', () => {
console.log('clicked');
}, { once: true });
<button id="myButton">Click</button>

Use of debounce on Ext 3.4 framework

I want to implement the debounce function on Ext.Button, so I extended it and override the onClick function, like this:
MyButton = Ext.extend(Ext.Button, {
onClick: function(e) {
var that = this;
var args = e;
clearTimeout(this.timeoutDebounce);
this.timeoutDebounce = setTimeout(function(){
MyButton.superclass.onClick.apply(that, [args])
}, this.debounce);
}
});
Debounce is a parameter passed on the x-type declaration.
The problem here is that the "args" parameter I'm passing to onClick has changed when it's called from "click" to "mouvemove" and it doesn't fire the events it should.
Is there a way to record the "e" parameter received in the function to pass to onClick on superclass?
The function passed to setTimeout must be wrapped in order to keep the value presented in current scope:
function createCallback(args) {
return function() {
MyButton.superclass.onClick.apply(that, [args]);
}
}
Also, e is passed by reference, so you need to create a copy of it. Using ExtJS, you can use Ext.apply method:
Ext.apply({}, e);
The full code should be:
var MyButton = Ext.extend(Ext.Button, {
onClick: function(e) {
var that = this;
function createCallback(args) {
return function() {
MyButton.superclass.onClick.apply(that, [args]);
// you can also use call since you know the arguments:
// MyButton.superclass.onClick.call(that, args);
}
}
clearTimeout(this.timeoutDebounce);
var copy = Ext.apply({}, e);
this.timeoutDebounce = setTimeout(createCallback(copy), this.debounce);
}
});
You should clone the object:
var args = Ext.apply({}, e);
this.timeoutDebounce = setTimeout((function(args){
return function(){MyButton.superclass.onClick.apply(that, [args])};
})(args), this.debounce);

Change reference on existing event

I have the following JavaScript code:
var MyGlobalRef = (function () {
function init(obj1, obj2) {
prepareEvents(obj1);
prepareEvents(obj2);
function prepareEvents(obj) {
var handleMouseUp = function (evt) {
// do work with obj
};
obj.addEventListener('mouseup', handleMouseUp);
}
}
return {
init: init
}
})();
In my main page, I'm frequently calling the init function like this:
function moveNext(){
MyGlobalRef.init(getNewObj1(), getNewObj2());
}
My problem is that on that moveNext() function, I tend to replace the existing objects, meaning that there are only 2 objects in the page at all times. However, the more I call moveNext the more event listeners get generated. So by the time I moveNext 3 times, the mouse up event fires 3 times per one mouse up. The obvious solution is to call addEventListener only the first time. However, the problem with this is that obj (that is used in the mouse up event) does not update and still references the original obj. Also, I failed to use removeEventListener because I don't have a reference to the obj once it's initiated.
To expand on my comment. Here is what I envision. You return a destroy function as a result of init execution
Solution #1
var MyGlobalRef = (function () {
function init(obj1, obj2) {
prepareEvents(obj1);
prepareEvents(obj2);
function prepareEvents(obj) {
var handleMouseUp = function (evt) {
// do work with obj
};
obj.addEventListener('mouseup', handleMouseUp);
}
return function() {
obj1.removeEventListener('mouseup');
obj2.removeEventListener('mouseup');
};
}
return {
init: init
}
})();
// cleanup is a variable stored in your code to run a cleanup on obj1 and obj2 later on
function moveNext(){
if (cleanup) {
cleanup();
}
cleanup = MyGlobalRef.init(GetNewObj1A(), GetNewObj1A());
}
Solution #2
Keep reference to obj1 and obj2 in closure.
var MyGlobalRef = (function () {
var _obj1;
var _obj2;
function init(obj1, obj2) {
prepareEvents(obj1);
prepareEvents(obj2);
function prepareEvents(obj) {
var handleMouseUp = function (evt) {
// do work with obj
};
obj.addEventListener('mouseup', handleMouseUp);
}
_obj1 = obj1;
_obj2 = obj2;
}
function cleanup() {
if (_obj1) {
_obj1.removeEventListener('mouseup');
}
if (_obj2) {
_obj2.removeEventListener('mouseup');
}
}
return {
init: init,
cleanup: cleanup
}
})();
function moveNext(){
MyGlobalRef.cleanup();
MyGlobalRef.init(GetNewObj1A(), GetNewObj1A());
}
You call prepareEvents before the function, put function prepareEvents(obj) before your init function. Try it.

Send event when module was executed

I'm really stuck on this.. I need to send an event when both Load module and Hide module code was executed, and only then send the event. Ideas on how to achieve this?
// Load module
(
function() {
var s=document.createElement('script');
s.type='text/javascript';
s.async=true;
s.src='https://example.com/bundles.js';
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
)();
// Hide module
var inverval = setInterval(hideClass, 100);
function hideClass () {
if ($(".class").hide().length > 0) clearInterval(inverval);
}
// When both happend = Send a event to Google Analytics
DigitalData.push({'event':Module, 'eventLabel':'Page'});
If this is your only option, then perhaps there's something you are going about wrongly. Anyway, let's see ... Only when both events have taken place.
var HandleTwoEvents = function (key1, key2) {
this.count = 0;
this.pack = [];
$self = this;
this.startListening = function(fn) {
fn = fn || function () {}
window.addEventListener(key1, function (ev) {
if ($self.pack.indexOf(key1) < 0) {
$self.pack.push(key1);
$self.count++;
if ($self.count == 2) {
fn();
$self.count = 0;
}
}
console.log(key1, ev);
});
window.addEventListener(key2, function (ev) {
if ($self.pack.indexOf(key2) < 0) {
$self.pack.push(key2);
$self.count++;
if ($self.count == 2) {
fn();
$self.count = 0;
}
}
console.log(key2, ev);
});
}
}
Forgive me, i always use this function to create events
function createEvent(name, obj) {
var evt = document.createEvent("Event");
evt.initEvent(name, true, true);
evt.data = obj;
dispatchEvent(evt);
}
Now, to log both events ...
var both = new HandleTwoEvents("EventKeyOne", "EventKeyTwo");
both.startListening(function () {console.log("This means that both Events have taken place")});
Now, let's test ...
createEvent("EventKeyOne", {});
//key, data are the arguments ... function defined in startListening above does not execute, and upon inspection, both.count is seen to be 1
createEvent("EventKeyTwo", {});
//Now, function executes.
//It also works if "EventKeyTwo" is raised before "EventKeyOne"
Happy Coding!
PS: I'm sure there's a better way to handle the use of the $self variable, with some function binding, i guess. I've never been able to learn it.

Custom Events in CLASS

I need to launch custom events from CLASS. I know to do this with DOM objects and jquery, using triggerHandler, like $(object)..triggerHandler("inputChange", {param:X});
The problem is when i try this with a Class, like this:
var MyClass = (function(){
var static_var = 1;
var MyClass = function () {
var privateVar;
var privateFn = function(){ alert('Im private!'); };
this.someProperty = 5;
this.someFunction = function () {
alert('Im public!');
};
this.say = function() {
alert('Num ' + this.someProperty);
$(this).triggerHandler("eventCustom");
}
this.alter = function() {
this.someProperty ++;
}
};
return MyClass;
})();
TheClass = new MyClass();
$(TheClass).on('eventCustom', function() {
alert('Event!');
});
TheClass.say();
This doesn't launch warnings or errors, but the events listener is not working (or event is not dispatched). I think the jQuery event system doesn't work with not DOM object, correct?
Any other way (I need events, not callbacks for my specific case) to launch the events?
Thanks a lot!
I wrote an ES6 event class for nowadays in under 100 lines of code without using JQuery. If you don't want to use DOM-events you can extend your class, which should deal with Events.
For listening to events, you can use on, once, onReady, onceReady. On is execute the callbackfunction every time the label is trigger. Once only one time. The "ready"-functions execute the callback, if the label had been already triggerd before.
For triggering an event, use a trigger. To remove an eventhandler, use off.
I hope the example makes it clear:
class ClassEventsES6 {
constructor() {
this.listeners = new Map();
this.onceListeners = new Map();
this.triggerdLabels = new Map();
}
// help-function for onReady and onceReady
// the callbackfunction will execute,
// if the label has already been triggerd with the last called parameters
_fCheckPast(label, callback) {
if (this.triggerdLabels.has(label)) {
callback(this.triggerdLabels.get(label));
return true;
} else {
return false;
}
}
// execute the callback everytime the label is trigger
on(label, callback, checkPast = false) {
this.listeners.has(label) || this.listeners.set(label, []);
this.listeners.get(label).push(callback);
if (checkPast)
this._fCheckPast(label, callback);
}
// execute the callback everytime the label is trigger
// check if the label had been already called
// and if so excute the callback immediately
onReady(label, callback) {
this.on(label, callback, true);
}
// execute the callback onetime the label is trigger
once(label, callback, checkPast = false) {
this.onceListeners.has(label) || this.onceListeners.set(label, []);
if (!(checkPast && this._fCheckPast(label, callback))) {
// label wurde nocht nicht aufgerufen und
// der callback in _fCheckPast nicht ausgeführt
this.onceListeners.get(label).push(callback);
}
}
// execute the callback onetime the label is trigger
// or execute the callback if the label had been called already
onceReady(label, callback) {
this.once(label, callback, true);
}
// remove the callback for a label
off(label, callback = true) {
if (callback === true) {
// remove listeners for all callbackfunctions
this.listeners.delete(label);
this.onceListeners.delete(label);
} else {
// remove listeners only with match callbackfunctions
let _off = (inListener) => {
let listeners = inListener.get(label);
if (listeners) {
inListener.set(label, listeners.filter((value) => !(value === callback)));
}
};
_off(this.listeners);
_off(this.onceListeners);
}
}
// trigger the event with the label
trigger(label, ...args) {
let res = false;
this.triggerdLabels.set(label, ...args); // save all triggerd labels for onready and onceready
let _trigger = (inListener, label, ...args) => {
let listeners = inListener.get(label);
if (listeners && listeners.length) {
listeners.forEach((listener) => {
listener(...args);
});
res = true;
}
};
_trigger(this.onceListeners, label, ...args);
_trigger(this.listeners, label, ...args);
this.onceListeners.delete(label); // callback for once executed, so delete it.
return res;
}
}
// +++ here starts the example +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class TestClassEvents extends ClassEventsES6 {
constructor() {
super();
this.once('sayHallo', this.fStartToTalk);
this.on('sayHallo', this.fSayHallo);
}
fStartToTalk() {
console.log('I start to talk... ');
}
fSayHallo(name = 'Nobody') {
console.log('Hallo ' + name);
}
}
let testClassEvents = new TestClassEvents();
testClassEvents.trigger('sayHallo', 'Tony');
testClassEvents.trigger('sayHallo', 'Tim');
testClassEvents.onReady('sayHallo', e => console.log('I already said hello to ' + e));
testClassEvents.trigger('sayHallo', 'Angie');
testClassEvents.off('sayHallo');
testClassEvents.trigger('sayHallo', 'Peter');
console.log('I dont say hallo to Peter, because the event is off!')
Your understanding of how javascript works is limited since you are approaching it from a traditional OOP point of view. Take a look at this fiddle http://jsfiddle.net/9pCmh/ & you will see that you can actually pass functions as variables to other functions. There are no classes in javascript, only functions which can be closures which can be made to emulate traditional classes:
var MyClass = (function(){
var static_var = 1;
var MyClass = function ( callback ) {
var privateVar;
var privateFn = function(){ alert('Im private!'); };
this.someProperty = 5;
this.someFunction = function () {
alert('Im public!');
};
this.say = function() {
alert('Num ' + this.someProperty);
callback();
}
this.alter = function() {
this.someProperty ++;
}
};
return MyClass;
})();
TheClass = new MyClass(function() {
alert('Event!');
});
TheClass.say();
Alternatively you could create a function in your "class" to configure the callback/trigger instead of passing it into the constructor.
Have a look at this as a start for your further reading on this concept... How do JavaScript closures work?
Edit
To appease those critics looking for an eventQueue here is an updated jsfiddle :)
http://jsfiddle.net/Qxtnd/9/
var events = new function() {
var _triggers = {};
this.on = function(event,callback) {
if(!_triggers[event])
_triggers[event] = [];
_triggers[event].push( callback );
}
this.triggerHandler = function(event,params) {
if( _triggers[event] ) {
for( i in _triggers[event] )
_triggers[event][i](params);
}
}
};
var MyClass = (function(){
var MyClass = function () {
this.say = function() {
alert('Num ' + this.someProperty);
events.triggerHandler('eventCustom');
}
};
return MyClass;
})();
TheClass = new MyClass();
events.on('eventCustom', function() {
alert('Event!');
});
events.on('eventCustom', function() {
alert('Another Event!');
});
TheClass.say();

Categories

Resources