Reference Source

js/base/binding/bindings_loader.js

// NPM IMPORTS
import assert from 'assert'
import { format } from 'util'
import _ from 'lodash'

// COMMON IMPORTS
import T      from 'devapt-core-common/dist/js/utils/types'
import Stream from 'devapt-core-common/dist/js/messaging/stream'

// BROWSER IMPORTS
import BindingStream          from './binding_stream'
import BindingServiceTimeline from './binding_service_timeline'
import BindingService         from './binding_service'


const context = 'browser/base/binding/binding_Loader'



/**
 * @file UI component binding loader class.
 * 
 * @author Luc BORIES
 * 
 * @license Apache-2.0
 */
export default class BindingLoader
{
	/**
	 * Creates an instance of BindingLoader.
	 * 
	 * @returns {nothing}
	 */
	constructor()
	{
	}



	/**
	 * Normalize an array of objects selectors in an array of objects.
	 * @static
	 * 
	 * @param {RuntimeBase} arg_runtime - client runtime.
	 * @param {Component} arg_component - component instance.
	 * @param {array} arg_selectors - selectors strings array.
	 * @param {array|string} arg_dom_types - selected items types strings array or single string (default "dom").
	 * 
	 * @returns {array} - objects|strings array.
	 */
	static normalize_objects(arg_runtime, arg_component, arg_selectors, arg_types = 'dom')
	{
		arg_component.enter_group('normalize_objects')

		if ( ! T.isArray(arg_selectors) )
		{
			arg_component.leave_group('normalize_objects:bad selectors array')
			return []
		}

		const objects = []
		arg_selectors.forEach(
			(selector, index)=>{
				if ( ! T.isString(selector) )
				{
					console.warn('normalize_objects:component=%s:bad selector type=%s', arg_component.get_name(), typeof selector)
					return
				}

				// DEBUG
				// console.log(context + ':normalize_objects:component=%s:binding target dom selector=%s', arg_component.get_name(), dom_selector)

				const object_type = T.isString(arg_types) ? arg_types : ( T.isArray(arg_types) && arg_types.length > index ? arg_types[index] : arg_types[arg_types.length - 1])

				if (object_type == 'view')
				{
					if (selector == 'this')
					{
						arg_component.debug('normalize_objects:view:this is found')
						objects.push( arg_component)
						return
					}

					arg_component.debug('normalize_objects:view:' + selector)
					const target_object = arg_runtime._ui.get(selector)
					if (target_object)
					{
						objects.push(target_object)
						return
					}

					console.warn(context + ':normalize_objects:component=%s:bad view selector=%s', arg_component.get_name(), selector)
					return
				}

				if (object_type == 'jquery')
				{
					if (selector == 'this')
					{
						arg_component.debug('normalize_objects:jquery:this is found')
						objects.push( $( arg_component.get_dom_id() ) )
						return
					}
					
					arg_component.debug('normalize_objects:jquery:' + selector)
					const jqo = $(selector)
					if (jqo && jqo.length > 0)
					{
						objects.push(jqo)
						return
					}

					console.warn(context + ':normalize_objects:component=%s:bad jquery selector=%s', arg_component.get_name(), selector, jqo)
					return
				}

				if (object_type == 'dom')
				{
					if (selector == 'this')
					{
						arg_component.debug('normalize_objects:dom:this is found')
						objects.push( arg_component.get_dom_element() )
						return
					}

					arg_component.debug('normalize_objects:dom:' + selector)
					const element = document.getElementById(selector)
					if (element)
					{
						objects.push(element)
						return
					}

					const selection = document.querySelector(selector)
					if (selection)
					{
						objects.push(selection)
						return
					}

					console.warn(context + ':normalize_objects:component=%s:bad dom selector=%s', arg_component.get_name(), selector)
					return
				}

				if (object_type == 'delegate')
				{
					objects.push(selector)
					return
				}

				console.warn(context + ':normalize_objects:component=%s:bad selector type=%s for selector=%s', arg_component.get_name(), object_type, selector)
			}
		)
		
		arg_component.leave_group('normalize_objects')
		return objects
	}
	
	

	/**
	 * Load and apply a component binding configuration.
	 * 
	 * @param {string} arg_id - binding identifier.
	 * @param {RuntimeBase} arg_runtime - client runtime.
	 * @param {Component} arg_component - component instance.
	 * @param {Immutable.Map|undefined} arg_binding_cfg - component binding configuration.
	 * 
	 * @returns {BindingStream|array} 
	 */
	static load(arg_id, arg_runtime, arg_component, arg_binding_cfg)
	{
		// console.info(context + ':load:loading binding for component ' + arg_component.get_name(), arg_binding_cfg)
		
		// CHECK BINDING CONFIGURATION
		if (! T.isObject(arg_binding_cfg) )
		{
			return
		}
		
		// GET CONFIGURATION ATTRIBUTES
		const type = ('type' in arg_binding_cfg) ? arg_binding_cfg['type'] : undefined
		const state_path = ('state_path' in arg_binding_cfg) ? arg_binding_cfg['state_path'] : ['bindings_values', arg_id]
		let xform = ('transform' in arg_binding_cfg) ? arg_binding_cfg['transform'] : undefined
		const options = ('options' in arg_binding_cfg) ? arg_binding_cfg['options'] : undefined
		
		const source_svc_name   = ('service' in arg_binding_cfg) ? arg_binding_cfg['service'] : undefined
		const source_svc_method = ('method' in arg_binding_cfg) ? arg_binding_cfg['method'] : undefined
		const source_timeline   = ('timeline' in arg_binding_cfg) ? arg_binding_cfg['timeline'] : undefined
		const source_stream     = ('source_stream' in arg_binding_cfg) ? arg_binding_cfg['source_stream'] : undefined
		// const source_event      = ('event' in arg_binding_cfg) ? arg_binding_cfg['event'] : undefined
		const source_dom_event = ('dom_event' in arg_binding_cfg) ? arg_binding_cfg['dom_event'] : undefined
		
		// SOURCES
		const source_type = ('source_type' in arg_binding_cfg) ? arg_binding_cfg['source_type'] : undefined
		const source_types = ('source_types' in arg_binding_cfg) ? arg_binding_cfg['source_types'] : (source_type ? [source_type] : undefined)

		const source_selector = ('source_selector' in arg_binding_cfg) ? arg_binding_cfg['source_selector'] : undefined
		const source_selectors = ('source_selectors' in arg_binding_cfg) ? arg_binding_cfg['source_selectors'] : (source_selector ? [source_selector] : undefined)

		// TARGETS
		const target_type = ('target_type' in arg_binding_cfg) ? arg_binding_cfg['target_type'] : undefined
		const target_types = ('target_types' in arg_binding_cfg) ? arg_binding_cfg['target_types'] : (target_type ? [target_type] : undefined)

		const target_selector = ('target_selector' in arg_binding_cfg) ? arg_binding_cfg['target_selector'] : undefined
		const target_selectors = ('target_selectors' in arg_binding_cfg) ? arg_binding_cfg['target_selectors'] : (target_selector ? [target_selector] : undefined)

		// METHOD
		const target_method  = ('target_method' in arg_binding_cfg)  ? arg_binding_cfg['target_method']  : undefined
		const target_methods = ('target_methods' in arg_binding_cfg) ? arg_binding_cfg['target_methods'] : undefined
		
		// NORMALIZE SOURCES
		// console.log(context + ':load:component=%s:binding type=%s:source_selectors&source_types=', arg_component.get_name(), type, source_selectors, source_types)
		const sources = source_selectors ? this.normalize_objects(arg_runtime, arg_component, source_selectors, source_types) : undefined

		// NORMALIZE TARGETS
		// console.log(context + ':load:component=%s:binding type=%s:target_selectors&target_types=', arg_component.get_name(), type, target_selectors, target_types)
		const targets = target_selectors ? this.normalize_objects(arg_runtime, arg_component, target_selectors, target_types) : undefined

		// GET STARTING VALUE
		let starting_value = undefined
		if ( T.isArray(state_path) && state_path.length > 0 )
		{
			starting_value = arg_component.get_state_value(state_path, undefined)
			if (! starting_value)
			{
				starting_value = arg_runtime.get_state_store().get_state().getIn(state_path, undefined)
				if (starting_value && starting_value.toJS)
				{
					starting_value = starting_value.toJS()
				}
			}
		}


		// BIND SOURCES AND TARGETS
		console.log(context + ':load:component=%s:binding type=%s', arg_component.get_name(), type)
		switch(type)
		{
			case 'timeline': {
				assert( T.isString(source_timeline),       context + format(':load:component=%s:bad timeline name=%s',            arg_component.get_name(), source_timeline) )
				assert( T.isString(source_svc_name),       context + format(':load:component=%s:bad service name=%s,timeline=%s', arg_component.get_name(), source_timeline, source_svc_name) )
				assert( T.isString(source_svc_method),     context + format(':load:component=%s:bad service name=%s,timeline=%s', arg_component.get_name(), source_timeline, source_svc_method) )
				assert( T.isArray(targets) && targets.length > 0, context + format(':load:component=%s,timeline=%s:bad targets',  arg_component.get_name(), source_timeline, source_svc_method) )
				assert( T.isString(target_method),  context + format(':load:component=%s,timeline=%s:bad target method=%s',       arg_component.get_name(), source_timeline, target_method) )
				
				return new BindingServiceTimeline(arg_id, arg_runtime, arg_component)
					.set_state_path(state_path)
					.set_starting_value(starting_value)
					.set_source_service_name(source_svc_name)
					.set_source_service_method(source_svc_method)
					.set_source_timeline_name(source_timeline)
					.set_targets_instances_array(targets)
					.set_target_method_name(target_method)
					.set_options(options)
					.build()
			}

			case 'service': {
				assert( T.isString(source_svc_name),       context + format(':load:component=%s:bad service name=%s',  arg_component.get_name(), source_svc_name) )
				assert( T.isString(source_svc_method),     context + format(':load:component=%s:bad method name=%s',   arg_component.get_name(), source_svc_method) )
				assert( T.isArray(targets) && targets.length > 0, context + format(':load:component=%s:bad targets',   arg_component.get_name()) )
				assert( T.isString(target_method),  context + format(':load:component=%s:bad target method=%s',        arg_component.get_name(), target_method) )

				return new BindingService(arg_id, arg_runtime, arg_component)
					.set_state_path(state_path)
					.set_starting_value(starting_value)
					.set_source_service_name(source_svc_name)
					.set_source_service_method(source_svc_method)
					.set_source_transformation(xform)
					.set_targets_instances_array(targets)
					.set_target_method_name(target_method)
					.set_options(options)
					.build()
			}

			case 'emitter_jquery': {
				assert( T.isArray(sources),         context + format(':load:component=%s:bad sources array=%s',       arg_component.get_name()) )
				assert( T.isArray(targets) && targets.length > 0, context + format(':load:component=%s:bad targets',  arg_component.get_name()) )
				assert( T.isString(source_dom_event),  context + format(':load:component=%s:bad source event=%s',     arg_component.get_name(), source_dom_event) )
				assert( T.isString(target_method),  context + format(':load:component=%s:bad target method=%s',       arg_component.get_name(), target_method) )
				
				const streams = []
				sources.forEach(
					(jqo)=>{
						const stream = new Stream( jqo.asEventStream(source_dom_event) )
						streams.push(stream)
					}
				)
				
				console.log(context + ':load:component=%s:binding type=%s:event=%s:sources=', arg_component.get_name(), type, source_dom_event, sources)
				
				if (!xform)
				{
					xform = (event)=>{
						const method_cfg = T.isObject(options) ? options.method : undefined
						const operands   = T.isObject(method_cfg) ? method_cfg.operands : undefined
						
						console.log(context + ':load:component=%s:binding type=%s:event=%s:on handler:data=', arg_component.get_name(), type, source_dom_event, operands)
						
						return {
							is_event_handler:true,
							component_name:undefined,
							event_name:source_dom_event,
							dom_selector:undefined,
							target:event.target,
							data:operands
						}
					}
				}

				return new BindingStream(arg_id, arg_runtime, arg_component)
					.set_stream(streams)
					.set_state_path(state_path)
					.set_source_transformation(xform)
					.set_targets_instances_array(targets)
					.set_target_method_name(target_method)
					.set_options(options)
					.build()
			}

			case 'emitter_dom': {
				assert( T.isArray(sources), context + format(':load:component=%s:bad sources array=%s',               arg_component.get_name()) )
				assert( T.isArray(targets) && targets.length > 0, context + format(':load:component=%s:bad targets',  arg_component.get_name()) )
				assert( T.isString(source_dom_event),  context + format(':load:component=%s:bad source event=%s',     arg_component.get_name(), source_dom_event) )
				assert( T.isString(target_method) || T.isNotEmptyArray(target_methods),  context + format(':load:component=%s:bad target method=%s:methods=%s', arg_component.get_name(), target_method, target_methods) )
				
				const data = options && options.method && options.method.operands ? options.method.operands : undefined
				const trace_enabled = false

				if ( T.isString(target_method) )
				{
					const streams = []
					sources.forEach(
						(arg_dom_selector)=>{
							const stream = new Stream()
							const handler = (component, dom_event_name, arg_selector, dom_event, dom_event_target, arg_data)=>{
								const data = {
									is_event_handler:true,
									component_name:component.get_name(),
									event_name:dom_event_name,
									dom_selector:arg_selector,
									target:dom_event_target,
									data:arg_data
								}
								
								console.log(context + ':load:component=%s:binding type=%s:event=%s:on handler:data=', arg_component.get_name(), type, source_dom_event, data)

								stream.push(data)
							}

							console.log(context + ':load:component=%s:binding type=%s:event=%s:selector=%s', arg_component.get_name(), type, source_dom_event, arg_dom_selector)
							
							arg_component.on_dom_event(source_dom_event, arg_dom_selector, handler, data, trace_enabled)
							streams.push(stream)
						}
					)
					
					// console.log(context + ':load:component=%s:binding type=%s:event=%s:sources=', arg_component.get_name(), type, source_dom_event, sources)
					
					return new BindingStream(arg_id, arg_runtime, arg_component)
						.set_stream(streams)
						.set_state_path(state_path)
						.set_source_transformation(xform)
						.set_targets_instances_array(targets)
						.set_target_method_name(target_method)
						.set_options(options)
						.build()
				}

				if ( T.isNotEmptyArray(target_methods) )
				{
					const binding_streams = []
					sources.forEach(
						(arg_dom_selector)=>{
							console.log(context + ':load:component=%s:target_methods binding type=%s:event=%s:selector=%s:methods=', arg_component.get_name(), type, source_dom_event, arg_dom_selector, target_methods)
							
							const stream = new Stream()
							const handler = (component, dom_event_name, arg_selector, dom_event, dom_event_target, arg_data)=>{
								const data = {
									is_event_handler:true,
									component_name:component.get_name(),
									event_name:dom_event_name,
									dom_selector:arg_selector,
									target:dom_event_target,
									data:arg_data
								}
								
								console.log(context + ':load:component=%s:target_methods binding type=%s:event=%s:on handler:data=', arg_component.get_name(), type, source_dom_event, data)

								stream.push(data)
							}

							arg_component.on_dom_event(source_dom_event, arg_dom_selector, handler, data, trace_enabled)
							
							_.forEach(targets,
								(target, index)=>{
									const method = index < target_methods.length ? target_methods[index] : target_methods[target_methods.length - 1]
									let method_name = undefined
									let method_operands = undefined
									if ( T.isObject(method) )
									{
										method_name = method.method
										method_operands = method.operands
									}
									else if ( T.isString(method) )
									{
										method_name = method
										if (data)
										{
											method_operands = {
												method:{
													operands:data
												}
											}
										}
									} else {
										console.warn(context + ':load:component=%s:error bad method:binding type=%s:event=%s:selector=%s:methods=%s', arg_component.get_name(), type, source_dom_event, arg_dom_selector, target_methods)
										return
									}

									console.log(context + ':load:component=%s:target_methods binding type=%s:method_name=%s:method_operands=', arg_component.get_name(), type, method_name, method_operands)

									const binding_stream = new BindingStream(arg_id, arg_runtime, arg_component)
										.set_stream(stream)
										.set_state_path(state_path)
										.set_source_transformation(xform)
										.set_targets_instances_array([target])
										.set_target_method_name(method_name)
										.set_options(method_operands)
										.build()
									binding_streams.push(binding_stream)
								}
							)
						}
					)

					return binding_streams
				}

				break
			}

			case 'stream': {
				assert( T.isObject(source_stream) && source_stream.is_stream, context + format(':load:component=%s:bad source stream', arg_component.get_name()) )
				assert( T.isArray(targets) && targets.length > 0, context + format(':load:component=%s:bad targets',  arg_component.get_name()) )
				assert( T.isString(target_method) || T.isNotEmptyArray(target_methods),  context + format(':load:component=%s:bad target method=%s',       arg_component.get_name(), target_method) )
				
				console.log(context + ':load:component=%s:target_methods binding type=%s:method_name=%s:method_operands=', arg_component.get_name(), type, target_method, options)
				
				return new BindingStream(arg_id, arg_runtime, arg_component)
					.set_stream(source_stream)
					.set_state_path(state_path)
					.set_source_transformation(xform)
					.set_targets_instances_array(targets)
					.set_target_method_name(target_methods ? target_methods : target_method)
					.set_options(options)
					.build()
			}

			default:{
				console.warn(context + ':load:component=%s:binding type=%s:type not found', arg_component.get_name(), type)
			}
		}
	}
}