My Firefox extension is providing a JavaScript function that a website can use to access addon functionality. The website calls this function and provides two callbacks.
Website code:
function onButtonClick() {
var callbackSuccess = function() { alert("Yeah!"); };
var callbackError = function() { alert("Oh no!"); };
if (window.magicAddon) { // Check if my addon is installed
magicAddon.doStuff(callbackSuccess, callbackError);
}
}
Content Script:
unsafeWindow.magicAddon = {
doStuff: function(callbackSuccess, callbackError) {
// Bind the two callbacks to events. The addon will fire one of them
self.port.on("doStuffSuccess", callbackSuccess);
self.port.on("doStuffError", callbackError);
// Fire the event that lets the addon do stuff
self.port.emit("doStuff");
}
};
That works great on the first call, but the next time the website calls doStuff(), new listeners add up and alert() is executed twice. Next time three alerts, and so on.
Any idea how to elegantly avoid that listeners add up? Can I clear an event type completely?
What not worked so far:
self.port.once(..)
instead, because I have two callback events: Only the one that fires back is cleared, the other one stays and adds up with the next.self.port.removeListener
, because I don't have the old callback reference.Problem seems similar to How to remove an event listener?, only that he uses one callback listener and therefore can use self.port.once(..)
.
You can use self.port.once
and then manually remove the other callback:
doStuff: function(callbackSuccess, callbackError) {
// Bind the two callbacks to events. The addon will fire one of them
self.port.once("doStuffSuccess", function() {
callbackSuccess();
self.port.removeListener(callbackError);
});
self.port.once("doStuffError", function() {
callbackError();
self.port.removeListener(callbackSuccess);
});
// Fire the event that lets the addon do stuff
self.port.emit("doStuff");
}
You're in a content script, so you can't clear an event type completely, you're able to do so only in main add-on code, and using low level API.
However, I would suggest to avoid unsafeWindow
to provide this kind of functionality, because, well, it's unsafe. If you maintain your API async, you could use the postMessage
pipeline between content script and pages, to do the same; and provides a separate javascript file that people can include in their website where you expose an abstraction of postMessages calls (e.g. magicAddon.doStuff()
). If you want, you could also automatically inject that script from your add-on, in the websites.
Handling this mechanism is definitely a bit more complex, but you can avoid the usage of unsafeWindow
.
You can find more about content script communication here.
Hope it helps!
Update: To answer at your comment, you need a variable to trace the doStuff
call activity:
doStuff: function() {
var executing = false;
return function(callbackSuccess, callbackError) {
if (executing)
return;
executing = true;
// Bind the two callbacks to events. The addon will fire one of them
self.port.once("doStuffSuccess", function() {
executing = false;
callbackSuccess();
self.port.removeListener(callbackError);
});
self.port.once("doStuffError", function() {
executing = false;
callbackError();
self.port.removeListener(callbackSuccess);
});
// Fire the event that lets the addon do stuff
self.port.emit("doStuff");
}
}()
Note the ()
at the end. Basically in this way the function set at the end to doStuff
, is the result of the function we initially assigned.
In that way we create a closure for the doStuff
method, where a executing
variable is living, and keep trace if there is already a doStuff
execution or not, in order to discard any other doStuff
call, until is done.
Note: even if in this case is not necessary for javascript, could be a good convention wrap that function in parenthesis, to identify that this function is 'self executing': `doStuff: (function() {...}())
You could also use property of object of magicAddon
for this job, but in that case it will be exposed.