/**
* @copyright 2020 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/**
* API Status: **Development**
* @module nmodule/bajaui/rc/baja/binding/Binding
*/
define([
'baja!',
'log!nmodule.bajaui.rc.baja.binding.Binding',
'Promise',
'nmodule/bajaui/rc/binding/impl/widgetEvents' ], function (
baja,
log,
Promise,
widgetEvents) {
'use strict';
const logWarning = log.warning.bind(log);
const SETTLED_ORD_SYMBOL = Symbol('settledOrd');
/**
* BajaScript representation of a `bajaui:Binding`. An instance of this class
* knows how to propagate data values from an `OrdTarget` to a `Widget`.
*
* @abstract
* @class
* @alias module:nmodule/bajaui/rc/baja/binding/Binding
* @extends baja.Component
* @implements module:bajaux/model/binding/IBinding
* @implements module:nmodule/bajaui/rc/binding/IValueProvider
* @implements module:nmodule/bajaui/rc/binding/IWidgetEventListener
*/
return class Binding extends baja.Component {
/**
* @returns {module:baja/ord/OrdTarget|undefined} the OrdTarget this Binding is bound to, or
* undefined if unbound
*/
getOrdTarget() {
return this.$ordTarget;
}
/**
* Only to be called by the UxMedia framework itself.
*
* @private
* @param {module:baja/ord/OrdTarget|undefined} ordTarget the OrdTarget this Binding is bound to
*/
setOrdTarget(ordTarget) {
this.$ordTarget = ordTarget;
}
/**
* Given a certain property name, typically a Widget property, provide the
* corresponding value for that name. This is roughly analogous to
* `BBinding#getOnWidget`.
*
* @param {string} name the name of the property to retrieve a value for
* @param {object} cx user context
* @returns {*|null|Promise.<*|null>} by default, returns `null` which
* indicates there is no value to be provided by this name. Override in
* subclasses.
*/
provide(name, cx) {
return null;
}
/**
* Every Binding belongs to a BindingList which contains all the other
* Bindings that are also bound to its target.
*
* @returns {module:bajaux/model/binding/BindingList}
*/
getBindingList() {
return this.$bindingList;
}
/**
* This is a callback that will be run whenever the Binding is updated to
* point to a new OrdTarget. This may happen when the binding's ORD changes,
* or when it is re-resolved.
*
* @returns {Promise}
*/
targetChanged() {
return null;
}
/**
* @returns {module:bajaux/Widget} the Widget this Binding is bound to
*/
getWidget() {
return this.$widget;
}
/**
* @param {module:bajaux/Widget} widget the Widget this Binding is bound to
*/
setWidget(widget) {
this.$widget = widget;
}
/**
* @private
* @param {module:bajaux/Widget} widget
* @since Niagara 4.15
*/
$init(widget) {
this.setWidget(widget);
this[SETTLED_ORD_SYMBOL] = this.get('ord');
}
/**
* When the Binding is bound to a Widget, it can start listening for events
* from that Widget. Override this method as needed.
*
* You may find it helpful to call `addWidgetEvents` from your override of this
* method.
*
* @param {module:bajaux/Widget} widget
* @see module:nmodule/bajaui/rc/binding/impl/widgetEvents
*/
addListeners(widget) {}
/**
* Registers the provided events on the widget and keeps track of these
* events which are disarmed with the default implementation of
* Binding#removeListeners.
*
* The added listeners will fire in the order they were added to the widget.
* The handler for a listener may optionally return false which will cause
* the later event handlers to no longer fire. The provided handler for an
* event also executes in order, asynchronously via promises. This means
* it will wait for the async operation to finish prior to moving on to the
* next event of the same type. Valid event types include both JQuery and
* bajaux events.
*
* Note that this is a _convenience_ method and overriding this will not gain
* your Binding subclass any functionality.
*
* @example
* binding.addWidgetEvents(widget, {
* click: () => {
* // do something when click happens
* }
* });
*
* @example
* binding.addWidgetEvents(widget, {
* click: () => {
* // This promise will resolve prior to executing the second click event
* return this.fetchDataAsync()
* .then(() => {
* // do something with the fetched data and continue to next click
* });
* }
* });
*
* @example
* binding.addWidgetEvents(widget, {
* loaded: () => {
* return false; // prevent firing events that were armed later.
* }
* });
*
* @param {module:bajaux/Widget} widget
* @param {object} events
*/
addWidgetEvents(widget, events) {
this.$widgetEventDisarms = this.$widgetEventDisarms || [];
this.$widgetEventDisarms.push(widgetEvents(widget, events).disarm);
}
/**
* Any event handlers that would not automatically be cleaned up by
* `widget.destroy()` can be explicitly cleaned up here.
*
* By default, this will clean up any events that were added via
* `addWidgetEvents()`.
*
* @param {module:bajaux/Widget} widget
*/
removeListeners(widget) {
if (this.$widgetEventDisarms) {
this.$widgetEventDisarms.forEach((disarm) => disarm());
this.$widgetEventDisarms = [];
}
}
/**
* Hide or disable the widget based on whether this binding is currently
* degraded.
* @returns {Promise}
*/
applyDegradeBehavior() {
const widget = this.$widget;
const degraded = this.isDegraded();
switch (this.get('degradeBehavior').getTag()) {
case 'disable':
return widget.setEnabled(!degraded);
case 'hide':
widget.properties().add('visible', !degraded);
return Promise.resolve();
default:
return Promise.resolve();
}
}
/**
* @returns {boolean} if the binding is successfully bound to an
* `OrdTarget`.
*/
isBound() {
return !!this.$ordTarget;
}
/**
* @returns {boolean} if the binding is unusable for any reason. The default
* implementation returns `!this.isBound()`.
*/
isDegraded() {
return !this.isBound();
}
/**
* Callback that will be called when the Px page containing this Binding is
* saved. Each Binding will have a chance to save any user-entered data (it
* is the Binding's responsibility to check if the user has made any changes
* or not).
*
* @returns {*|Promise} to be resolved when the binding has saved any
* user-entered data
*/
save() {}
/**
* Fires an event to signal to the framework that the binding is now bound to
* a different target ORD. The framework should resolve the Binding's new
* ORD, set a brand-new OrdTarget, and refresh its bound Widget so the UI is
* brought up to date.
*
* Should not be overridden without calling `super()`.
*/
requestRebind() {
this.fireHandlers('rebind', logWarning, this);
}
/**
* Fires an event to signal that the Binding has new values to provide. The
* framework should propagate all the Binding's properties to its bound
* Widget so the UI is brought up to date.
*
* Should not be overridden without calling `super()`.
*/
requestRefresh() {
this.fireHandlers('refresh', logWarning, this);
}
/**
* Fires an event to signal that this Binding, and all of its peers, should be unbound.
*
* @private
* @since Niagara 4.15
*/
$requestUnbindAll() {
this.fireHandlers('unbindAll', logWarning, this);
}
/**
* Helper method to return a string representation of the binding of the form
* <moduleName>:<bindingTypeDisplayName>[boundOrd]
*
* @private
* @returns {String} Returns the String representation of the binding's ord.
*/
$toDisplayString() {
const type = this.getType().getModuleName() + ':' + this.getTypeDisplayName();
const ord = String(this.getOrd());
return `${ type } [${ ord }]`;
}
/**
* @private
* @param {baja.Ord} baseOrd
* @param {baja.Ord} ord
* @returns {baja.Ord}
*/
$relativizeToBase(baseOrd, ord) {
let relativizedOrd;
if (!baseOrd.isNull()) {
relativizedOrd = ord.isNull() ? ord : baja.Ord.make({ base: baseOrd, child: ord });
} else {
relativizedOrd = ord;
}
try {
relativizedOrd = relativizedOrd.normalize();
} catch (ignore) {}
return relativizedOrd;
}
/**
* @private
* @returns {baja.Ord} the actual ORD to resolve against the station - may be different from the
* value of the `ord` slot if optimized.
* @since Niagara 4.15
*/
$getSettledOrd() {
return this[SETTLED_ORD_SYMBOL] || this.get('ord');
}
/**
* Get the ord associated with this Binding, resolve it, and update the binding's OrdTarget.
*
* @private
* @param {object} params
* @param {function(baja.Ord)} params.resolveOrd use this to resolve an ORD to an OrdTarget,
* optimized for best performance in the UxMedia page this Binding is used in
* @returns {Promise} to be resolved after the work is done and the OrdTarget is updated. Will
* reject if resolution fails, or no base ORD set.
* @since Niagara 4.15
*/
$resolve({ baseOrd, resolveOrd }) {
let ord = this.$getSettledOrd();
return Promise.resolve()
.then(() => this.$doResolve(ord, { baseOrd, resolveOrd }))
.then((ordTarget) => {
if (!ordTarget) {
return;
}
const optimizedOrd = ordTarget.getFacets().get('optimizedOrd');
if (optimizedOrd) {
this[SETTLED_ORD_SYMBOL] = optimizedOrd;
}
this.setOrdTarget(ordTarget);
})
.catch(() => {
const ordStr = ord.toString();
let msg = 'unresolved: ' + ordStr;
if (ordStr.startsWith('sys:|') || ordStr.indexOf('|sys:|') >= 0) {
// Provide a better clue of why an ORD containing a query to the
// SystemDb might not resolve
msg = msg + " (ensure the SystemDb is available and/or the base is indexed into the SystemDb)";
}
throw new Error(msg);
});
}
/**
* Private, but intended for internal subclasses to override. Lets the binding do the work of
* "resolving my ord" - if the subclass does something special other than just
* this.get('ord').resolve(), it can implement that here.
*
* @private
* @param {baja.Ord} ord the ord to resolve, _not_ relativized against the base ORD yet
* @param {object} params
* @param {baja.Ord} params.baseOrd
* @param {function(baja.Ord)} params.resolveOrd use this to resolve an ORD to an OrdTarget,
* optimized for best performance in the UxMedia page this Binding is used in
* @returns {Promise.<module:baja/ord/OrdTarget|*>} resolves the given ORD to an OrdTarget, or
* falsy if resolution was intentionally not performed (e.g. ord is null)
* @since Niagara 4.15
*/
$doResolve(ord, { baseOrd, resolveOrd }) {
if (ord.isNull()) {
return Promise.resolve(null);
}
return resolveOrd(this.$relativizeToBase(baseOrd, ord));
}
};
});