/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam and Gareth Johnson
*/
/**
* A widget that implements this MixIn will be able to
* load and subscribe to Components. The MixIn name is 'subscriber'.
*
* @module bajaux/mixin/subscriberMixIn
* @requires baja
* @requires jquery
* @requires bajaux/events
* @requires bajaux/Widget
*/
define([ 'baja!',
'jquery',
'Promise',
'bajaux/events',
'bajaux/Widget' ], function (
baja,
$,
Promise,
events,
Widget) {
"use strict";
var unknownErr = Widget.unknownErr;
////////////////////////////////////////////////////////////////
// Nav Event Listening
////////////////////////////////////////////////////////////////
function detachNavEventListener(widget) {
// If we have a function monitoring whether the Component in question is removed
// then detach it from the BajaScript NavEvent architecture.
if (widget.$subNavFunc) {
baja.nav.detach("unmount", widget.$subNavFunc);
delete widget.$subNavFunc;
}
}
//TODO: memory leak here.
/*
assigning the function to $subNavFunc seems to set up a circular reference
situation that chrome can't GC (even though widgetNavComp has *no* retaining
tree).
possible workaround: keep a registry of nav event listeners in a local object
rather than sticking them directly onto widgets.
possible workaround: only have one nav event listener, but a registry of
widgets. look up registered widget by handle.
*/
function attachNavEventListener(widget, value) {
// Invoked whenever the loaded Component is removed
widget.$subNavFunc = function widgetNavComp() {
// If the Component for this widget has been removed then show
// the widget's error message.
if (this.getHandle() === value.getHandle() &&
typeof widget.showError === "function") {
var navOrdStr = this.getNavOrd().toString();
baja.lex({
module: "bajaux",
ok: function (lex) {
widget.showError(lex.get("widget.error.title"), lex.get({
key: "widget.error.componentRemoved",
args: [ navOrdStr ]
}));
}
});
}
};
baja.nav.attach("unmount", widget.$subNavFunc);
}
////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////
function isBajaComponent(value) {
return baja.hasType(value, 'baja:Component');
}
function removeFromArray(arr, obj) {
var i;
if (arr) {
for (i = 0; i < arr.length; i++) {
if (arr[i] === obj) {
return arr.splice(i, 1);
}
}
}
}
function addMixin(widget, mixin) {
var mixins = widget.$mixins,
i = mixins.indexOf(mixin);
if (i === -1) {
mixins.push(mixin);
}
}
////////////////////////////////////////////////////////////////
// Subscriber Mix-In
////////////////////////////////////////////////////////////////
/**
* @alias module:bajaux/mixin/subscriberMixIn
*
* @param {module:bajaux/Widget} target target widget to have the Subscriber mixin
* applied to it.
* @param {Object} [params]
* @param {Boolean} [params.autoSubscribe=true] By default, any component
* loaded into the widget will be automatically subscribed. Set this to false
* to skip the subscription; manually subscribe when necessary by accessing
* `this.getSubscriber()`. Unsubscription for cleanup purposes will still
* be performed.
*/
var exports = function exports(target, params) {
if (!(target instanceof Widget)) {
throw new Error("Subscriber MixIn only applies to instances or sub-classes of Widget");
}
var superLoad = target.load,
superDestroy = target.destroy,
superEnabled = target.setEnabled,
superResolve = target.resolve,
autoSubscribe = !params || params.autoSubscribe !== false;
addMixin(target, 'subscriber');
addMixin(target, 'batchLoad');
/**
* Overrides resolve and injects a Subscriber into the ORD
* resolution so it's subscribed.
*
* @memberOf module:bajaux/mixin/subscriberMixIn
* @param data Specifies some data used to resolve a load value
* so `load(value)` can be called on the widget.
* @param {Object} [resolveParams] An Object Literal used for ORD
* resolution. This parameter is designed to be used internally by bajaux
* and shouldn't be used by developers.
* @returns {Promise}
*/
target.resolve = function resolve(data, resolveParams) {
var that = this;
resolveParams = resolveParams || {};
if (that.isEnabled() && autoSubscribe) {
resolveParams.subscriber = that.getSubscriber();
}
return superResolve.apply(that, [ data, resolveParams ]);
};
/**
* Override the default widget load method. Loads a value into the widget.
* If the value is a Component, then it will be subscribed (unless
* `autoSubscribe` is false).
*
* This function supports the contract defined in `batchLoadMixin`.
*
* @see module:bajaux/Widget#load
* @see module:bajaux/mixin/batchLoadMixin
*
* @memberOf module:bajaux/mixin/subscriberMixIn
* @param {*} value The value for the Widget to load.
* @param {Object} [params]
* @param {baja.comm.Batch} [params.batch] component subscription will
* use this batch, if provided
* @param {Function} [params.progressCallback] a function to be called when
* subscription progress occurs
* @returns {Promise} A promise that's resolved once the value has been
* fully loaded.
*/
target.load = function load(value, params) {
var that = this,
args = arguments,
oldValue = that.$value,
progressCallback = params && params.progressCallback;
that.$loading = true;
that.$value = value;
function resolve() {
return value;
}
function callSuper() {
that.$value = oldValue;
return superLoad.apply(that, args);
}
if (!that.isInitialized()) {
return callSuper().then(resolve);
}
function unsubscribeOldValue() {
//if the widget was disabled while it had the old value loaded, then
//the old value will be cached for re-subscription. uncache the old
//value so that it won't be re-subscribed when we re-enable the widget.
removeFromArray(that.$subComps, oldValue);
if (isBajaComponent(oldValue) &&
oldValue !== value &&
that.getSubscriber().isSubscribed(oldValue)) {
return that.getSubscriber().unsubscribe(oldValue);
} else {
return Promise.resolve();
}
}
function subscribeNewValue() {
var promise;
// Start subscription process unless the Widget doesn't want automatic subscription.
if (that.isEnabled() &&
isBajaComponent(value) &&
value.isMounted() &&
autoSubscribe) {
promise = that.getSubscriber().subscribe({
comps: value,
batch: params && params.batch ? params.batch : undefined
});
}
if (progressCallback) {
progressCallback("commitReady");
}
return promise;
}
function fail(err) {
that.$value = oldValue;
that.$loading = false;
err = err || unknownErr;
// We don't want to fire this event twice.
that.trigger(events.LOAD_FAIL_EVENT, err);
throw err;
}
if (isBajaComponent(oldValue)) {
detachNavEventListener(that);
}
if (isBajaComponent(value)) {
attachNavEventListener(that, value);
}
return unsubscribeOldValue()
.then(subscribeNewValue)
.then(callSuper)
.then(resolve, fail);
};
/**
* Overrides the default widget setEnabled method. If a widget is enabled
* then a subscription for the value is started (unless `autoSubscribe`
* returns false). If the value is unsubscribed then an unsubscription for
* the value is attempted.
*
* @see module:bajaux/Widget#setEnabled
*
* @memberOf module:bajaux/mixin/subscriberMixIn
* @param {Boolean} enabled
* @returns {Promise} A promise resolved once the widget is enabled and the
* loaded component is subscribed
*/
target.setEnabled = function setEnabled(enabled) {
var that = this,
args = arguments,
val = that.value(),
sub = that.getSubscriber(),
comps;
enabled = !!enabled;
that.$enabled = enabled;
function resolve() {
return enabled;
}
function callSuper() {
return superEnabled.apply(that, args);
}
if (enabled) {
comps = that.$subComps || [];
// Handle when a Component may have been loaded while the
// widget was disabled.
if (isBajaComponent(val) && val.isMounted() && $.inArray(val, comps) === -1) {
comps.push(val);
}
} else {
comps = that.$subComps = sub.getComponents() || [];
}
if (comps.length === 0) {
return callSuper().then(resolve);
}
function handleSubscription() {
//if unsubscribing, just kick it off but don't wait for it to finish.
return Promise.resolve(enabled ?
sub.subscribe(comps) :
(sub.unsubscribe(comps) || null));
}
return handleSubscription()
.catch(function (err) {
err = err || unknownErr;
that.trigger(enabled ? events.ENABLE_FAIL_EVENT : events.DISABLE_FAIL_EVENT, err);
throw err;
})
.then(callSuper)
.then(resolve);
};
/**
* Overrides the default widget destroy method. This method ensures
* all handlers are removed by the Subscriber when the widget is destroyed.
*
* @see module:bajaux/Widget#destroy
* @memberOf module:bajaux/mixin/subscriberMixIn
* @returns {Promise} A promise resolved once everything has been destroyed.
*/
target.destroy = function destroy() {
var that = this;
detachNavEventListener(that);
// Detach all event handlers from the Subscriber.
that.getSubscriber().detach();
// Remove any references to Components that are cached.
delete that.$subComps;
// Call the super destroy to delete everything else.
return superDestroy.apply(that, arguments)
.then(function () {
that.getSubscriber().unsubscribeAll().catch(baja.error);
return null;
});
};
/**
* Returns this widget's subscriber.
*
* @memberOf module:bajaux/mixin/subscriberMixIn
* @returns {baja.Subscriber} The widget's subscriber.
*/
target.getSubscriber = target.getSubscriber || function getSubscriber() {
var that = this;
if (!that.$subscriber) {
that.$subscriber = new baja.Subscriber();
}
return that.$subscriber;
};
};
return exports;
});