js/base/component/stated_dom.js
// NPM IMPORTS
import assert from 'assert'
import _ from 'lodash'
// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'
import uid from 'devapt-core-common/dist/js/utils/uid.js'
// BROWSER IMPORTS
import BoundDom from './bound_dom'
const context = 'browser/base/component/stated_dom'
/**
* @file UI stated dom class.
*
* @author Luc BORIES
* @license Apache-2.0
*
* @example:
* API
* ->update():Promise - Update view with current state.
* ->update_children():Promise - Update view with current state.
*
* ->clear():Promise - Clear component to initial values.
*
* ->dispatch_update_state_action(arg_new_state):nothing - Dispatch update state action.
* ->dispatch_update_state_value_action(arg_path, arg_value):nothing - Dispatch update state action.
*
* ->get_children_component():array - Get view children components.
*
*/
export default class StatedDom extends BoundDom
{
/**
* Creates an instance of StatedDom.
*
* @param {RuntimeBase} arg_runtime - client runtime.
* @param {Immutable.Map} arg_state - component initial state.
* @param {string} arg_log_context - context of traces of this instance (optional).
*
* @returns {nothing}
*/
constructor(arg_runtime, arg_state, arg_log_context)
{
const log_context = arg_log_context ? arg_log_context : context
super(arg_runtime, arg_state, log_context)
this.is_stated_dom = true
// CHILDREN COMPONENTS
this._children_components = undefined
// DEBUG
// console.info(context + ':constructor:creating component ' + this.get_name())
// this.enable_trace()
}
/**
* Update view with current state.
*
* @returns {Promise}
*/
update()
{
this.enter_group('update')
console.info(context + ':update:%s', this.get_name())
this._ready_promise = this._ready_promise.then(
()=>{
return this._update()
}
)
this.leave_group('update:async')
return this._ready_promise
}
_update()
{
this.debug('update:name=' + this.get_name() + ',dom_id=' + this.get_dom_id() )
const new_elm = document.getElementById(this.get_dom_id())
const prev_elm = this.get_dom_element()
// console.log(prev_elm, context + ':update:prev_elm')
// console.log(new_elm, context + ':update:new_elm')
if (!new_elm)
{
// this.leave_group('update')
return Promise.resolve()
}
if (prev_elm != new_elm)
{
this.debug(':update:prev_elm <> new_elm')
if (prev_elm.parentNode)
{
prev_elm.parentNode.removeChild(prev_elm)
}
this._dom_element = new_elm
}
let promise = Promise.resolve()
if ( T.isFunction(this._update_self) )
{
this.debug(':update:call _update_self (async)')
promise = promise.then(
()=>{
this._update_self(prev_elm, new_elm)
}
)
}
promise = promise.then(
()=>
{
this.update_children()
}
)
return promise
}
/**
* Update view with current state.
*
* @returns {Promise}
*/
update_children()
{
this.enter_group('update_children')
this.get_children_component().forEach(
(component)=>{
this.debug(':update_children:component=' + component.get_name())
component.update()
}
)
this.leave_group('update_children')
}
/**
* Clear component to initial values.
*
* @returns {Promise}
*/
clear()
{
// TO OVERWRITE
}
/**
* Get a named stream.
*
* @param {string} arg_stream_name - stream name.
*
* @returns {Stream|undefined} - found stream.
*/
get_named_stream(arg_stream_name)
{
switch(arg_stream_name.toLocaleLowerCase()) {
case 'runtime_logs': return this._runtime.logs_stream
}
console.warn(context + ':get_named_stream:%s:unknow named stream', this.get_name(), arg_stream_name.toLocaleLowerCase())
return undefined
}
/**
* Dispatch update state action.
*
* @param {Immutable.Map} arg_new_state - new state Immutable Map.
*
* @returns {nothing}
*/
dispatch_update_state_action(arg_new_state)
{
if ( ! T.isObject(arg_new_state) )
{
return
}
const new_state = arg_new_state.toJS ? arg_new_state.toJS() : arg_new_state
// console.log(context + ':dispatch_update_state_action:new state:', new_state)
const action = { type:'ADD_JSON_RESOURCE', resource:this.get_name(), path:this.get_state_path(), json:new_state }
window.devapt().ui().store.dispatch(action)
}
/**
* Dispatch update state action.
*
* @param {array|string} arg_path - component state path.
* @param {any} arg_value - component state value.
*
* @returns {nothing}
*/
dispatch_update_state_value_action(arg_path, arg_value)
{
if (! T.isArray(arg_path) )
{
console.error(context + ':dispatch_update_state_value_action:bad path array:path,value:', arg_path, arg_value)
return
}
// console.log(context + ':dispatch_update_state_value_action:path,value:', arg_path, arg_value)
const new_state = this.get_state().setIn(arg_path, arg_value)
this.dispatch_update_state_action(new_state)
}
/**
* Get view children components.
*
* @returns {array} - list of Component.
*/
get_children_component()
{
// SKIP FOR MENUBAR
if (this.is_menubar)
{
return []
}
if ( ! this._children_components)
{
this._children_components = []
// GET APPLICATION STATE AND INIT APPLICATION STATE PATH
const ui = window.devapt().ui()
const current_app_state = ui.store.get_state()
const state_path = ['views']
// GET COMPONENT DESCRIPTION AND CHILDREN
const component_desc = ui._ui_factory.find_component_desc(current_app_state, this.get_name(), state_path)
const children = component_desc ? component_desc.get('children', undefined) : undefined
const children_names = children ? Object.keys( children.toJS() ) : []
this.debug(':get_children_component:init with children_names:', children_names)
// GET COMPONENT ITEMS
const items = this.get_state_value('items', [])
this.debug(':get_children_component:init with items:', JSON.stringify(items))
const all_children = _.concat(children_names, items)
// console.info(context + ':get_children_component:all_children:%s:', this.get_name(), all_children)
const unique_children = {}
all_children.forEach(
(item)=>{
if ( T.isObject(item) )
{
this.debug(':get_children_component:loop on item object')
if ( T.isString(item.view) )
{
if (item.viewitem in unique_children)
{
return
}
this.debug(':get_children_component:loop on item string:', item.view)
const component = window.devapt().ui(item.view)
if (component && component.is_component)
{
this._children_components.push(component)
unique_children[component.get_name()] = true
return
}
this.warn(':get_children_component:bad item component for:', item.view)
return
}
this.warn(':get_children_component:bad item object for:', JSON.stringify(item))
return
}
if ( T.isString(item) )
{
if (item in unique_children)
{
return
}
this.debug(':get_children_component:loop on item string:', item)
const component = window.devapt().ui(item)
if (component && component.is_component)
{
this._children_components.push(component)
unique_children[component.get_name()] = true
return
}
this.warn(':get_children_component:bad item component for:', item)
return
}
this.warn(':get_children_component:bad item type for:', item.toString())
}
)
}
this.debug(':get_children_component:', this._children_components)
return this._children_components
}
/**
* Render a component inside this element from a json description.
*
* @param {object} arg_options - json source configuration.
*
* @returns {nothing}
*/
register_and_render_inside_from_json(arg_options)
{
this.enter_group('register_and_render_inside_from_json')
const json = this.register_from_json(arg_options)
if (! T.isObject(json) )
{
this.leave_group('register_and_render_inside_from_json:error:bad json object')
return
}
this.render_inside_from_json(json.name, json)
this.leave_group('register_and_render_inside_from_json')
}
/**
* Register a component description from a json content.
*
* @param {object} arg_options - json source configuration.
*
* @returns {nothing}
*/
register_from_json(arg_options)
{
this.enter_group('register_from_json')
console.log(context + ':register_from_json:options=', arg_options)
if (arg_options.is_event_handler)
{
arg_options = arg_options.data
}
// CHECK CONFIGURATION
if ( ! T.isObject(arg_options) )
{
console.warn(context + ':register_from_json:bad options object')
this.leave_group('register_from_json:error:bad options object')
return
}
if ( ! T.isString(arg_options.json_source_view) )
{
console.warn(context + ':register_from_json:bad options.json_source_view string')
this.leave_group('register_from_json:error:bad options.json_source_view string')
return
}
if ( ! T.isString(arg_options.json_source_getter) )
{
console.warn(context + ':register_from_json:bad options.json_source_getter string')
this.leave_group('register_from_json:error:bad options.json_source_getter string')
return
}
const source_object = this.get_runtime().ui().get(arg_options.json_source_view)
if ( ! T.isObject(source_object) || ! source_object.is_component )
{
console.warn(context + ':register_from_json:%s:view=%s:bad json source component', this.get_name(), arg_options.json_source_view, source_object)
this.leave_group('register_from_json:error:bad json source component')
return
}
if ( ! (arg_options.json_source_getter in source_object) )
{
console.warn(context + ':register_from_json:bad json source method for component')
this.leave_group('register_from_json:error:bad json source method for component')
return
}
if ( ! ( T.isFunction( source_object[arg_options.json_source_getter] )) )
{
console.warn(context + ':register_from_json:bad json source method for component')
this.leave_group('register_from_json:error:bad json source method for component')
return
}
// GET JSON FROM SOURCE
try{
const json = source_object[arg_options.json_source_getter]()
// DEBUG
console.log(context + ':register_from_json:json=', json)
// CHECK COMPONENT NAME
if ( ! T.isString(json.name) )
{
console.warn(context + ':register_from_json:bad json.name string')
this.leave_group('register_from_json:error:bad json.name string')
return
}
// STORE COMPONENT DESCRIPTION
const action = { type:'ADD_JSON_RESOURCE', resource:json.name, collection:'views', json:json }
this.get_runtime().get_state_store().dispatch(action)
this.leave_group('register_from_json')
return json
}
catch(e) {
console.warn(context + ':register_from_json:error %s', e)
this.leave_group('register_from_json:error:' + e)
return undefined
}
}
/**
* Render a component inside this element from a json description.
*
* @param {string} arg_name - component name.
* @param {object} arg_json_desc - component description.
*
* @returns {nothing}
*/
render_inside_from_json(arg_name, arg_json_desc)
{
this.enter_group('render_inside_from_json')
console.log(context + ':render_inside_from_json:name and description:', arg_name, arg_json_desc)
try{
// CREATE COMPONENT ELEMENT
const this_element = this.get_dom_element()
if ( ! this_element)
{
console.warn(context + ':render_inside_from_json:bad dom element')
this.leave_group('render_inside_from_json:error:bad dom element')
return
}
const this_document = this_element.ownerDocument
const existing_element = this_document.getElementById(arg_name)
let sub_element = undefined
if (existing_element)
{
if (existing_element.parentElement == this_element)
{
sub_element = existing_element
} else {
console.warn(context + ':render_inside_from_json:a previous element exist with given name=%s', arg_name)
this.leave_group('render_inside_from_json:error:bad dom element')
return
}
} else {
sub_element = this_element.ownerDocument.createElement('div')
sub_element.setAttribute('id', arg_name)
this_element.appendChild(sub_element)
}
const component = this.get_runtime().ui().create_local(arg_name, arg_json_desc)
component.render(true)
.then(
()=>{
window.devapt().content_rendered()
}
)
} catch(e){
console.warn(context + ':render_inside_from_json:error %s', e)
return
}
}
}