fluo
Version:
A prototype-based fork of the Reflux data flow library similar to Facebook Flux
258 lines (225 loc) • 8.93 kB
JavaScript
var _ = require('./utils');
var Publisher = require('./Publisher');
var instanceJoinCreator = require('./instanceJoinCreator');
/**
* A module of methods related to listening.
* @constructor
* @extends {Publisher}
*/
var Listener = function () {
Publisher.call(this);
this.subscriptions = [];
};
_.inherits(Listener, Publisher);
/**
* An internal utility function used by `validateListening`
*
* @param {Action|Store} listenable The listenable we want to search for
* @returns {Boolean} The result of a recursive search among `this.subscriptions`
*/
Listener.prototype.hasListener = function (listenable) {
var subs = this.subscriptions || [];
for (var i = 0; i < subs.length; ++i) {
var listenables = [].concat(subs[i].listenable);
for (var j = 0; j < listenables.length; ++j) {
var listener = listenables[j];
if (listener === listenable) {
return true;
}
if (listener.hasListener && listener.hasListener(listenable)) {
return true;
}
}
}
return false;
};
/**
* A convenience method that listens to all listenables in the given object.
*
* @param {Object} listenables An object of listenables. Keys will be used as callback method names.
*/
Listener.prototype.listenToMany = function (listenables) {
var allListenables = flattenListenables(listenables);
for (var key in allListenables) {
var cbname = _.callbackName(key);
var localname = this[cbname] ? cbname : this[key] ? key : undefined;
if (localname) {
var callback = (
this[cbname + 'Default'] ||
this[localname + 'Default'] ||
localname
);
this.listenTo(allListenables[key], localname, callback);
}
}
};
/**
* Checks if the current context can listen to the supplied listenable
*
* @param {Action|Store} listenable An Action or Store that should be
* listened to.
* @returns {String|Undefined} An error message, or undefined if there was no problem.
*/
Listener.prototype.validateListening = function (listenable) {
if (listenable === this) {
return 'Listener is not able to listen to itself';
}
if (!_.isFunction(listenable.listen)) {
return listenable + ' is missing a listen method';
}
if (listenable.hasListener && listenable.hasListener(this)) {
return 'Listener cannot listen to this listenable because of circular loop';
}
};
/**
* Sets up a subscription to the given listenable for the context object
*
* @param {Action|Store} listenable An Action or Store that should be
* listened to.
* @param {Function|String} callback The callback to register as event handler
* @param {Function|String} defaultCallback The callback to register as default handler
* @returns {Object} A subscription obj where `stop` is an unsub function and `listenable` is the object being listened to
*/
Listener.prototype.listenTo = function (listenable, callback, defaultCallback) {
_.throwIf(this.validateListening(listenable));
this.fetchInitialState(listenable, defaultCallback);
var subs = this.subscriptions;
var desub = listenable.listen(this[callback] || callback, this);
var unsubscriber = function () {
var index = subs.indexOf(subscriptionObj);
_.throwIf(index === -1,
'Tried to remove listen already gone from subscriptions list!');
subs.splice(index, 1);
desub();
};
var subscriptionObj = {
stop: unsubscriber,
listenable: listenable
};
subs.push(subscriptionObj);
return subscriptionObj;
};
/**
* Stops listening to a single listenable
*
* @param {Action|Store} listenable The action or store we no longer want to listen to
* @returns {Boolean} True if a subscription was found and removed, otherwise false.
*/
Listener.prototype.stopListeningTo = function (listenable) {
var subs = this.subscriptions || [];
for (var i = 0; i < subs.length; ++i) {
var sub = subs[i];
if (sub.listenable === listenable) {
sub.stop();
_.throwIf(subs.indexOf(sub) !== -1,
'Failed to remove listen from subscriptions list!');
return true;
}
}
return false;
};
/**
* Stops all subscriptions and empties subscriptions array
*/
Listener.prototype.stopListeningToAll = function () {
var subs = this.subscriptions || [];
var remaining;
while ((remaining = subs.length)) {
subs[0].stop();
_.throwIf(subs.length !== remaining - 1,
'Failed to remove listen from subscriptions list!');
}
};
/**
* Used in `listenTo`. Fetches initial data from a publisher if it has a `getInitialState` method.
* @param {Action|Store} listenable The publisher we want to get initial state from
* @param {Function|String} defaultCallback The method to receive the data
*/
Listener.prototype.fetchInitialState = function (listenable, defaultCallback) {
if (typeof defaultCallback === 'string') {
defaultCallback = this[defaultCallback];
}
var self = this;
if (_.isFunction(defaultCallback) && _.isFunction(listenable.getInitialState)) {
var data = listenable.getInitialState();
if (data && _.isFunction(data.then)) {
data.then(function() {
defaultCallback.apply(self, arguments);
});
} else {
defaultCallback.call(this, data);
}
}
};
/**
* The callback will be called once all listenables have triggered at least once.
* It will be invoked with the last emission from each listenable.
* @param {...Publishers} publishers Publishers that should be tracked.
* @param {Function|String} callback The method to call when all publishers have emitted
* @returns {Object} A subscription obj where `stop` is an unsub function and `listenable` is an array of listenables
*/
Listener.prototype.joinTrailing = instanceJoinCreator('last');
/**
* The callback will be called once all listenables have triggered at least once.
* It will be invoked with the first emission from each listenable.
* @param {...Publishers} publishers Publishers that should be tracked.
* @param {Function|String} callback The method to call when all publishers have emitted
* @returns {Object} A subscription obj where `stop` is an unsub function and `listenable` is an array of listenables
*/
Listener.prototype.joinLeading = instanceJoinCreator('first');
/**
* The callback will be called once all listenables have triggered at least once.
* It will be invoked with all emission from each listenable.
* @param {...Publishers} publishers Publishers that should be tracked.
* @param {Function|String} callback The method to call when all publishers have emitted
* @returns {Object} A subscription obj where `stop` is an unsub function and `listenable` is an array of listenables
*/
Listener.prototype.joinConcat = instanceJoinCreator('all');
/**
* The callback will be called once all listenables have triggered.
* If a callback triggers twice before that happens, an error is thrown.
* @param {...Publishers} publishers Publishers that should be tracked.
* @param {Function|String} callback The method to call when all publishers have emitted
* @returns {Object} A subscription obj where `stop` is an unsub function and `listenable` is an array of listenables
*/
Listener.prototype.joinStrict = instanceJoinCreator('strict');
/**
* Extract child listenables from a parent from their
* children property and return them in a keyed Object
*
* @param {Object} listenable The parent listenable
*/
var mapChildListenables = function (listenable) {
var children = {};
var childListenables = listenable.children || [];
for (var i = 0; i < childListenables.length; ++i) {
var childName = childListenables[i];
if (listenable[childName]) {
children[childName] = listenable[childName];
}
}
return children;
};
/**
* Make a flat dictionary of all listenables including their
* possible children (recursively), concatenating names in camelCase.
*
* @param {Object} listenables The top-level listenables
*/
var flattenListenables = function (listenables) {
var flattened = {};
for (var key in listenables) {
var listenable = listenables[key];
var childMap = mapChildListenables(listenable);
// recursively flatten children
var children = flattenListenables(childMap);
// add the primary listenable and chilren
flattened[key] = listenable;
for (var childKey in children) {
var childListenable = children[childKey];
flattened[key + _.capitalize(childKey)] = childListenable;
}
}
return flattened;
};
module.exports = Listener;