js/base/binding/binding_stream.js
// NPM IMPORTS
import assert from 'assert'
import { format } from 'util'
// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'
import uid from 'devapt-core-common/dist/js/utils/uid.js'
import { transform } from 'devapt-core-common/dist/js/utils/transform'
const context = 'browser/base/binding/binding_stream'
/**
* @file UI component binding class.
*
* @author Luc BORIES
*
* @license Apache-2.0
*/
export default class BindingStream
{
/**
* Creates an instance of Binding for stream.
*
* @param {string} arg_id - binding identifier.
* @param {RuntimeBase} arg_runtime - client runtime.
* @param {Component} arg_component - component instance.
*
* @returns {nothing}
*/
constructor(arg_id, arg_runtime, arg_component)
{
this.is_binding_stream = true
this._id = arg_id ? arg_id : 'binding_' + uid()
this._runtime = arg_runtime
this._component = arg_component
this._unsubscribe = undefined
this._unsubscribe_state_update = undefined
this._stream = undefined
this._state_path = undefined
this._starting_value = undefined
this._source_svc_name = undefined
this._source_svc_method = undefined
this._source_xform = undefined
this._source_timeline = undefined
this._targets = undefined
this._target_method = undefined
this._options = undefined
}
set_stream(arg_stream)
{
this._stream = arg_stream
return this
}
set_state_path(arg_state_path)
{
this._state_path = arg_state_path
return this
}
set_starting_value(arg_starting_value)
{
this._starting_value = arg_starting_value
return this
}
set_source_service_name(arg_source_svc_name)
{
this._source_svc_name = arg_source_svc_name
return this
}
set_source_service_method(arg_source_svc_method)
{
this._source_svc_method = arg_source_svc_method
return this
}
set_source_transformation(arg_transformation)
{
this._source_xform = arg_transformation
return this
}
set_source_timeline_name(arg_source_timeline)
{
this._source_timeline = arg_source_timeline
return this
}
set_targets_instances_array(arg_targets)
{
this._targets = arg_targets
return this
}
set_target_method_name(arg_target_method)
{
this._target_method = arg_target_method
return this
}
set_options(arg_options)
{
this._options = arg_options
return this
}
/**
* Build binding.
*
* @returns {Promise}
*/
build()
{
console.info(context + ':build:loading binding for component ' + this._component.get_name(), this._target_method, this._stream)
this._component.enter_group('build')
// CHECK ATTRIBUTES
if ( T.isArray(this._stream) )
{
this._stream.forEach(
(stream, index)=>{
assert( T.isObject(stream) && stream.is_stream, context + format(':build:component=%s:bad stream object at index=%s', this._component.get_name(), index) )
}
)
} else {
assert( T.isObject(this._stream) && this._stream.is_stream, context + format(':build:component=%s:bad stream object', this._component.get_name()) )
}
assert( T.isArray(this._targets) && this._targets.length > 0, context + format(':build:component=%s,timeline=%s:bad targets', this._component.get_name(), this._source_timeline, this._source_svc_method) )
assert( T.isString(this._target_method) || T.isNotEmptyArray(this._target_method), context + format(':build:component=%s,timeline=%s:bad target method=%s', this._component.get_name(), this._source_timeline, this._target_method) )
const method_cfg = T.isObject(this._options) ? this._options.method : undefined
const operands = T.isObject(method_cfg) ? method_cfg.operands : undefined
const format_cfg = T.isObject(this._options) ? this._options.format : undefined
const unsubscribes = []
this._targets.forEach(
(target, index)=>{
const stream = T.isArray(this._stream) ? (this._stream.length > index ? this._stream[index] : this._stream[this._stream.length - 1]) : this._stream
let unbind = undefined
if ( T.isString(this._target_method) )
{
// console.info(context + ':build:loading binding for component ' + this._component.get_name() + ' with method=' + this._target_method)
unbind = this.bind_stream(stream, this._source_xform, target, this._target_method, operands, format_cfg, this._starting_value)
unsubscribes.push(unbind)
}
else if ( T.isNotEmptyArray(this._target_method) )
{
this._target_method.forEach(
(method_name)=>{
if ( T.isString(method_name) )
{
// console.info(context + ':build:loading binding for component ' + this._component.get_name() + ' with methods=' + method_name)
unbind = this.bind_stream(stream, this._source_xform, target, method_name, operands, format_cfg, this._starting_value)
unsubscribes.push(unbind)
}
}
)
}
}
)
this._unsubscribe = ()=>{
unsubscribes.forEach(
(unsubscribe)=>{
unsubscribe()
}
)
}
if ( T.isArray(this._state_path) && this._state_path.length > 0 )
{
const stream = T.isArray(this._stream) ? this._stream[0] : this._stream
if (! T.isObject(stream) || ! stream.is_stream)
{
console.warn(context + ':build:bad state path stream for component %s', this._component.get_name(), this._stream, stream)
return
}
this._unsubscribe_state_update = this.bind_stream(stream, this._source_xform, this._component, 'dispatch_update_state_value_action', [this._state_path])
}
this._component.leave_group('build:async')
return Promise.resolve()
}
/**
* Bind a stream to a target object action.
*
* @param {object} arg_stream - stream instance.
* @param {string|array|function} arg_values_xform - values transformation (optional).
* @param {object} arg_bound_object - target object instance (optional: this as default).
* @param {string} arg_bound_method - target object method string.
* @param {anything} arg_method_operands - target object method operands (optional).
* @param {object} arg_format_object - formatting settings.
* @param {any} arg_starting_value - starting value.
* @param {object} arg_options - binding options.
*
* @returns {function} - unsubscribe function.
*/
bind_stream(arg_stream, arg_values_xform, arg_bound_object, arg_bound_method, arg_method_operands, arg_format_object, arg_starting_value, arg_options)
{
// console.info(context + ':bind_stream:loading binding for component ' + this._component.get_name(), arg_bound_method, arg_method_operands, this._stream, arg_bound_object, this._source_timeline)
let unbind_cb = undefined
// SET TARGET OBJECT
arg_bound_object = arg_bound_object ? arg_bound_object : this._component
if ( T.isString(arg_bound_object) )
{
if ( T.isUndefined(arg_bound_method) )
{
arg_bound_method = arg_bound_object
arg_bound_object = this._component
} else if (arg_bound_object == 'this')
{
arg_bound_object = this._component
}
else
{
console.error(context + ':bind_stream:bad bound object string:%s', arg_bound_object)
return undefined
}
}
// TODO: DO NOT USE TYPR TO TEST DOM ELEMENTS
// TYPR USE toString FUNCTION TO TEST TYPE against [object Object].
// FOR OBJECT, toString give [object Object]
// BUT DOM ELEMENTS toString GIVE [object HTMLXXXElement]
assert( T.isObject(arg_stream) && arg_stream.is_stream, context + ':bind_stream:bad stream object')
assert( T.isObject(arg_bound_object) || typeof arg_bound_object == 'object', context + ':bind_stream:bad bound object')
assert( T.isString(arg_bound_method), context + ':bind_stream:bad bound method string')
// CHECK OBJECT METHOD
if ( ! (arg_bound_method in arg_bound_object) )
{
console.error(context + ':bind_stream:bad method [%s] for bound object=', arg_bound_method, arg_bound_object)
return undefined
}
// DEBUG
arg_stream.get_transformed_stream().onValue(
(values) => {
console.log(context + ':bind_stream:on initial stream:%s:bound method=%s, bound object, values', this._component.get_name(), arg_bound_method, arg_bound_object, values)
}
)
// SET FORMATTING FUNCTION
let formatting_xform = undefined
if ( T.isObject(arg_format_object) && T.isString(arg_format_object.type) )
{
formatting_xform = this.get_format_value_function(arg_format_object)
}
// SET XFORM FOR SIMPLE SETTINGS
if ( T.isString(arg_values_xform) )
{
const field_name = arg_values_xform
arg_values_xform = {
"result_type":"single",
"fields":[
{
"name":field_name,
"path":field_name
}
]
}
}
if ( T.isUndefined(arg_values_xform) || arg_values_xform == null )
{
let xform_stream = T.isFunction(formatting_xform) ? arg_stream.get_transformed_stream().map(formatting_xform) : arg_stream.get_transformed_stream()
if ( ! T.isUndefined(arg_starting_value) )
{
xform_stream = xform_stream.startWith(arg_starting_value)
}
if (arg_method_operands)
{
if ( T.isArray(arg_method_operands) )
{
// console.log(context + ':bind_stream:set a stream handler for method=%s with array operands=', arg_bound_method, arg_method_operands)
unbind_cb = xform_stream.toProperty().assign(arg_bound_object, arg_bound_method, ...arg_method_operands)
}
else
{
// console.log(context + ':bind_stream:set a stream handler for method=%s with not array operands=', arg_bound_method, arg_method_operands)
unbind_cb = xform_stream.toProperty().assign(arg_bound_object, arg_bound_method, arg_method_operands)
}
}
else
{
unbind_cb = xform_stream.toProperty().assign(arg_bound_object, arg_bound_method)
}
}
else if ( T.isFunction(arg_values_xform) )
{
let xform_stream = arg_stream.get_transformed_stream().map(arg_values_xform)
xform_stream = T.isFunction(formatting_xform) ? xform_stream.map(formatting_xform) : xform_stream
if ( ! T.isUndefined(arg_starting_value) )
{
xform_stream = xform_stream.startWith(arg_starting_value)
}
if (arg_method_operands)
{
unbind_cb = xform_stream.toProperty().assign(arg_bound_object, arg_bound_method, ...arg_method_operands)
}
else
{
unbind_cb = xform_stream.map(arg_values_xform).toProperty().assign(arg_bound_object, arg_bound_method)
}
}
else if ( T.isObject(arg_values_xform) )
{
const datas_xform = (arg_stream_record) => {
if (arg_stream_record.datas)
{
return arg_stream_record.datas
}
return arg_stream_record
}
let xform_stream = arg_stream.get_transformed_stream().map(datas_xform).map( transform(arg_values_xform) )
// DEBUG
// xform_stream.map(datas_xform).map( transform(arg_values_xform) ).onValue(
// (values) => {
// console.log(context + ':bind_stream:%s:on values_xform stream:bound method=%s, bound object, values', name, arg_bound_method, arg_bound_object, values)
// }
// )
xform_stream = T.isFunction(formatting_xform) ? xform_stream.map(formatting_xform) : xform_stream
if ( ! T.isUndefined(arg_starting_value) )
{
xform_stream = xform_stream.startWith(arg_starting_value)
}
if (arg_options && T.isBoolean(arg_options.dispatch) && arg_options.dispatch && T.isNumber(arg_options.index) && arg_options.index > 0 )
{
const at_index_xform = (arg_stream_record) => {
if ( T.isArray(arg_stream_record) && arg_stream_record.length > arg_options.index )
{
return arg_stream_record[arg_options.index]
}
return undefined
}
xform_stream = xform_stream.map(at_index_xform)
}
// DEBUG
// xform_stream.onValue(
// (values) => {
// console.log(context + ':bind_stream:on transformed stream:%s:bound method=%s, bound object, values', name, arg_bound_method, arg_bound_object, values)
// }
// )
if (this._runtime.shoud_log_bindingd_stream())
{
const target = arg_bound_object && arg_bound_object.get_name ? arg_bound_object.get_name() : (arg_bound_object && arg_bound_object.attr ? arg_bound_object.attr('id') : (arg_bound_object && arg_bound_object.getAtttribute ? arg_bound_object.getAttribute('id') : 'unknow'))
xform_stream.onValue(
(values) => {
const datas = T.isArray(values) ? 'array of length ' + values.length : (T.isObject(values) ? 'object of keys ' + Object.keys(values).toString() : 'datas')
this._component.debug('bind_stream:on transformed stream:%s:target=%s,method=%s:values', name, target, arg_bound_method, datas)
}
)
}
if (arg_method_operands)
{
if ( T.isArray(arg_method_operands) )
{
unbind_cb = xform_stream.onValue(arg_bound_object, arg_bound_method, ...arg_method_operands)
} else {
unbind_cb = xform_stream.onValue(arg_bound_object, arg_bound_method, arg_method_operands)
}
}
else
{
unbind_cb = xform_stream.onValue(arg_bound_object, arg_bound_method)
}
}
else
{
assert(false, context + ':bind_svc:bad values paths string|array|function')
}
return unbind_cb
}
/**
* Get a formatting function or identity.
* Format settings example:
* {
* "type":"number",
* "digits":2
* }
*
* @param {object} arg_format_object - format settings.
*
* @returns {Function} - Formatting function or identity.
*/
get_format_value_function(arg_format_object)
{
if (! T.isObject(arg_format_object) || ! T.isString(arg_format_object.type) )
{
return (x) => x
}
switch(arg_format_object.type.toLocaleLowerCase()) {
case 'number': {
if ( T.isNumber(arg_format_object.digits) )
{
return (x) => {
if (T.isNumber(x))
{
return x.toFixed(arg_format_object.digits)
}
return undefined
}
}
return (x) => (T.isNumber(x) ? x : undefined)
}
}
return (x) => x
}
}