/**
* @copyright 2020 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/* eslint-env browser */
/**
* API Status: **Development**
* @module nmodule/bajaui/rc/model/UxModel
*/
define([
'bajaux/Widget',
'Promise',
'underscore',
'nmodule/bajaui/rc/model/BindingList',
'nmodule/bajaui/rc/model/jsxToUxModel' ], function (
Widget,
Promise,
_,
BindingList,
jsxToUxModel) {
'use strict';
const { first, map, rest } = _;
const { jsx } = jsxToUxModel;
/**
* we use this to cache the default properties
* @type {symbol}
*/
const CTOR_DEFAULT_PROPS_SYMBOL = Symbol('constructorProperties');
const CTOR_SYMBOL = Symbol('constructor');
/**
* Represents all information needed to create one widget, and its children,
* in a Ux Media graphic.
*
* Note that this is not `spandrel` data. This is an intermediate, abstracted
* data model that is a representation of a tree of widgets and bindings that
* would make up a graphic or portion of a graphic. (In other words, a `.px`
* file would translate readily into a `UxModel`.) But `UxModel` is also
* intended to provide usable `spandrel` data to be used at rendering time.
*
* @todo this should eventually move to uxBuilder, but keeping it here for ease of development in conjunction with bajaui
* @class
* @alias module:nmodule/bajaui/rc/model/UxModel
*/
class UxModel {
/**
* Don't call directly - use `make()` instead.
* @private
*/
constructor(obj = {}) {
this.$obj = obj;
obj.kids = processKids(obj.kids);
this.$bindingList = new BindingList(obj.bindings);
}
/**
* @param {object} [obj]
* @param {string} [obj.name] the name of the widget represented by this
* `UxModel`. This will be automatically set on child nodes; a parent-less
* root widget may have no name or a name arbitrarily chosen.
* @param {Function} [obj.type] the Type of the widget to create
* @param {object} [obj.properties] an object literal of the widget's
* properties
* @param {boolean} [obj.readonly] true if the widget should be readonly
* @param {string} [obj.formFactor] the form factor this widget should be constructed with, if known
* @param {Array.<object|module:nmodule/bajaui/rc/model/UxModel>} [obj.kids] objects
* describing the widget's children
* @param {Array.<module:nmodule/bajaui/rc/binding/IValueProvider>} [obj.bindings] bindings
* to propagate data updates to the widget (these will be assigned to a
* `BindingList`)
* @param {*} [obj.value] can be specified if loading a value
* @returns {Promise.<module:nmodule/bajaui/rc/model/UxModel>}
*/
static make(obj) {
if (obj instanceof UxModel) {
obj = obj.$obj;
}
// TODO: resolve baja types in properties/bindings
return Promise.resolve(new UxModel(obj));
}
/**
* @private
* @see module:nmodule/bajaui/rc/model/jsxToUxModel
*/
static jsx(type, props, ...kids) {
return jsx(type, props || {}, kids);
}
/**
* @returns {string}
*/
getName() {
return this.$obj.name;
}
/**
* @param {string|string[]} path
* @returns {module:nmodule/bajaui/rc/model/UxModel|module:nmodule/bajaui/rc/baja/binding/Binding|undefined} the UxModel
* kid by the given name. If an array of names are given, will follow the
* path down through the UxModel structure.
*/
get(path) {
if (!Array.isArray(path)) { path = [ path ]; }
return byName(this, path);
}
/**
* @returns {Function} constructor for the widget to create
*/
getType() {
return this.$obj.type;
}
/**
* @returns {module:nmodule/bajaui/rc/model/BindingList}
*/
getBindingList() {
return this.$bindingList;
}
/**
* @returns {Array.<module:nmodule/bajaui/rc/model/UxModel>} UxModel
* instances for this widget's children
*/
getKids() {
const { kids = [] } = this.$obj;
return kids.slice();
}
/**
* @returns {object} object literal describing this widget's properties
*/
getProperties() {
const obj = this.$obj;
let properties = obj.properties || {};
if (!this.$widgetPropertiesApplied) {
const Ctor = this.getType();
if (Ctor) {
let ctorProperties = Ctor[CTOR_DEFAULT_PROPS_SYMBOL];
if (!ctorProperties || ctorProperties[CTOR_SYMBOL] !== Ctor) {
ctorProperties = Ctor[CTOR_DEFAULT_PROPS_SYMBOL] = getDefaultProperties(Ctor);
ctorProperties[CTOR_SYMBOL] = Ctor;
}
const newProps = Object.assign({}, ctorProperties, properties);
properties = obj.properties = newProps;
}
this.$widgetPropertiesApplied = true;
}
return properties;
}
/**
* @returns {*|null} the value to be loaded into this widget
*/
getValue() {
return this.$obj.value;
}
/**
* @since Niagara 4.14
* @returns {boolean} true if this widget should be readonly
*/
isReadonly() {
return !!this.$obj.readonly;
}
/**
* @since Niagara 4.14
* @returns {string|undefined} the form factor this widget should be constructed with, if known
*/
getFormFactor() {
return this.$obj.formFactor;
}
/**
* Produce a `spandrel` config object that represents this Ux element as
* rendered in the DOM. The `value` property will always be `this`, as the
* `UxModel` will be loaded into the `spandrel` widget as the value.
*
* Remember that the `spandrel` data will contain any bindings present in
* the model as well! Beware of simply passing back `toSpandrel()` results
* from the `UxModel` passed to your render function - you may get duplicate
* bindings. `toSpandrel()` is typically more appropriate for calling on
* kids.
*
* @param {object|string|Function} params parameters used for generating the
* `spandrel` data; can also be `dom` passed directly as a string or
* function
* @param {string|Function} params.dom the DOM element into which to render
* this element. Can be a function that receives an object with
* `properties`, which are the properties of this Ux element, to be used to
* generate the DOM
* @param {Array.<object>|Function} [params.kids] You can specify the `kids`
* property of the `spandrel` config directly. Alternately, this can be a
* function that receives each `UxModel` in `getKids()`, and returns
* `kid.toSpandrel()` or a `spandrel` object of your choosing.
* @returns {object} an object fit to be passed as a `spandrel` argument
*/
toSpandrel(params = {}) {
if (isDom(params) || typeof params === 'function') {
params = { dom: params };
}
let { dom, kids } = params;
const properties = this.getProperties();
if (typeof dom === 'function') {
dom = dom({ properties });
}
if (typeof kids === 'function') {
kids = this.getKids().map(kids);
}
const value = this.getValue();
return {
dom,
enabled: properties.enabled !== false,
kids,
properties,
readonly: this.isReadonly(),
formFactor: this.getFormFactor(),
type: this.getType(),
value: value === undefined ? this : value,
data: { bindingList: this.getBindingList() }
};
}
}
function processKids(kids) {
if (!kids) { return []; }
return map(kids, (kid, name) => {
if (!(kid instanceof UxModel)) {
kid = new UxModel(kid);
}
const obj = kid.$obj;
obj.name = obj.name || String(name);
return kid;
});
}
function byName(model, path) {
if (!path.length) { return model; }
const obj = model.$obj;
const name = first(path);
const kids = obj.kids;
let kid;
if (Array.isArray(kids)) {
kid = kids.find((k) => k.getName() === String(name));
} else {
kid = kids[name];
}
if (!kid) {
const binding = model.$bindingList.getBindings().find((b) => b.getName() === name);
return binding || undefined;
}
return byName(kid, rest(path));
}
function isDom(dom) {
return typeof dom === 'string' || dom instanceof HTMLElement;
}
function getDefaultProperties(Ctor) {
const ctorProperties = {};
const widget = new Ctor();
if (widget instanceof Widget) {
// accessing via private variables is bad - but this is a super
// hotspot so must be fast
const arr = widget.$properties.$array;
for (let i = 0, len = arr.length; i < len; ++i) {
const prop = arr[i];
const def = prop.defaultValue;
if (def !== null && def !== undefined) {
ctorProperties[prop.name] = def;
}
}
}
return ctorProperties;
}
return UxModel;
});