js/base/component/rendering.js
// NPM IMPORTS
import assert from 'assert'
import vdom_parser from 'vdom-parser'
import diff from 'virtual-dom/diff'
import patch from 'virtual-dom/patch'
import create_element from 'virtual-dom/create-element'
// COMMON IMPORTS
import T from 'devapt-core-common/dist/js/utils/types'
import rendering_factory from 'devapt-core-common/dist/js/rendering/rendering_factory'
import RenderingResolverBuilder from 'devapt-core-common/dist/js/rendering/rendering_resolver'
// BROWSER IMPORTS
const context = 'browser/base/component/rendering'
/**
* @file UI component rendering class.
*
* @author Luc BORIES
*
* @license Apache-2.0
*/
export default class Rendering
{
/**
* Creates an instance of Rendering.
*
* @param {Component} arg_component - component instance.
* @param {string} arg_dom_id - component dom element id
*
* @returns {nothing}
*/
constructor(arg_component, arg_dom_id)
{
this.is_rendering = true
this._component = arg_component
assert( T.isNotEmptyString(arg_dom_id), context + ':constructor:bad dom id string')
// GET OR CREATE DOM ELEMENT
let dom_element = document.getElementById(arg_dom_id)
if (! dom_element)
{
dom_element = document.createElement('div')
dom_element.setAttribute('id', arg_dom_id)
}
this._dom_id = arg_dom_id
this._event_delegator = undefined
this.set_dom_element(dom_element)
this._dom_vnode = undefined
}
/**
* Get DOM id.
*
* @returns {string} - component DOM id.
*/
get_dom_id()
{
return this._dom_id
}
/**
* Test DOM Element instance.
*
* @returns {boolean}
*/
has_dom_element()
{
return T.isObject(this._dom_element) // TODO ENHANCE ELEMENT TYPE CHECK
}
/**
* Get DOM element.
*
* @returns {Element} - DOM Element instance.
*/
get_dom_element()
{
return this._dom_element
}
has_child_element(arg_parent_element, arg_child_element)
{
this._component.enter_group('has_child_element')
const elements = arg_parent_element ? arg_parent_element.children : undefined
if (elements && elements.length > 0)
{
let child = undefined
let i = 0
while(i < elements.length)
{
child = elements[i]
if (child == arg_child_element)
{
this._component.leave_group('has_child_element:found')
return true
}
++i
}
}
this._component.leave_group('has_child_element:not found')
return false
}
/**
* Set DOM element.
*
* @param {Element} arg_element - element instance.
*
* @returns {nothing}
*/
set_dom_element(arg_element)
{
this._component.enter_group('set_dom_element')
// CHECK GIVEN DOM ELEMENT
if (! arg_element)
{
this._component.error('set_dom_element:no given dom element')
this._component.leave_group('set_dom_element:no given dom element')
return
}
assert( (typeof arg_element) == 'object', context + ':set_dom_element:bad element object')
const new_elm = arg_element
const prev_elm = this.get_dom_element()
const parent_elm = prev_elm ? prev_elm.parentNode : undefined
// DEBUG
// console.log(prev_elm, context + ':set_dom_element:prev_elm')
// console.log(new_elm, context + ':set_dom_element:new_elm')
// console.log(parent_elm, context + ':set_dom_element:parent_elm')
// REMOVE PREVIOUS NODE FROM ITS PARENT
if (prev_elm != new_elm)
{
this._component.debug('set_dom_element:prev_elm <> new_elm')
// DISABLE EVENT DELEGATION
if (this._event_delegator)
{
this._component.debug('set_dom_element:destroy existing event delegator')
this._event_delegator.destroy()
}
}
// APPEND DOM ELEMENT TO ITS PARENT
if (parent_elm)
{
this._component.debug('set_dom_element:has parent_elm')
if ( ! this.has_child_element(parent_elm, new_elm) )
{
this._component.debug('set_dom_element:has parent_elm:append child')
parent_elm.appendChild(new_elm)
}
}
// SET DOM ELEMENT
this._dom_element = new_elm
// ENABLE EVENT DELEGATION FOR ALL DOM SUB ELEMENTS
if (! this._event_delegator)
{
this._component.debug('set_dom_element:create event delegator')
const EventDelegate = require('dom-delegate').Delegate
this._event_delegator = new EventDelegate(this._dom_element)
} else {
this._event_delegator.root(this._dom_element)
}
this._component.leave_group('set_dom_element')
}
/**
* Mount dom event handler.
*
* @{string} arg_dom_event - dom event name.
* @{string} arg_dom_selector - dom selector string ('tag_name.class1.class2').
* @{function} arg_handler - handler function f(component, event name, selection, event, target).
* @{any} arg_data - handler datas, default undefined (optional).
* @{boolean} arg_debug - trace flag, default true (optional).
*
* @returns {boolean}
*/
on_dom_event(arg_dom_event, arg_dom_selector, arg_handler, arg_data=undefined, arg_debug=true)
{
assert( T.isObject(this._event_delegator), context + ':on_dom_event:bad event delegator object' )
const name = this._component.get_name()
let selector = undefined
if ( T.isObject(arg_dom_selector) && arg_dom_selector.is_component)
{
selector = arg_dom_selector.get_dom_id()
} else if ( T.isString(arg_dom_selector) )
{
selector = arg_dom_selector
} else {
return false
}
this._event_delegator.on(arg_dom_event, selector,
(event, target)=>{
if (arg_debug)
{
console.log(context + ':on_dom_event:handler:component=%s event=%s selector=%s target=', name, arg_dom_event, arg_dom_selector, target, event, arg_data)
}
if ( T.isFunction(arg_handler) )
{
arg_handler(this._component, arg_dom_event, arg_dom_selector, event, target, arg_data)
}
event.stopPropagation()
return false
}
)
return true
}
/**
* Test DOM Virtual Node.
*
* @returns {boolean}
*/
has_dom_vnode()
{
return T.isObject(this._dom_vnode) // TODO ENHANCE VNODE TYPE CHECK
}
/**
* Get DOM Virtual Node.
*
* @returns {VNode}
*/
get_dom_vnode()
{
return this._dom_vnode
}
/**
* Set DOM Virtual Node.
*
* @param {VNode} arg_vnode - VNode instance.
*
* @returns {nothing}
*/
set_dom_vnode(arg_vnode)
{
if ( T.isObject(arg_vnode) ) // TODO ENHANCE VNODE TYPE CHECK
{
this._dom_vnode = arg_vnode
}
}
/**
* Render component VNode.
*
* @returns {Promise} - Promise of this component to chain promises.
*/
render()
{
this._component.enter_group('render')
const credentials = this._component._runtime.get_session_credentials()
const res_resolver = this._component._runtime.ui().get_resource_description_resolver()
const rf_resolver = this._component._runtime.ui().get_rendering_function_resolver()
const rendering_resolver = RenderingResolverBuilder.from_resolvers('browser resolver from ui', res_resolver, rf_resolver)
const rendering_context = {
trace_fn:undefined,//console.log,//
resolver:rendering_resolver,
credentials:credentials,
rendering_factory:rendering_factory
}
// debugger
const description = res_resolver( this._component.get_name() )
description.settings = description.settings ? description.settings : {}
description.settings.id = this.get_dom_id()
const rendering_result = rendering_factory(description, rendering_context, undefined)
const vnode = rendering_result.get_vtree(this.get_dom_id())
if (!vnode)
{
const msg = context + ':render:%s:bad vnode for rendering result'
console.error(msg + ' for ' + this._component.get_name())
console.log(msg + ':rendering_result=', this._component.get_name(), rendering_result)
console.log(msg + ':description=', this._component.get_name(), description)
this._component.leave_group('render:error')
return Promise.reject(msg)
}
const ui = window.devapt().ui()
let p = this.process_rendering_vnode(vnode)
// PROCESS HEADERS
ui._ui_rendering.process_rendering_result_headers(rendering_result.headers, credentials)
// PROCESS HEAD STYLES AND SCRIPTS
ui._ui_rendering.process_rendering_result_styles_urls (document.head, rendering_result.head_styles_urls, credentials)
ui._ui_rendering.process_rendering_result_styles_tags (document.head, rendering_result.head_styles_tags, credentials)
ui._ui_rendering.process_rendering_result_scripts_urls(document.head, rendering_result.head_scripts_urls, credentials)
ui._ui_rendering.process_rendering_result_scripts_tags(document.head, rendering_result.head_scripts_tags, credentials)
// PROCESS BODY STYLES AND SCRIPTS
ui._ui_rendering.process_rendering_result_styles_urls (document.body, rendering_result.body_styles_urls, credentials)
ui._ui_rendering.process_rendering_result_styles_tags (document.body, rendering_result.body_styles_tags, credentials)
ui._ui_rendering.process_rendering_result_scripts_urls(document.body, rendering_result.body_scripts_urls, credentials)
ui._ui_rendering.process_rendering_result_scripts_tags(document.body, rendering_result.body_scripts_tags, credentials)
this._component.leave_group('render:async')
return p
}
/**
* Process rendered VNode: Create or update DOM element.
*
* @param {VNode} arg_vnode - rendered virtual node.
*
* @returns {Promise} - Promise of this component to chain promises.
*/
process_rendering_vnode(arg_vnode)
{
this._component.enter_group('process_rendering_vnode')
// DEBUG
// debugger
// console.log(context + ':process_rendering_vnode:%s:vnode', this._component.get_name(), arg_vnode)
// GET COMPONENT ATTRIBUTES
const dom_id = this.get_dom_id()
this._component.debug('process_rendering_vnode:dom_id=' + dom_id)
// GET PREVIOUS VNODE OR BUILD IT FROM EXISTING HTML DOM
let prev_element = this.get_dom_element()
let prev_vnode = this.get_dom_vnode()
if ( ! prev_vnode && prev_element)
{
this._component.debug('process_rendering_vnode:no previous node, one previous element: create a previous vnode (parse previous element)')
// console.log(context + ':process_rendering_vnode:no previous vnode and dom element')
prev_vnode = vdom_parser(prev_element)
}
if (! prev_element)
{
this._component.debug('process_rendering_vnode:no previous element: create previous element with a DIV')
// console.log(context + ':process_rendering_vnode:create dom element')
prev_element = document.createElement('DIV')
prev_element.setAttribute('id', dom_id)
this.set_dom_element(prev_element)
prev_vnode = undefined
}
// SET NEW VNODE FROM JSON RESULT
let new_element = undefined
let new_vnode = arg_vnode
if (!new_vnode)
{
this._component.leave_group('process_rendering_vnode:new vnode not found')
return Promise.reject(context + ':process_rendering_vnode:new vnode not found for ' + this._component.get_name())
}
this.set_dom_vnode(new_vnode)
// PATCH EXISTING HTML DOM
if (prev_vnode && new_vnode && prev_element)
{
this._component.debug('process_rendering_vnode:previous node, new vnode, previous element:patch existing DOM for ' + this._component.get_name())
// console.log(context + ':process_rendering_vnode:previous vnode and new vnode and dom element exist')
// console.log(context + ':process_rendering_vnode:prev_element', prev_element )
// console.log(context + ':process_rendering_vnode:prev_vnode', prev_vnode )
// console.log(context + ':process_rendering_vnode:new_vnode', new_vnode )
const patches = diff(prev_vnode, new_vnode)
this._component.debug('patches', patches)
// console.log(context + ':process_rendering_vnode:patches', patches)
new_element = patch(prev_element, patches)
this.set_dom_element(new_element)
this._component.debug('process_rendering_vnode:new_element', new_element )
// console.log(context + ':process_rendering_vnode:new_element', new_element)
}
// BUILD HTML DOM
if (! prev_vnode && new_vnode)
{
this._component.debug('process_rendering_vnode:no previous node, new vnode:create element with new vnode')
// console.log(context + ':process_rendering_vnode:no previous vnode and new vnode', new_vnode)
const dom_element = create_element(new_vnode)
this.set_dom_element(dom_element)
// console.log(context + ':process_rendering_vnode:dom_elm', dom_element, this.get_dom_element())
}
this._component.leave_group('process_rendering_vnode')
return Promise.resolve(this._component)
}
/**
* Save rendering virtul node. Update component VNode with current component HTML.
*
* @returns {nothing}
*/
save_rendering()
{
this._component.info('save_rendering')
const dom_element = this.get_dom_element()
const vnode = vdom_parser(dom_element)
// console.info(context + ':save_rendering:vnode', vnode)
this.set_dom_vnode(vnode)
}
}