Is there a sane way to remove event handlers using SpacePen/jQuery?


#1

It appears that if you want to remove an event listener that was added via on(), preempt(), or command(), you have to hold on to a bunch of references so that you can later pass them to off(). Oftentimes, an event handler is an anonymous function, so if you want to remove the handler later, you can no longer declare it an anonymous way, which is annoying. It also requires creating an object with at least two fields (events and handler), which you have to pass around to whoever is ultimately responsible for removing the listener. This also means that your handler is potentially exposed to much more code than it should be.

By comparison, consider goog.events.listen() from the Closure Library. It takes an equivalent set of arguments that on() does, but it returns an opaque object to represent the registration of your listener: goog.events.Key (this is just a type alias for number, so it is very cheap). The listener can be removed by goog.events.unlistenByKey(key), which takes the goog.events.Key object returned by goog.events.listen(). This addresses all of the shortcomings of the jQuery approach mentioned above.

(1) Is there a better way to do this in jQuery/SpacePen today?
(2) Could an analogous abstraction be introduced to SpacePen?


#2
  1. As far as I know this is just a jquery function. You might try the jquery forum.

  2. SpacePen is going away in Atom. It is being replaced by react. Now that I think about it what you want may be in react. Check out react.


#3

@mark_hahn

(1) @nathansobo noted yesterday that “React is on pause:” What’s the status of React in Atom?.

(2) According to the blog post, SpacePen will be kept around for backwards compatibility.

(3) I am trying extremely hard to build an autocomplete widget based on React. Unfortunately, it requires handling events from EditorView, which dispatches events in a SpacePen/jQuery fashion. Getting the two event systems to interoperate seems to be a known issue.


#4

Currently, it appears that removing a listener added via preempt() is impossible: https://github.com/atom/space-pen/issues/49.


#5

Apologies for things being in flux with regards to the view system. We’ll be working to iron this stuff out as the next thing on our list. For now, you could try using the Subscriber mixin from our emissary library. It tracks the subscription for you on the subscribing object and can automatically unsubscribe from everything or from a particular object at a later point.

{Subscriber} = require 'emissary'

class MyClass
  Subscriber.includeInto(this)
  
  myMethod: ->
    @subscribe view, 'event-name', -> # ...

  unsubscribeLater: ->
    @unsubscribe()
    # or
    @unsubscribe(view)

#6

@nathansobo I’m looking at subscribe.coffee, and since it’s CoffeeScript, it’s hard to tell whether any of these methods return a meaningful value or not. Specifically, do any of these methods return an opaque value that I can use with unsubscribe() later? If not, it still seems like if I subscribe multiple handlers to the same event target with the same type, I still have to hold a reference to the closure in order to remove the handler later (assuming I don’t want to remove the other handlers).


#7

Subscriber will hold onto them for you on the object that mixes it in. This is one of the many reasons why we’re moving away from jQuery. Our Emitter mixin returns subscription objects, but jQuery doesn’t. The problem is that you can’t currently register a command without jQuery. We’ll be fixing that soon.


#8

@nathansobo It seems like the Closure equivalent is an goog.events.EventTarget. Most classes in the Closure Library extend EventTarget (mixins aren’t common in Closure). EventTarget delegates to goog.events under the hood, but makes it easy to remove all of the listeners registered through it, as you desire.

One of the nice things that it does is set the instance as the default receiver for the listener, so inside your class, you could do:

MyClassThatExtendsEventTarget.prototype.addHandler = function() {
  this.listen('my-event-type', this.myHandler);
};

as opposed to having to create a new object via bind():

MyClassThatExtendsEventTarget.prototype.addHandler = function() {
  // UNNECESSARY bind()!
  this.listen('my-event-type', this.myHandler.bind(this));
};

or creating a new closure, which in CoffeeScript looks cheap:

  addHandler: ->
    @listen 'my-event-type', () => @myHandler()

but in reality, this is quite a bit more JavaScript than it should be:

  Foo.prototype.addHandler = function() {
    return this.listen('my-event-type', (function(_this) {
      return function() {
        return _this.myHandler();
      };
    })(this));
  };

So I’m not sure how Subscriber works, but hopefully it has a default binding for this so you can just do:

  addHandler: ->
    @subscribe 'my-event-type', @myHandler

#9

Filed an issue to get a stopgap solution in the near-term: https://github.com/atom/space-pen/issues/51.


#10

Mu package is getting Error: Cannot find module 'emissary'. Do I have to include it in node_modules or something?

Edit: Never mind. {Subscriber} = atom worked.

Edit again: No, it didn’t work. My original question stands.

3rd Edit: Duh, it is an npm module. Sorry for your trouble.


#11

Include it in package.json:

And then run apm install from the directory where the package.json is stored.