v0.0.13: Refactored the render-loop to solve hard-to-track bugs and added more (elaborate) unit-tests to make sure it all works
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
# Don't publish the src containing ESNext proposal's code. Only publish the bundled output in dist/ and the ES6-transpiled src from lib/
|
||||
src/*
|
||||
node_modules/*
|
||||
rollup.config.js
|
||||
yarn.lock
|
||||
@@ -110,7 +110,7 @@ export class CsxConfig {
|
||||
let srcOpts = {
|
||||
es: format==='es',
|
||||
single: type==='lib',
|
||||
sourcemap: true,// Just always there for now
|
||||
sourcemap: minified ? true : (format==='es'? false : true),
|
||||
minified: !!minified
|
||||
};
|
||||
let outDir = type==='lib'? lib : dist;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cerxes/csx",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.13",
|
||||
"author": "Miel Truyen <miel.truyen@cerxes.net>",
|
||||
"description": "CSX is a minimalistic UI-framework inspired by React+JSX for usage with WebComponents.",
|
||||
"repository": {
|
||||
|
||||
58
packages/csx/src/vdom/node-meta.js
Normal file
58
packages/csx/src/vdom/node-meta.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import './types';
|
||||
import {
|
||||
HostNodeRenderer, Host,
|
||||
ShadowNodeRenderer, ShadowDOM,
|
||||
PrimitiveRenderer, Primitive,
|
||||
NodeTreeRenderer, NativeRenderer
|
||||
} from "./renderers";
|
||||
|
||||
/**
|
||||
* Meta data used to handle a node in the render lope
|
||||
* @typedef {VNodeRendererMeta} VNodeMeta
|
||||
* @category VDOM.renderer
|
||||
* @property {VNodeRenderer} renderer - The renderer to use to update, create or delete this node
|
||||
* @property {VNodeType} normedType - Normed type of the node
|
||||
**/
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param vnode
|
||||
* @returns {VNodeMeta|undefined}
|
||||
*/
|
||||
export function getNodeMeta(vnode) {
|
||||
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
|
||||
if (vnode instanceof Node){
|
||||
return {
|
||||
...NativeRenderer.meta(),
|
||||
renderer: NativeRenderer,
|
||||
normedType: Node
|
||||
};
|
||||
}
|
||||
let type = vnode?.type;
|
||||
if (!type){
|
||||
return {
|
||||
...PrimitiveRenderer.meta(),
|
||||
renderer: PrimitiveRenderer,
|
||||
normedType: Primitive
|
||||
};
|
||||
} else if (type === Host){
|
||||
return {
|
||||
...HostNodeRenderer.meta(),
|
||||
renderer: HostNodeRenderer,
|
||||
normedType: Host
|
||||
};
|
||||
} else if (type === ShadowDOM){
|
||||
return {
|
||||
...ShadowNodeRenderer.meta(),
|
||||
renderer: ShadowNodeRenderer,
|
||||
normedType: ShadowDOM
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...NodeTreeRenderer.meta(),
|
||||
renderer: NodeTreeRenderer,
|
||||
normedType: window.customElements?.get(type) ?? type
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,53 @@
|
||||
import './types';
|
||||
import {
|
||||
HostNodeRenderer, Host,
|
||||
ShadowNodeRenderer, ShadowDOM,
|
||||
PrimitiveRenderer, Primitive,
|
||||
NodeTreeRenderer, NativeRenderer
|
||||
} from "./renderers";
|
||||
|
||||
export function getNodeMeta(vnode) {
|
||||
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
|
||||
if (vnode instanceof Node) return { renderer: NativeRenderer, normedType: Node };
|
||||
let type = vnode?.type;
|
||||
if (!type) return { renderer: PrimitiveRenderer, normedType: Primitive };
|
||||
else if (type === Host) return { renderer: HostNodeRenderer, normedType: Host };
|
||||
else if (type === ShadowDOM) return { renderer: ShadowNodeRenderer, normedType: ShadowDOM };
|
||||
else return { renderer: NodeTreeRenderer, normedType: window.customElements?.get(type) ?? type };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} RenderOptions
|
||||
* @category VDOM
|
||||
* @property {Element} [host] - The element to update to the specified VDOM
|
||||
* @property {VNode} [old] - Old VNode representation of rendered host
|
||||
* @property {Document} [document] - The document we're rendering to
|
||||
* @property {Element} [parent] - The parent element (TODO not sure what this will do when specified; Insert it as child element of the parent where?)
|
||||
*/
|
||||
import { getNodeMeta } from "./node-meta";
|
||||
|
||||
/**
|
||||
* This exists as a very basic example/test for JSX-to-DOM
|
||||
* @category VDOM
|
||||
* @param {VNode} vnode
|
||||
* @param {RenderOptions} opts
|
||||
* @param {RenderOptions} [opts]
|
||||
* @param {Element} [opts.host] - The element to update to the specified VDOM
|
||||
* @param {VNode} [opts.old] - Old VNode representation of rendered host
|
||||
* @param {Document} [opts.document] - The document we're rendering to
|
||||
* @return {Element}
|
||||
*/
|
||||
export function render(vnode, opts = {}) {
|
||||
// TODO this code could use restructuring when opts.host and vnode.type are incompatible (non updatable type), the host element should be replaced
|
||||
// with a newly created element, like it does with all child-elements..
|
||||
// General flow of this code is to process the hierarchy using a queue (so no recursion is used)
|
||||
// on each node of the hierarchy a renderer is determined which is compared to the renderer of the previous version of this vnode-hierarchy
|
||||
// to determine if these nodes can be updated (e.g updating a div, or textnode) and if they behave as a child-node (e.g. shows up in childNodes)
|
||||
// or are some other special type of node (like Host or ShadowDOM)
|
||||
/**
|
||||
*
|
||||
* @type {VRenderState}
|
||||
*/
|
||||
/** @type {VRenderState} */
|
||||
let state = {
|
||||
keyedElements: new Map(),
|
||||
refs: [],
|
||||
queue: [{
|
||||
// Start item
|
||||
item: {
|
||||
document: opts.document || document,
|
||||
host: opts.host,
|
||||
parent: opts.parent,
|
||||
old: opts.old,
|
||||
vnode: vnode
|
||||
},
|
||||
meta: getNodeMeta(vnode)
|
||||
meta: getNodeMeta(vnode),
|
||||
parent: null,
|
||||
attached: true// Root item is treated as if it is attached
|
||||
}]
|
||||
};
|
||||
|
||||
let newRoot = undefined;
|
||||
while (state.queue.length > 0) {
|
||||
let { item, meta, previous } = state.queue.splice(0, 1)[ 0 ];
|
||||
/** @type {VRenderQueueItem} */
|
||||
let queueItem = state.queue.shift();
|
||||
let {
|
||||
/** @type {VRenderItem} */ item,
|
||||
/** @type {VNodeMeta} */ meta,
|
||||
/** @type {VRenderItem} */ previousNode,
|
||||
/** @type {boolean} */ attached
|
||||
} = queueItem;
|
||||
/** @type {VNodeRenderer} */
|
||||
let renderer = meta.renderer;
|
||||
if (!renderer) throw new Error("No renderer for vnode", item.vnode);
|
||||
|
||||
// SVG handling..
|
||||
// SVG handling.. (this could be improved)
|
||||
if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
|
||||
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
||||
|
||||
@@ -74,8 +56,9 @@ export function render(vnode, opts = {}) {
|
||||
if (!item.host) {
|
||||
item.host = renderer.create(item, meta);
|
||||
newlyCreated = true;
|
||||
|
||||
if (item.vnode?.props?.ref) {// If props specify a ref-function, queue it to be called at the end of the render
|
||||
|
||||
// If props specify a ref-function, queue it to be called at the end of the render
|
||||
if (item.vnode?.props?.ref) {
|
||||
state.refs.push([item.vnode.props.ref, item.host]);
|
||||
}
|
||||
}
|
||||
@@ -84,180 +67,166 @@ export function render(vnode, opts = {}) {
|
||||
renderer.update(item, meta);
|
||||
|
||||
// Update children
|
||||
if (meta.normedType !== Node && (item.vnode?.children || item.old?.children)) {
|
||||
let childTypes = new Set();
|
||||
if (meta.hasChildren) {
|
||||
/** @type {FlattenedVNode[]} */
|
||||
let vChildren = flattenVNodeChildren(item.vnode);
|
||||
/** @type {FlattenedOldVNode[]} */
|
||||
let oldVChildren = flattenVNodeChildren(item?.old);
|
||||
/** @type {Map<*, FlattenedOldVNode>} */
|
||||
let keyedChildren = new Map();// Old child-items
|
||||
/** @type {NodeState[]} */
|
||||
let nodes = Array.from(item.host.childNodes).map(node=>({node, attached: true}));
|
||||
/** @type {FlattenedOldVNode[]} */
|
||||
let specialOldVChildren = []; // List of old-vnodes that are not node-type node and have no key
|
||||
|
||||
// Flatten and organize new vNode-children (this could be a separate function, or implemented using a helper function (because mucht of the code is similar between old/new vnodes)
|
||||
/**
|
||||
* @type { Object.<VNodeType, Array.<VRenderQueueItem>> }
|
||||
*/
|
||||
let vChildren = {};
|
||||
let queue = (item.vnode?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.splice(0, 1)[ 0 ];
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta && meta.renderer) {
|
||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
||||
let childType = meta.normedType;
|
||||
if (!meta.renderer.remove) childType = 'node'; // Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
|
||||
childTypes.add(childType);// Track that children of this type exist and should be iterated later
|
||||
vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
|
||||
vChildren[ childType ].push({
|
||||
item: {
|
||||
...item,
|
||||
old: undefined,
|
||||
vnode: next,
|
||||
host: undefined,
|
||||
parent: item.host
|
||||
},
|
||||
meta: meta
|
||||
});
|
||||
}
|
||||
// Iterate old children and link them to their corresponding elements
|
||||
let nodeI = 0, nodesLength = nodes.length;
|
||||
let oldVItemI = 0, oldVItemLength = oldVChildren.length;
|
||||
while(oldVItemI < oldVItemLength){
|
||||
/** @type {FlattenedOldVNode} */
|
||||
let oldVItem = oldVChildren[oldVItemI];
|
||||
/** @type {NodeState} */
|
||||
let node = nodes[nodeI];
|
||||
let nodeKey = oldVItem.vnode?.props?.key
|
||||
if(nodeKey !== undefined){
|
||||
keyedChildren.set(nodeKey, oldVItem);
|
||||
}
|
||||
++oldVItemI;
|
||||
if(oldVItem.meta.isNode && node){
|
||||
oldVItem.node = node;
|
||||
node.old = oldVItem;
|
||||
++nodeI;
|
||||
}else if(nodeKey === undefined){
|
||||
specialOldVChildren.push(oldVItem);
|
||||
}
|
||||
}
|
||||
// Any items not part of the old-vnode spec should be left alone, and ignored in further processing
|
||||
nodesLength = nodeI; // TODO this feels dirty, but not having this here will screw up our examples, we have yet to add a proper test
|
||||
|
||||
// Flatten and organize old-children
|
||||
/**
|
||||
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
|
||||
*/
|
||||
let oldVChildren = {};
|
||||
let curElement = item.host.firstChild;
|
||||
queue = (item.old?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.splice(0, 1)[ 0 ];
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta && meta.renderer) {
|
||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
||||
let childType = meta.normedType;
|
||||
let childElement;
|
||||
if (!meta.renderer.remove) {
|
||||
childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
|
||||
if (curElement) {
|
||||
childElement = curElement;
|
||||
curElement = curElement.nextSibling;
|
||||
}
|
||||
}
|
||||
childTypes.add(childType);// Track that children of this type exist and should be iterated later
|
||||
oldVChildren[ childType ] = oldVChildren[ childType ] || []; // Make sure the array exists
|
||||
let oldItem = {
|
||||
vnode: next,
|
||||
element: childElement,
|
||||
meta: meta
|
||||
};
|
||||
oldVChildren[ childType ].push(oldItem);
|
||||
if (next.props?.key) {
|
||||
state.keyedElements.set(next.props?.key, oldItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedChildTypes = Array.from(childTypes).sort((a, b) => a === 'node' ? 1 : -1); // Always do ChildNode-types last
|
||||
let queuedItems = [];
|
||||
/**@type {VRenderQueueItem}*/ let previous = null;
|
||||
for (let childType of sortedChildTypes) {
|
||||
let newChildren = vChildren[ childType ];
|
||||
let oldChildren = oldVChildren[ childType ];
|
||||
// Iterate new children, remove old nodes as needed and create new queue items
|
||||
/** @type {VRenderQueueItem[]} */
|
||||
let queueChildren = [];
|
||||
/** @type {VRenderItem} */
|
||||
let previousNodeItem = undefined;
|
||||
let vItemI = 0, vItemLength = vChildren.length;
|
||||
nodeI = 0;
|
||||
while(vItemI < vItemLength || nodeI < nodesLength){
|
||||
let vItem = vChildren[vItemI];
|
||||
let node = nodes[nodeI];
|
||||
|
||||
while (newChildren && newChildren.length) {
|
||||
let child = newChildren.splice(0, 1)[ 0 ];
|
||||
if(vItem){
|
||||
let nodeKey = vItem?.vnode?.props?.key;
|
||||
/** @type {FlattenedOldVNode} */
|
||||
let oldItem = undefined;
|
||||
if(nodeKey!==undefined){
|
||||
oldItem = keyedChildren.get(nodeKey);
|
||||
}
|
||||
|
||||
// Key handling
|
||||
let childKey = child.item.vnode.props?.key;
|
||||
/**@type {VOldQueueItem}*/ let oldChild;
|
||||
if (childKey) {
|
||||
oldChild = state.keyedElements.get(childKey);
|
||||
if (oldChild) {
|
||||
if (oldChildren && oldChildren[ 0 ] === oldChild) {
|
||||
// Old keyed child already in the right place (just clear it from the queue);
|
||||
oldChildren.splice(0, 1);
|
||||
/** @type {NodeState} */
|
||||
let matchedNode = undefined;
|
||||
|
||||
// If this is a DOM-node-type match it to an existing node
|
||||
if(vItem.meta.isNode) {
|
||||
// Match new item to existing node when possible
|
||||
matchedNode = oldItem?.node;
|
||||
if (matchedNode) {
|
||||
// DOM-node matched by key
|
||||
if (matchedNode === node) {
|
||||
// Item in order with old representation
|
||||
++nodeI;
|
||||
} else {
|
||||
// Old keyed child not already in the right place
|
||||
let indexOfKeyed = oldChildren.indexOf(oldChild);
|
||||
if (indexOfKeyed) {
|
||||
oldChildren.splice(indexOfKeyed, 1);
|
||||
if(oldChild.element) {
|
||||
item.host.removeChild(oldChild.element);
|
||||
}else{
|
||||
// This apparantly happens
|
||||
}
|
||||
}
|
||||
if (previous) {
|
||||
previous.host.after(oldChild.element);
|
||||
} else {
|
||||
//item.parent.prepend(oldChild.element);
|
||||
item.host.prepend(oldChild.element);
|
||||
// Out of order, detach
|
||||
if (matchedNode.attached) {
|
||||
item.host.removeChild(matchedNode.node);
|
||||
matchedNode.attached = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!oldChild) oldChild = oldChildren && oldChildren.splice(0, 1)[ 0 ];
|
||||
|
||||
child.previous = previous;
|
||||
if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
|
||||
&& (child.meta.normedType !== Node || child.item.vnode === oldChild.vnode)) {
|
||||
// Update old-child
|
||||
child.item.host = oldChild.element;
|
||||
child.item.old = oldChild.vnode;
|
||||
queuedItems.push(child);
|
||||
} else {
|
||||
// New child
|
||||
if (oldChild) {
|
||||
if(oldChild.element) {
|
||||
if (oldChild.meta.renderer.remove){
|
||||
oldChild.meta.renderer.remove({
|
||||
...item,
|
||||
parent: item.host,
|
||||
host: oldChild.element
|
||||
});
|
||||
}else {
|
||||
item.host.removeChild(oldChild.element);
|
||||
} else if (node) {
|
||||
// No match by key
|
||||
// If old VNode associated, check if updatable
|
||||
if (node.old
|
||||
&& node.old.vnode?.props?.key === undefined
|
||||
&& node.old.meta.normedType === vItem.meta.normedType
|
||||
) {
|
||||
oldItem = node.old;
|
||||
matchedNode = node;
|
||||
++nodeI;
|
||||
} else {
|
||||
// Not updatable, detach
|
||||
if (node.attached) {
|
||||
item.host.removeChild(node.node);
|
||||
node.attached = false;
|
||||
}
|
||||
}else{
|
||||
// This apparantly happens
|
||||
}
|
||||
}
|
||||
queuedItems.push(child);
|
||||
}
|
||||
if (!child.meta.renderer.remove) {
|
||||
// If child is a node-type item track it as the previous (so we can insert next node-type items after it as intended)
|
||||
previous = child.item;
|
||||
}
|
||||
}
|
||||
while (oldChildren && oldChildren.length) {
|
||||
let oldChild = oldChildren.splice(0, 1)[ 0 ];
|
||||
if(oldChild.element) {
|
||||
if (oldChild.meta.renderer.remove) {
|
||||
oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element });
|
||||
} else {
|
||||
item.host.removeChild(oldChild.element);
|
||||
}
|
||||
}else{
|
||||
// This apparantly happens
|
||||
// If this is a special type and it was not matched to an old VNode by key, find it the first non-keyed
|
||||
// old VNode with the same normedType (These are ShadowDOM or Host-types)
|
||||
if(!oldItem){
|
||||
let indexOf = specialOldVChildren.findIndex(old=>old.meta.normedType===vItem.meta.normedType);
|
||||
if(indexOf>=0){
|
||||
oldItem = specialOldVChildren[indexOf];
|
||||
specialOldVChildren.splice(indexOf,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track that the node is used by a new node (we don't use this anywhere?!)
|
||||
if(matchedNode){
|
||||
matchedNode.new = vItem;
|
||||
}
|
||||
|
||||
/** @type {VRenderQueueItem} */
|
||||
let queueItem = {
|
||||
item: {
|
||||
vnode: vItem.vnode,
|
||||
document: item.document,
|
||||
host: matchedNode?.node,
|
||||
old: oldItem?.vnode,
|
||||
inSvg: item.inSvg,
|
||||
parent: item,
|
||||
},
|
||||
attached: matchedNode?.attached,
|
||||
previousNode: previousNodeItem,
|
||||
meta: vItem.meta
|
||||
}
|
||||
queueChildren.push(queueItem);
|
||||
if(vItem.meta.isNode){
|
||||
previousNodeItem = queueItem.item;
|
||||
}
|
||||
|
||||
++vItemI;
|
||||
}else if(node){
|
||||
// Trailing node item
|
||||
if(node.attached && !node.new) {
|
||||
// If this node was not reused for a new vnode representation, and was not already detached
|
||||
// for other reasons, remove it
|
||||
item.host.removeChild(node.node);
|
||||
node.attached = false;
|
||||
}
|
||||
++nodeI;
|
||||
}
|
||||
}
|
||||
|
||||
state.queue.splice(0, 0, ...queuedItems);
|
||||
state.queue.unshift(...queueChildren);
|
||||
|
||||
// Remove any special types (Host, ShadowDOM that were no longer present)
|
||||
for(let oldSpecialVnode of specialOldVChildren){
|
||||
oldSpecialVnode.meta.renderer.remove?.(oldSpecialVnode.vnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyCreated) {
|
||||
if (!meta.renderer.remove) {
|
||||
if (item.parent) {
|
||||
if (!previous) {
|
||||
// First child
|
||||
item.parent.prepend(item.host);
|
||||
} else {
|
||||
// Subsequent child
|
||||
previous.host.after(item.host);
|
||||
}
|
||||
}
|
||||
if (!attached && meta.isNode) {
|
||||
if (previousNode) {
|
||||
// Subsequent child
|
||||
previousNode.host.after(item.host);
|
||||
} else if (item.parent) {
|
||||
// First child
|
||||
item.parent.host.prepend(item.host);
|
||||
}
|
||||
if (!item.parent) newRoot = item.host;
|
||||
}
|
||||
if(!item.parent && !newRoot){
|
||||
newRoot = item.host;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,3 +235,46 @@ export function render(vnode, opts = {}) {
|
||||
}
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intermediate type used to process child-items
|
||||
* @typedef {object} FlattenedVNode
|
||||
* @property {VNode} vnode
|
||||
* @property {VNodeMeta} meta
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} NodeState
|
||||
* @property {ChildNode} node
|
||||
* @property {boolean} attached
|
||||
* @property {FlattenedOldVNode} old
|
||||
* @property {FlattenedVNode} new
|
||||
*/
|
||||
/**
|
||||
* Intermediate type used to process old child-items
|
||||
* @typedef {FlattenedVNode} FlattenedOldVNode
|
||||
* @property {NodeState} node - The DOM-node
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {VNode} vnode
|
||||
* @returns {FlattenedVNode[]}
|
||||
*/
|
||||
function flattenVNodeChildren(vnode){
|
||||
let children = [];
|
||||
let queue = (vnode?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.shift();
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta) {
|
||||
children.push({ vnode: next, meta });
|
||||
}else{
|
||||
// null or undefined child, ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
@@ -10,12 +10,22 @@ export const Host = Symbol('Host');
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const HostNodeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: true, // Host node should always
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
create(item){
|
||||
if(!item.parent) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent;
|
||||
if(!item.parent.host) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent.host;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -31,7 +41,7 @@ export const HostNodeRenderer = {
|
||||
* @param {VRenderState} state
|
||||
*/
|
||||
update(item, state){
|
||||
item.host = item.host || item.parent;
|
||||
item.host = item.host || item.parent.host;
|
||||
NodeTreeRenderer.update(item,state);
|
||||
},
|
||||
};
|
||||
@@ -7,6 +7,16 @@ import '../types';
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const NativeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: false,// Counter intiutitive, but this is false as CSX will never manage children of a premade DOM-element
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
@@ -18,6 +28,9 @@ export const NativeRenderer = {
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
update(item){
|
||||
return;// NO-OP
|
||||
if(item.old && item.old!==item.vnode){
|
||||
item.host.replaceWith(item.vnode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,16 @@ export const Primitive = Symbol("primitive");
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const PrimitiveRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: false,// Primitive type (TextNode) can't have children
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,16 @@ let namespace = {
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const NodeTreeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {VRenderItem} item
|
||||
|
||||
@@ -9,12 +9,22 @@ export const ShadowDOM = Symbol('ShadowDOM');
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const ShadowNodeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
create(item) {
|
||||
if (!item.parent) throw new Error("ShadowDOM node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent.shadowRoot || item.parent.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
|
||||
else return item.parent.host.shadowRoot || item.parent.host.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -28,6 +38,6 @@ export const ShadowNodeRenderer = {
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
update(item) {
|
||||
item.host = item.host || item.parent.shadowRoot;
|
||||
item.host = item.host || item.parent.host.shadowRoot;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./vnode";
|
||||
export * from "./render-item";
|
||||
export * from "./render-state";
|
||||
export * from "./vnode-renderer";
|
||||
export * from "./renderer-meta";
|
||||
@@ -2,13 +2,12 @@ import './vnode';
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderItem
|
||||
* @interface
|
||||
* @interface VRenderItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VNode} vnode - The VNode representation to update to
|
||||
* @property {VNode} [old] - The previous VNode representation of this item
|
||||
* @property {Element} host - The DOM-node being rendered
|
||||
* @property {Document} document - The DOM-document to be added to
|
||||
* @property {boolean} inSvg - Indicates whether this node is a child of an SVG element, and should thus be created with createElementNS(...)
|
||||
* @property {Element} [parent] - Parent DOM-node
|
||||
* @property {VRenderItem} [parent] - Parent render item
|
||||
**/
|
||||
|
||||
@@ -6,8 +6,7 @@ import "./vnode";
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderQueueItemMetadata
|
||||
* @interface
|
||||
* @typedef {object} VRenderQueueItemMetadata
|
||||
* @category VDOM.renderer
|
||||
* @property {VNodeRenderer} renderer - The renderer that will render this item
|
||||
* @property {VNodeType} normedType - The normed type of a VNode, for most VNode this just maps to vnode.type, but a text-node normally does not have a type.
|
||||
@@ -16,30 +15,28 @@ import "./vnode";
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderQueueItem
|
||||
* @interface
|
||||
* @typedef {object} VRenderQueueItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VRenderItem} item - The item to queue for rendering
|
||||
* @property {VRenderItem} item - The item queued for rendering
|
||||
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
|
||||
* @property {VRenderItem} previous - The item that will have been inserted before this one
|
||||
* @property {VRenderItem} [previousNode] - The previous node-item that will have been inserted before this one
|
||||
* @property {boolean} [attached] - Whether the host-node was already attached to the dom
|
||||
**/
|
||||
|
||||
/**
|
||||
* Temporary data structure for listing an old VNode
|
||||
* @typedef VOldQueueItem
|
||||
* @interface
|
||||
* @typedef {object} VOldQueueItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VNode} vnode - The old vnode
|
||||
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
|
||||
* @property {Element} element - The matching element
|
||||
* @property {boolean} detached - Indicates if the element was already removed
|
||||
**/
|
||||
|
||||
/**
|
||||
* Global rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderState
|
||||
* @interface
|
||||
* @typedef {object} VRenderState
|
||||
* @category VDOM.renderer
|
||||
* @property {Array.<VRenderQueueItem>} queue - The queue of items to be rendered
|
||||
* @property {Array.<[Function,Element]>} refs - Ref-callback functions be called when rendering is done
|
||||
* @property {Map.<string, VOldQueueItem>} keyedElements - A map of (old) keyed elements
|
||||
**/
|
||||
|
||||
9
packages/csx/src/vdom/types/renderer-meta.js
Normal file
9
packages/csx/src/vdom/types/renderer-meta.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import './vnode';
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @interface VNodeRendererMeta
|
||||
* @category VDOM.renderer
|
||||
* @property {boolean} isNode - Whether the node-type handled by this renderer has a place in the DOM hierarchy
|
||||
* @property {boolean} hasChildren - Indicates if the node-type handled by this renderer can have child-nodes
|
||||
**/
|
||||
@@ -1,18 +1,26 @@
|
||||
import "./render-item";// Info about what we're rendering and where to
|
||||
import "./renderer-meta"; // Meta-data about the type of node handled
|
||||
|
||||
// Note: This type is not meant to be public
|
||||
|
||||
/**
|
||||
* Represents a renderer capable of rendering a VNode of a certain type
|
||||
* @interface VNodeRenderer
|
||||
* @class
|
||||
**/
|
||||
|
||||
/**
|
||||
* This method creates the element corresponding to a vnode
|
||||
* @method
|
||||
* @name VNodeRenderer#meta
|
||||
* @returns {VNodeRendererMeta}
|
||||
*/
|
||||
|
||||
/**
|
||||
* This method creates the element corresponding to a vnode
|
||||
* @method
|
||||
* @name VNodeRenderer#create
|
||||
* @param {VRenderItem} item
|
||||
* @param {VRenderItem} parent
|
||||
* @returns {Element}
|
||||
*/
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
/**
|
||||
* A tree of virtual-nodes (e.g, type,props,attr and nested children)
|
||||
* @typedef VNodeTree
|
||||
* @interface
|
||||
* @interface VNodeTree
|
||||
* @category VDOM
|
||||
* @property {VNodeType} type - TagName or CustomElement of the html-element
|
||||
* @property {VNodeProps} props - Properties to set on the element
|
||||
|
||||
Reference in New Issue
Block a user