Given a publish-subscribe pattern using ES6 as follows (extracted from https://davidwalsh.name/pubsub-javascript):
class PubSub {
constructor() {
this.handlers = [];
}
subscribe(event, handler, context) {
if (typeof context === 'undefined') {
context = handler;
}
{
if (this.getHandler(event, handler) == null) {
this.handlers.push({event: event, handler: handler.bind(context), key: Guid()});
}
}
}
unsubscribe(event, handler) {
let filteredHandler = this.getHandler(event, handler);
if (filteredHandler != null) {
let idx = this.handlers.indexOf(filteredHandler);
if (idx > -1) {
this.handlers.splice(idx, 1);
}
}
}
publish(event, args) {
this.handlers.forEach(topic => {
if (topic.event === event) {
topic.handler(args)
}
})
}
getHandler(event, handler) {
if (this.handlers == null || this.handlers.length < 1) {
return null;
}
let filtered = null;
this.handlers.forEach(topic => {
if (topic.event === event && topic.handler === handler) {
filtered = topic;
}
});
return filtered;
}
getNumOfSubsribers() {
if (this.handlers != null && this.handlers.length > 0) {
return this.handlers.length;
}
return 0;
}
}
The subscribe and publish methods work. However, the getHandler and unsubscribe method do not work as expected (getHandler seems returning null). I have tried to search around but could not get a satisfactory solution to this problem (not sure how a function bound to a given context can be filtered out from an array).
What have I done wrong in the code? Kindly advise me on getHandler and also unsubscribe part of the code.
Appreciate some kind help.
That code is odd in a couple of ways.
The reason getHandler doesn't work is that the handler property of the object pushed on handlers is not the function that was passed in; it's the result of calling bind on that function. Formatted properly, this is subscribe:
subscribe(event, handler, context) {
if (typeof context === 'undefined') {
context = handler;
}
{
if (this.getHandler(event, handler) == null) {
this.handlers.push({
event: event,
handler: handler.bind(context), // ** NOTE **
key: Guid()
});
}
}
}
That value will never be equal to the original, by definition.
Instead, it should include the original handler as well so it can check for it later. Let's also get rid of the pointless standalone block:
subscribe(event, handler, context) {
if (typeof context === 'undefined') {
context = handler;
}
if (this.getHandler(event, handler) == null) {
this.handlers.push({
event: event,
handler: handler.bind(context),
originalHandler: handler, // ***
key: Guid()
});
}
}
Now, getHandler can look for matches with originalHandler. While we're there, let's stop looping when we find the handler rather than keeping going, and use the semantically-appropriate Array#find:
getHandler(event, handler) {
if (this.handlers == null || this.handlers.length < 1) {
return null;
}
let filtered = this.handlers.find(topic => topic.event === event && topic.originalHandler === handler);
return filtered;
}
There are other issues with the code (like binding the handler to itself if no context is provided), but a full code review is out of scope; the above is why getHandler doesn't work and thus why unsubscribe doesn't work. With that fix, unsubscribe should also work (though it seems odd to search twice).
Related
I am calling an object method in two ways in my code:
this.reveal.updateVisuals(i, 'show');
or
this.reveal.updateVisuals(i, 'hide');
and I am passing the hide and show condition as a string, to be later evaluated and used as a method. Please note the condition: if (effect === 'show/hide').
updateVisuals: function (time, effect) {
// Check if parameter exists and property can be read
if (this.breakpointsMap && typeof this.breakpointsMap[checkTime] !== "undefined") {
if (effect === 'show') {
// display the items that were fast forwarded
var k = this.breakpointsMap[checkTime].length;
while (k--) {
try {
this.breakpointsMap[checkTime][k].show();
} catch (err) {}
}
} else if (effect === 'hide') {
// display the items that were fast forwarded
var k = this.breakpointsMap[checkTime].length;
while (k--) {
try {
this.breakpointsMap[checkTime][k].hide();
} catch (err) {}
}
}
}
}
However the code seems duplicated and I was wondering if there is a way to pass hide or show as a method to the method and apply it on the array, when needed. I tried something like this:
this.reveal.updateVisuals(i).show
There are a lot of ways you can use to simplify this, here are a couple:
updateVisuals: function (time, effect) {
if (this.breakpointsMap && typeof this.breakpointsMap[checkTime] !== "undefined") {
this.breakpointsMap[checkTime].forEach(e => e[effect]());
}
}
Or returning the array:
updateVisuals: function (time, effect) {
if (this.breakpointsMap && typeof this.breakpointsMap[checkTime] !== "undefined") {
return this.breakpointsMap[checkTime];
}else{
return [];
}
}
this.reveal.updateVisuals(i).forEach(e => e.show());
You can access a method property by it's (string) name using [bracket] notation.
updateVisuals: function (time, effect) {
// Check if parameter exists and property can be read
if (this.breakpointsMap && typeof this.breakpointsMap[checkTime] !== "undefined") {
var k = this.breakpointsMap[checkTime].length;
while (k--) {
try {
this.breakpointsMap[checkTime][k][effect]();
} catch (err) {}
}
}
}
if you are using Es6, you can do:
function updateVisuals (time, effect) {
// Check if parameter exists and property can be read
if (this.breakpointsMap && typeof this.breakpointsMap[checkTime] !== "undefined") {
let execFn= (arrindex) => breakpointsMap[checkTime][arrindex].show();
if (effect === 'hide')
{
execFn = (arrindex) => breakpointsMap[checkTime][arrindex].hide();
}
// display the items that were fast forwarded
var k = this.breakpointsMap[checkTime].length;
while (k--) {
try {
execFn(k);
} catch (err) {}
}
}
}
I assume that var checkTime is global or in closure. If you are using version lower tan es6 you can use execFn= function (arrindex) {...}, a then bind this argument when calling method after.
I have a shimmed and polyfilled version of angularjs 1.3 working perfectly on ie8. Unfortunately when mootools is included on the page there are quite a few conflicts. I have managed to get a handle on all but one with the following which adds add / remove EventListener and dispatchEvent to Window.prototype, HTMLDocument.prototype and Element.prototype. It checks to see if mootools is loaded and if so it adds them differently.
!window.addEventListener && (function (WindowPrototype, DocumentPrototype, ElementPrototype, addEventListener, removeEventListener, dispatchEvent, registry) {
var addEventListenerFn = function(type, listener) {
var target = this;
registry.unshift([target, type, listener,
function (event) {
event.currentTarget = target;
event.preventDefault = function () {
event.returnValue = false;
};
event.stopPropagation = function () {
event.cancelBubble = true;
};
event.target = event.srcElement || target;
listener.call(target, event);
}]);
// http://msdn.microsoft.com/en-us/library/ie/hh180173%28v=vs.85%29.aspx
if (type === 'load' && this.tagName && this.tagName === 'SCRIPT') {
var reg = registry[0][3];
this.onreadystatechange = function (event) {
if (this.readyState === "loaded" || this.readyState === "complete") {
reg.call(this, {
type: "load"
});
}
}
} else {
this.attachEvent('on' + type, registry[0][3]);
}
};
var removeEventListenerFn = function(type, listener) {
for (var index = 0, register; register = registry[index]; ++index) {
if (register[0] == this && register[1] == type && register[2] == listener) {
if (type === 'load' && this.tagName && this.tagName === 'SCRIPT') {
this.onreadystatechange = null;
}
return this.detachEvent('on' + type, registry.splice(index, 1)[0][3]);
}
}
};
var dispatchEventFn = function(eventObject) {
return this.fireEvent('on' + eventObject.type, eventObject);
};
if(Element.prototype.$constructor && typeof Element.prototype.$constructor === 'function') {
Element.implement(addEventListener, addEventListenerFn);
Element.implement(removeEventListener, removeEventListenerFn);
Element.implement(dispatchEvent, dispatchEventFn);
Window.implement(addEventListener, addEventListenerFn);
Window.implement(removeEventListener, removeEventListenerFn);
Window.implement(dispatchEvent, dispatchEventFn);
} else {
WindowPrototype[addEventListener] = ElementPrototype[addEventListener] = addEventListenerFn;
WindowPrototype[removeEventListener] = ElementPrototype[removeEventListener] = removeEventListenerFn;
WindowPrototype[dispatchEvent] = ElementPrototype[dispatchEvent] = dispatchEventFn;
}
DocumentPrototype[addEventListener] = addEventListenerFn;
DocumentPrototype[removeEventListener] = removeEventListenerFn;
DocumentPrototype[dispatchEvent] = dispatchEventFn;
})(Window.prototype, HTMLDocument.prototype, Element.prototype, 'addEventListener', 'removeEventListener', 'dispatchEvent', []);
This has resolved all my errors bar one. When this function is called in Angular, when mootools is on the page, and element is a form addEventListener is undefined.
addEventListenerFn = function(element, type, fn) {
element.addEventListener(type, fn, false);
}
specifically this function is called from angulars formDirective like so
addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
Any ideas why the form element still dosn't have the addEventListener function available?
Extending native type via Element.prototype in IE8 is considered very unreliable as the prototype is only partially exposed and certain things are not inheriting from it / misbehave.
http://perfectionkills.com/whats-wrong-with-extending-the-dom/
What MooTools does in this case is rather than work around the quirks of all edgecases that don't adhere to the correct proto chain (and because of IE6/7 before that) is to COPY the Element prototypes on the objects of the DOM nodes as you pass it through the $ selector.
This is not ideal, because.
var foo = document.id('foo');
// all known methods from Element.prototype are copied on foo, which now hasOwnProperty for them
Element.prototype.bar = function(){};
foo.bar(); // no own property bar, going up the chain may fail dependent on nodeType
Anyway, that aside - you can fix your particular problem by copying your methods from the Element.prototype to the special HTMLFormElement.prototype - any any other elements constructors you may find to differ.
It's not scalable, you may then get an error in say HTMLInputElement and so forth, where do you draw the line?
Need to be able to arbitrarily create events (even those created during runtime) and then arbitrarily bind event listeners for such events to elements.
In the code below, everything works exactly as planned except that XBS.pullCurtain() is never called (but the event fires!). I've checked and rechecked the arguments, the sequence, etc.—no errors are thrown, everything appears to be what I'm expecting. But no sauce from the event listener. :S
// this func is used in the larger object below
function eCustom(eName, eProperties) {
var defaultProps = {"bubbles":true, "cancelable":false, "eventPhase":0, "type":eName};
if (typeof(eProperties) == "object") {
for (var prop in eProperties) {
if (eProperties.hasOwnProperty(prop) ) {
defaultProps[prop] = eProperties[prop];
}
}
}
return jQuery.Event(eName, defaultProps);
}
window.XBS = {
cfg: {
minLoadTime: 1000
},
evnt: {
wakeFromSleep: eCustom("wakeFromSleep"),
assetsLoaded: eCustom("assetsLoaded")
},
init: {
// obviously will be expanded haha
XBS.initImages();
},
stopwatch: {
__sw: new Date(),
reset:function() { XBS.stopwatch.startTime = -1},
start: function() {
var now = XBS.stopwatch.__sw.getTime();
XBS.stopwatch.startTime = now;
return now;
},
read: function() { return XBS.stopwatch.__sw.getTime() - XBS.stopwatch.start;},
now: function() { return XBS.stopwatch.__sw.getTime();},
elapsed: function(period) {
var diff = XBS.stopwatch.now() - XBS.stopwatch.startTime;
return (period !== undefined) ? diff > period : diff;
},
startTime:-1
},
sleep: function(period, wakeEvent, onWake, target) {
if (wakeEvent == undefined) wakeEvent = XBS.evnt.wakeFromSleep;
if (isFunction(onWake) ) {
// confirmed code reaches these lines and all vars are as expected
if (target == undefined) target = window;
$(target).on(wakeEvent, onWake);
}
setTimeout(function() {$(target).trigger(wakeEvent) }, period);
},
initImages: function() {
XBS.stopwatch.start();
// loadImages is a jQuery extension (link at bottom), but it works like a charm—
// problem isn't there, I'm 99% sure
$("#image-loading-queue").loadImages({
allLoadedClb:function() {
var success = null;
//minLoadTime because there will be presentational animations
if (XBS.stopwatch.elapsed(XBS.cfg.minLoadTime)) {
success = true;
} else {
var elapsed = XBS.cfg.minLoadTime - XBS.stopwatch.elapsed();
XBS.sleep(elapsed, XBS.evnt.wakeFromSleep, XBS.pullCurtain, "#curtain");
}
}
});
// this return is a placeholder for pending functionality, which is why it's not used
return success;
},
pullCurtain: function() {
// this will get more elaborate, but for now, I just want to call the darn thing!
$("#curtain").fadeToggle();
}
}
Borrowed Library:
http://www.jqueryscript.net/loading/Asynchronous-Image-Loading-with-jQuery-Image-Loader-Plugin.html
jsFiddle demo
You have an error in your jQuery on method call (which your JavaScript console is probably complaining about). The first parameter to on should only be the name of the event to handle (a string), not a jQuery Event object. Take a look at the documentation. It has two signatures: one which takes the method name (e.g. 'click') and one which takes a map of multiple method names to functions (e.g. {'click': fn1, 'mouseover': fn2}). It doesn't have a method signature for jQuery Event objects.
When you bind the handler, change the line to pass the event name only:
$(target).on(wakeEvent.type, onWake);
I want to do global event handling for reporting JavaScript errors. We have minified JS files in production so I'm attempting get it done with the help of sourcemap.
Unfortunately, uncaught errors (reported by the browser's top-level
error handler, window.onerror) do not currently include column numbers
in any current browser. The HTML5 Spec has been updated to require
this, so this may change in the near future.
Source : https://rollbar.com/docs/guides_sourcemaps/
So now I need to wrap backbone view events in try catch block. There should be generic way of extending Backbone.View. Probably somewhere at delegateEvents function.
Here is how finally I wrapped all jQuery event handlers in try-catch block.
// maintain a reference to the existing function
var oldOn = $.fn.on;
// ...before overwriting the jQuery extension point
$.fn.on = function(types, selector, data, fn, /*INTERNAL*/ one) {
// parameter correction for backward compatibility copied from `on` function of jQuery JavaScript Library v1.9.0
// Types can be a map of types/handlers
if (typeof types === "object") {
// ( types-Object, selector, data )
if (typeof selector !== "string") {
// ( types-Object, data )
data = data || selector;
selector = undefined;
}
for (type in types) {
this.on(type, selector, data, types[type], one);
}
return this;
}
if (data == null && fn == null) {
// ( types, fn )
fn = selector;
data = selector = undefined;
} else if (fn == null) {
if (typeof selector === "string") {
// ( types, selector, fn )
fn = data;
data = undefined;
} else {
// ( types, data, fn )
fn = data;
data = selector;
selector = undefined;
}
}
if (fn === false) {
fn = returnFalse;
} else if (!fn) {
return this;
}
// ENDS - parameter correction for backward compatibility copied from `on` function of jQuery JavaScript Library v1.9.0
if (fn) {
var origFn = fn;
var wrappedFn = function() {
try {
origFn.apply(this, arguments);
} catch (e) {
//handle the error here.
}
};
fn = wrappedFn;
}
return oldOn.apply(this, [types, selector, data, fn, /*INTERNAL*/ one]);
};
UPDATE:
There was a bug that jQuery-UI droppable divs were sticking to mouse pointer like glue ;) instead of getting dropped and it was traced to this code as culprit.
So please make sure you wrap this extension with condition if(!(arguments.length === 4 && arguments[1] === null)) {}
Like this.
// maintain a reference to the existing function
var oldOn = $.fn.on;
// ...before overwriting the jQuery extension point
$.fn.on = function(types, selector, data, fn, /*INTERNAL*/ one) {
// We can ignore .bind() calls - they are passed from jquery-ui or other outdated components
if(!(arguments.length === 4 && arguments[1] === null)) {
//rest of the above code here.
}
}
I am trying to set a custom error handler for 3rd party plugins/modules in my core library, but somehow, myHandler does not alert the e.message.
Can somebody help me please? thank you
Function.prototype.setErrorHandler = function(f) {
if (!f) {
throw new Error('No function provided.');
}
var that = this;
var g = function() {
try {
var a = [];
for(var i=0; i<arguments.length; i++) {
a.push(arguments[i]);
}
that.apply(null,a);
}
catch(e) {
return f(e);
}
};
g.old = this;
return g;
};
function myHandler(e) {
alert(e.message)
};
// my Core library object
(function(){
if (typeof window.Core === 'undefined') {
var Core = window.Core = function() {
this.addPlugin = function(namespace, obj){
if (typeof this[namespace] === 'undefined') {
if (typeof obj === 'function') {
obj.setErrorHandler(myHandler);
} else if (!!obj && typeof obj === 'object') {
for (var o in obj) {
if (obj.hasOwnProperty(o) && typeof obj[o] === 'function') {
obj[o].setErrorHandler(myHandler);
}
}
}
this[namespace] = obj;
return true;
} else {
alert("The namespace '" + namespace + "' is already taken...");
//return false;
}
};
};
window.Core = new Core();
}
})();
// test plugin
(function(){
var myPlugin = {
init: function() {},
conf: function() {
return this.foo.x; // error here
}
};
Core.addPlugin("myPlugin", myPlugin);
})();
// test
Core.myPlugin.conf(); // supposed to alert(e.message) from myHandler()
setErrorHandler in the above code doesn't set an error handler on a Function, as such. JavaScript does not give you the ability to change the called code inside a Function object.
Instead it makes a wrapped version of the function it's called on, and returns it.
obj.setErrorHandler(myHandler);
Can't work as the returned wrapper function is thrown away, not assigned to anything.
You could say:
obj[o]= obj[o].setErrorHandler(myHandler);
though I'm a bit worried about the consequences of swapping out functions with different, wrapped versions. That won't necessarily work for all cases and could certainly confuse third-party code. At the least, you'd want to ensure you don't wrap functions twice, and also retain the call-time this value in the wrapper:
that.apply(this, a);
(Note: you don't need the manual conversion of arguments to an Array. It's valid to pass the arguments object directly to apply.)