atom/atom — pull request #3418 by nathansobo (2014-08-28 18:46:19+00:00)
Title: Clean Up Workspace Event APIs

In #3378 I discussed the idea of integrating RxJS observables for our event system. But in the interest of getting to the API freeze faster, I think we should avoid pulling a complex framework into core at this time. This PR focuses on simplifying and clarifying the events API without introducing observables, but the proposed changes are compatible with adding RxJS observables in the future.

The goal of this PR is to reform all the event APIs in workspace, which includes `Workspace` and `Pane`. `PaneContainer` and `PaneAxis` aren't public, but they'll go along for the ride in the interest of internal consistency. Once we're happy with our policies in the workspace, we can apply similar reforms across the rest of core.

## Tasks

* [ ] Add New APIs
* [x] Pane
* [ ] Workspace
* [x] PaneAxis
* [ ] PaneContainer
* [ ] Document APIs
* [ ] Remove emissary dependencies where APIs aren't in use

## API Conventions

* All event subscriptions should occur through explicit methods rather than through strings passed to `::on`.

* Event subscription methods take callbacks as their final argument. In the future, we can add the ability to return an observable when no callback is provided.

* Event subscription methods return *disposables*. When you call `.dispose()` on the returned disposable the subscription is cancelled.

* Event callbacks are always invoked with a single object. If multiple pieces of information need to be passed, the object can be a hash with named entries.

* Event subscription methods conform to the following naming conventions:
* Discrete events are named with `onDid[Verb][Noun]` or `onWill[Verb][Noun]`:
* `onDidActivate`
* `onDidAddItem`
* `onWillDestroyItem`
* Properties that change can have two kinds of methods:
* Methods that invoke the callback the next time the property *changes* take the form `onDidChange[PropertyName]`:
* `onDidChangeActiveItem`
* Methods that invoke the callback immediately with the current value and also whenever it changes take the form `observe[PropertyName]`:
* `observeActiveItem`
* Collections can be observed with `observe[CollectionName]`, which will invoke the callback with all current and future elements:
* `observePanes`

## Usage Guidelines

I've created a simple npm called [event-kit][event-kit]. The goal is to provide a few building blocks for implementing and consuming evented APIs while maintaining maximal simplicity.

### Implementing Evented APIs

We want to move away from mixins and toward composition. Toward that end, `event-kit` exports a simple `Emitter` object that can be used to streamline evented APIs. Rather than mixing it in, you construct an `Emitter` instance and use it internally. Here's a sample of how you use it:

```coffee
{Emitter} = require 'event-kit'

class Pane
constructor: ->
# implementation...
@emitter = new Emitter

onDidChangeActiveItem: (fn) ->
@emitter.on 'did-change-active-item', fn

setActiveItem: (activeItem) ->
unless activeItem is @activeItem
@activeItem = activeItem
@emitter.emit 'did-change-active-item', activeItem
@activeItem
```

This is similar to the existing emitter from emissary, but it's maximally simple and omits several features such as namespaces and support for multiple event arguments. The `::on` method also returns `Disposable` instances which unsubscribe when `.dispose()` is called on them.

Using emitters via *instances* instead of mixins ensures users won't call `::on` directly and gives us leverage for deprecating existing direct usages of `::on`.

If needed, `Disposable` can be used directly. You build a disposable with a function that is called when the disposable is disposed. This intentionally follows the same API as RxJS.

```coffee
{Disposable} = require 'event-kit'

disposable = new Disposable(=> @cleanUp())
```

### Consuming Evented APIs

We want to encourage users to dispose of subscriptions when they are done with them, and we now think the best way to do this is to make it *explicit*. So now rather than using the `Subscriber` mixin, we will encourage users to build their own `CompositeDisposable` instance and add subscriptions to it.

```coffee
{CompositeObservable} = require 'event-kit'

class TabBar
constructor: (@pane) ->
@subscriptions = new CompositeObservable
@subscriptions.add @pane.observeActiveItem -> # ...
@subscriptions.add @pane.onDidAddItem -> # ...
@subscriptions.add @pane.onDidRemoveItem -> # ...

destroyed: ->
@subscriptions.dispose()
```

Users will need to remember to dispose of the subscriptions, but we think that making it more explicit will help clarify what is going on in examples and increase the likelihood that package authors clean up.