Virtual DOM (Vdom)
Since most parts of the neoteric framework as well as all apps you will create run inside the App worker, there is no direct access to the DOM (window & window.document are undefined inside the worker scope).
Neoteric uses a custom JSON / JS-Object based vdom engine to handle the component markup and the changes. As an example, let's take a look at the component.Button markup:
_vdom: {
tag: 'button',
cn : [{
tag: 'span',
cls: ['neo-button-glyph']
}, {
tag: 'span',
cls: ['neo-button-text']
}]
}
This template filled with content would create a resulting html like:
<button class="neo-button icon-left" id="neo-button-1000">
<span class="neo-button-glyph fa fa-home" id="neo-vnode-1000"></span>
<span class="neo-button-text" id="neo-vnode-1001">Hello World</span>
</button>
Which will look like this:
Available properties for each vdom object are:
{String} tag|nodeName the tag of the node
{String} html|innerHTML node.innerHTML
{Array} cn child nodes
{Array|String} cls node.className
{String} flag pseudo-attribute to find nodes via util.VDom.getByFlag() or util.VDom.getFlags()
{Number|String} height shortcut for style.height, defaults to px
{String} id the dom id, will get auto-populated if not set (e.g. neo-vnode-1)
{Number|String} minHeight shortcut for style.minHeight, defaults to px
{Number|String} minWidth shortcut for style.minWidth, defaults to px
{Object|String} style camel cased style definitions
{Boolean} removeDom pseudo-attribute to remove a node from the VNode tree
{Number|String} width shortcut for style.width, defaults to px
For more details, take a look at: vdom.Helper:parseHelper()
Rendering an app
Once you created your app, you will call the render() method for its main view.
This will send the full vdom tree to the Vdom worker and create a Virtual Node (VNode) tree.
See: component.Base:render()
The top level vnode will contain the full html as a string inside the outerHTML property. If you want to, you could create an outerHTML property for each sub-node as well (take a look at the vdom.Helper config returnChildNodeOuterHtml: false. Not recommended or needed to change it, but it could help you for debugging purposes).
Mounting an app
Once you call mount() on your application main view (or in case the autoMount config is set to true), the App worker will send the html to the main thread which will inject the html into a given DOM node id or the framework will take care of the DOM node itself in case the main view is a container.Viewport.
Virtual Nodes (Vnodes)
After render() gets called on your main view, the framework will assign the relevant vnode sub-trees to each container item in a recursive way. So at this point, every rendered component of your app will have a vdom and a vnode property.
Delta Updates
After your app is mounted, you can modify the vdom of each component directly to trigger delta updates. As an example:
myButton.iconColor='red';
This "assignment" will trigger the setter method of this config, which will call:
afterSetIconColor(value) {
let me = this,
vdom = me.vdom,
iconNode = me.getVdomRoot().cn[0];
if (!iconNode.style) {
iconNode.style = {};
}
if (value && value !== '') {
iconNode.style.color = value;
me.vdom = vdom;
}
}
The result could look like this:
The key here is me.vdom = vdom; which will trigger the vdom config setter, resulting in:
/**
* Gets called after the vdom config gets changed in case the component is already mounted (delta updates).
* @param {Object} vdom
* @param {Neo.vdom.VNode} vnode
* @private
*/
updateVdom(vdom, vnode) {
let me = this;
Neo.vdom.Helper.update({
vdom : vdom,
vnode: vnode
}).then(data => {
Object.keys(me.vnode).forEach(key => {
delete me.vnode[key];
});
Object.assign(me.vnode, data.vnode);
me.syncVdomIds(me.vnode, me.vdom);
}).catch(err => {
console.log('Error attempting to update component dom', err, me);
});
}
The App worker will send the vdom & vnode property of this component to the VDom worker,
which will create a new vnode using the vdom and then compare the old & new vnodes to calculate the deltas,
which will modify the DOM as needed inside the main thread. Take a look at:
vdom.Helper:update()
The given example will create an deltas array like:
deltas: [{
id: 'neo-button-1000',
style: {
color: 'red'
}
}]
The deltas will get sent to the main thread & main.DomAccess:update() will apply each delta update to the real DOM automatically.
The virtual dom engine for delta updates is optional!
In case you do know exactly what needs to change for a given component, you can also manually create the deltas and send the result from the App worker to the main thread. We do recommend to keep the vdom in sync though, since a top level component update could revert the manual changes otherwise. One example for this strategy would be component.Helix:
afterSetPerspective(value) {
let me = this;
if (me.mounted) {
Neo.currentWorker.promiseMessage('main', {
action: 'updateDom',
deltas: {
id : me.vdom.id,
style: {
perspective: value + 'px'
}
}
});
me.updateCloneTranslate();
}
}