4

Subsequent to removeEventListener in bootstrapped addon not working when addon disabled, I am exploring other possibilities.

Beside using bind() and caching the bound function, is there a way to use 'this' and pass argument?

// works fine but can't pass argeement
contextMenu.addEventListener('popupshowing', 
     this.contextPopupShowing, false);

// passes the argument but 'this' is no longer available
contextMenu.addEventListener('popupshowing', 
    function(){this.contextPopupShowing(window);}, false);

I have been using a number of event listeners with bind() and I am looking for alternative methods without using bind()

I even tried to grab window with a recursive function from <menupopup id="contentAreaContextMenu" ...>

Update: bind() interferes with removeEventListener

Community
  • 1
  • 1
erosman
  • 7,094
  • 7
  • 27
  • 46
  • What's wrong with using `.bind()` to connect up the right `this` with your method? This is exactly what it's for. There are other work-arounds, but none are as clean as `.bind()`. – jfriend00 Jun 29 '14 at 05:32
  • Thank you jfriend00. As mentioned, the interference of `bind` with `removeEventListener` – erosman Jun 29 '14 at 05:36
  • 2
    Your combination of wanting to use `.removeEventListener()` and control the `this` ptr in your function means you HAVE to both create a stub function to pass to `.addEventListener()` and remember that stub function for use with `.removeEventListener()`. There isn't any alternative. – jfriend00 Jun 29 '14 at 05:47

4 Answers4

3

Since we're talking restartless add-ons... A lot of restartless add-ons use unload and unloadWindow helper functions, to make it easier to implement shutdown properly and also help with stuff like addEventListener, so bear with me for a bit.

The helpers - unload

First, unload is a helper function that you pass another function to, that will be run upon shutdown (or can be called manually). Most implementations are extremely similar to this:

var unloaders = []; // Keeps track of unloader functions.

function unload(fn) {
  if (typeof(fn) != "function") {
    throw new Error("unloader is not a function");
  }
  unloaders.push(fn);
  return function() {
    try {
      fn();
    }
    catch (ex) {
      Cu.reportError("unloader threw " + fn.toSource());
      Cu.reportError(ex);
    }
    unloaders = unloaders.filter(function(c) { return c != fn; });
  };
}

You'd then hook up shutdown to do the right thing:

function shutdown() {
  ...
  for (let i = unloaders.length - 1; i >= 0; --i) {
    try {
      unloaders[i]();
    }
    catch (ex) {
      Cu.reportError("unloader threw on shutdown " + fn.toSource());
      Cu.reportError(ex);
    }
  }
  unloaders.length = 0;
}

Using unload

Now you can do stuff like:

function startup() {
  setupSomething();
  unload(removeSomething);

  setupSomethingElse();
  var manualRemove = unload(removeSomethingElse);
  ...
  if (condition) {
    manualRemove();
  }
}

The helpers - unloadWindow

You'll usually want to create a second function unloadWindow to unload stuff when either your add-on is shut down or the window gets closed, whatever happens first. Not removing stuff when the window gets closed can be very tricky, and create Zombie compartments of your bootstrap.js and/or code modules very easily (this is from experience writing and reviewing restartless add-ons).

function unloadWindow(window, fn) {
  let handler = unload(function() {
    window.removeEventListener('unload', handler, false);
    try {
      fn();
    }
    catch (ex) {
      Cu.reportError("window unloader threw " + fn.toSource());
      Cu.reportError(ex);
    }
  });
  window.addEventListener('unload', handler, false);
};

(Some people might want to "optimize" this, as to have only one "unload" handler, but usually you only have so unloadWindow calls that it won't matter.)

Putting it all together

Now you can .bind stuff and do whatever and let the the unloader closures keep track of it. Also, you can use this to keep your shut down code next to your initialization code, which might increase readability.

function setupWindow(window, document) {
  var bound = this.contextPopupShowing.bind(this);
  contextMenu.addEventListener('popupshowing', bound, false);
  unloadWindow(window, function() {
    contextMenu.removeEventListener('popupshowing', bound, false);
  });

  // Or stuff like
  var element = document.createElement(...);
  contextMenu.appendChild(element);
  unloadWindow(window, function() {
    contextMenu.removeChild(element);
  });

  // Or just combine the above into a single unloader
  unloadWindow(window, function() {
    contextMenu.removeEventListener('popupshowing', bound, false);
    contextMenu.removeChild(element);
  });
}
nmaier
  • 32,336
  • 5
  • 63
  • 78
  • Thank you Nils. I understood the caching of bound functions. I wrote a **tiny** restartless addon to familiarize myself (no pref, no XUL, simple contextmenu search) which is working fine. It has 4 event listeners. I can bind & cache them (creating 4 new variables & 4 new functions) which practically doubles the size of the addon (in memory). I wanted to make sure there are no other (better) ways of doing it before adopting that approach. :) – erosman Jun 29 '14 at 07:19
  • 1
    Yeah, I don't know what you're planning, but the extra memory to use and keep bound functions around is usually negligible unless you have tons of these and are on mobile. But if you got tons of these, you'll run into performance problems anyway. ;) E.g. a single github repo page I have open right now consumes about the same amount of memory as all my add-ons combined, and that includes Adblock Plus with their huge filter lists. – nmaier Jun 29 '14 at 07:28
  • Gracious........ I'm actually new to binding I did for first time here: [GitHub :: Noitidart / HiliteOnSelection](https://github.com/Noitidart/HiliteOnSelection). This is an exceptional post man, thanks. I didn't know they were real helper functions for this. – Noitidart Jun 29 '14 at 07:38
  • I am unclear about unloaders. I am going to put it in a separate question. – erosman Jun 29 '14 at 12:36
  • I was to hasty this morning when I posted and simplified code too much. It is better to call unloaders in reverse order. Edited to change that. – nmaier Jun 29 '14 at 13:14
  • Can you please explain this to me `unloaders = unloaders.filter(function(c) { return c != fn; });` First time I'm exploring `filter` – Noitidart Jun 29 '14 at 23:53
  • @Noitidart Just keep the items where the item is not `fn`, i.e. filter out any items that are `fn`. So it is just removing all instances of `fn` from the array. – nmaier Jun 30 '14 at 08:16
  • I was trying to find bugs in your code cuz u always find bugs in mine and I couldnt. I thought I caught u leaving `unloader`s in the `unloaders` array after window close *and then* I caught you not `filter`ing out `unloader`s when it shouldn't be, like after a window close. But no man. But no. Anyways I learned a ton and plan to implement this. For people that need further explanation on it see here: [GitHubGIST :: Noitidart / _ff-addon-tutorial-BootstrapUnloaderHelperFuncs.md #The-Helpers-Explained-Futher](https://gist.github.com/Noitidart/9d612a2eaa7bef5589e5#the-helpers-explained-further) – Noitidart Jul 22 '14 at 07:13
  • Hi @nmaier I think I found a point of concern, not an issue, just concern. Say the add-on is installed, and i added a bunch of unload functions with `unloadWindow`. Now if addon is uninstalled/disabled or window is closed it will run that unload function. However if Firefox is quit, this causes windows `unload` event to trigger right? I'm not sure if it is triggering. On quit of Firefox I don't want to run my unloaders as it will slow down shutdown of browser process as the whole browser is being torn down. – Noitidart Dec 09 '14 at 10:49
2

Before bind() was supported you had to save a reference to this outside the function. Then pass in a function that can forward the invocation the way you want.

var self = this;
contextMenu.addEventListener('popupshowing', function() {
     self.contextPopupShowing.apply(self, arguments);
}, false);

In this case we use apply to set the context to self, our saved version of this, and to send it whatever arguments were passed to the anonymous function via the magic arguments keyword that contains the list of arguments that a function was passed when invoked.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Thank you Alex. Does `apply` (or `call`) interfere with `removeEventListener` the same way that `bind` does? – erosman Jun 29 '14 at 05:28
  • PS. Someone says at [addEventListener using apply()](http://stackoverflow.com/questions/2891096/addeventlistener-using-apply) that: *"it won't have the apply and call functions on it as properties"*!? – erosman Jun 29 '14 at 05:43
  • 1
    This doesn't allow the OP to call `removeEventListener()` on the event handler which is apparently a requirement. – jfriend00 Jun 29 '14 at 05:51
2

You don't have to use bind for addEventListener. You can use handleEvent. It was in that topic I linked you too:

Removing event listener which was added with bind

MDN :: EventTarget.addEventListener - The value of "this" within the handler

handleEvent is actually a common way the javascript code in the firefox codebase does it.

Copied straight from MDN:

var Something = function(element) {
  this.name = 'Something Good';
  this.handleEvent = function(event) {
    console.log(this.name); // 'Something Good', as this is the Something object
    switch(event.type) {
      case 'click':
        // some code here...
        break;
      case 'dblclick':
        // some code here...
        break;
    }
  };

  // Note that the listeners in this case are this, not this.handleEvent
  element.addEventListener('click', this, false);
  element.addEventListener('dblclick', this, false);

  // You can properly remove the listners
  element.removeEventListener('click', this, false);
  element.removeEventListener('dblclick', this, false);
}

Where I mostly use bind is when doing a for loop and I make anonymous functions with something in the array like arr[i]. If I don't bind it then it always just takes the last element of the array, I have no idea why, and I hate it, so then I go to using [].forEach.call(arr, function(arrI).

Community
  • 1
  • 1
Noitidart
  • 35,443
  • 37
  • 154
  • 323
  • I read that but it was unclear to me. The element is in `window` therefore does that mean that `handleEvent` listens to ALL window events in `window.addEventListener('click', this, false);`? There are other event listeners that need to go elsewhere. – erosman Jun 29 '14 at 06:20
  • No but anything that does `el.addEventListener('click', this, false)` will get routed through that. Where `this` is the `window` scope. But why attach on window man, try to go with JSM modules. Bootstrapped addons are a scope of its own so just put your stuff in there. – Noitidart Jun 29 '14 at 07:21
  • I guess you can also do `el.addEventListener('click', window, false)` notice the `window` in place of `this` – Noitidart Jun 29 '14 at 07:38
  • Thank you Noitidart. I am trying out `handleEvent` to catch all events and redirect accordingly. – erosman Jun 29 '14 at 10:11
1

http://2ality.com/2013/06/auto-binding.html

var listener = myWidget.handleClick.bind(myWidget);
domElement.addEventListener('click', listener);
...
domElement.removeEventListener(listener);
zloctb
  • 10,592
  • 8
  • 70
  • 89