Skip to content

Commit

Permalink
Implement dispatch.once. Fixes #1.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Jun 6, 2015
1 parent 328d40c commit 872e7bc
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 16 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Register named callbacks and call them with arguments. Dispatching is a convenie

Changes from D3 3.x:

* It is now an error to attempt to register a callback type that: conflicts with a built-in property on all objects, such as `__proto__` or `hasOwnProperty`; conflicts with a built-in method on dispatch (namely, `on`); or conflicts with another type on the same dispatch (e.g., `dispatch("foo", "foo")`).
* It is now an error to attempt to register a callback type that: conflicts with a built-in property on all objects, such as `__proto__` or `hasOwnProperty`; conflicts with a built-in method on dispatch (e.g., `once` or `on`); or conflicts with another type on the same dispatch (e.g., `dispatch("foo", "foo")`).

* The exposed [dispatch.*type*](#type) field is now strictly a method for invoking callbacks. Use `dispatch.on(type, …)` to get or set callbacks, rather than `dispatch[type].on(…)`.

Expand Down Expand Up @@ -43,6 +43,10 @@ The *type* is a string, such as `"start"` or `"end"`. To register multiple callb

If a *callback* is specified, it is registered for the specified *type*. If a callback was already registered for the same type, the existing callback is removed before the new callback is added. If *callback* is not specified, returns the current callback for the specified *type*, if any. The specified *callback* is invoked with the context and arguments specified by the caller; see [dispatch.*type*](#type).

<a name="once" href="#once">#</a> dispatch.<b>once</b>(<i>type</i>, <i>callback</i>)

Like [dispatch.on](#on), but automatically removes the specified *callback* upon its first invocation.

<a name="type" href="#type">#</a> dispatch.<b>*type*</b>(<i>arguments…</i>)

The *type* method (such as `dispatch.start` for the `"start"` type) invokes each registered callback for the specified type, passing the callback the specified *arguments*. The `this` context will be used as the context of the registered callbacks.
Expand Down
52 changes: 37 additions & 15 deletions src/dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ function Dispatch(types) {
typeName,
that = this;

that.on = function(type, callback) {
var i = (type += "").indexOf("."), name = "";
that.once = function(type, callback) {
type = parseType(type);
type.type.on(type.name, callback).once = true;
return that;
};

// Extract optional name, e.g., "foo" in "click.foo".
if (i >= 0) name = type.slice(i + 1), type = type.slice(0, i);
that.on = function(type, callback) {
type = parseType(type);

// If a type was specified, set or get the callback as appropriate.
if (type) return type = typeByName.get(type), arguments.length < 2 ? type.on(name) : (type.on(name, callback), that);
if (type.type) return arguments.length < 2
? type.type.on(type.name)
: (type.type.on(type.name, callback), that);

// Otherwise, if a null callback was specified, remove all callbacks with the given name.
// Otherwise, ignore! Can’t add or return untyped callbacks.
if (arguments.length === 2) {
if (callback == null) typeByName.forEach(function(type) { type.on(name, null); });
if (callback == null) {
typeByName.forEach(function(t) {
t.on(type.name, null);
});
}
return that;
}
};
Expand All @@ -35,6 +44,14 @@ function Dispatch(types) {
that[typeName] = applyOf(type);
}

// Extract optional name, e.g., "foo" in "click.foo".
function parseType(type) {
var i = (type += "").indexOf("."), name = "";
if (i >= 0) name = type.slice(i + 1), type = type.slice(0, i);
if (type && !(type = typeByName.get(type))) throw new Error("unknown type: " + type);
return {type: type, name: name};
}

function applyOf(type) {
return function() {
type.apply(this, arguments);
Expand All @@ -52,18 +69,22 @@ function Type() {

Type.prototype = {
apply: function(that, args) {
var z = this.callbacks, // Defensive reference; copy-on-remove.
var callbacks = this.callbacks, // Defensive reference; copy-on-remove.
callback,
callbackValue,
i = -1,
n = z.length,
l;
n = callbacks.length;
while (++i < n) {
if (l = z[i].value) {
l.apply(that, args);
if (callbackValue = (callback = callbacks[i]).value) {
if (callback.once) this.on(callback.name, null);
callbackValue.apply(that, args);
}
}
},
on: function(name, callback) {
var callback0 = this.callbackByName.get(name += ""), i;
on: function(name, value) {
var callback0 = this.callbackByName.get(name),
callback,
i;

// Return the current callback, if any.
if (arguments.length < 2) return callback0 && callback0.value;
Expand All @@ -77,10 +98,11 @@ Type.prototype = {
}

// Add the new callback, if any.
if (callback) {
callback = {value: callback};
if (value) {
callback = {name: name, value: value, once: false};
this.callbackByName.set(name, callback);
this.callbacks.push(callback);
return callback;
}
}
};
Expand Down
20 changes: 20 additions & 0 deletions test/dispatch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,23 @@ tape("dispatch(type).on(.name, f) has no effect", function(test) {
test.equal(d.on(".a"), undefined);
test.end();
});

tape("dispatch(type).once(type, f) returns the dispatch object", function(test) {
var d = dispatch("foo");
test.equal(d.once("foo", function() {}), d);
test.end();
});

tape("dispatch(type).once(type, f) assigns the specified callback, and then removes it upon invocation", function(test) {
var foo = 0,
FOO = function() { ++foo; },
d = dispatch("foo", "bar");
d.once("foo", FOO);
test.equal(d.on("foo"), FOO);
d.foo();
test.equal(foo, 1);
test.equal(d.on("foo"), undefined);
d.foo();
test.equal(foo, 1);
test.end();
});

0 comments on commit 872e7bc

Please sign in to comment.