diff --git a/packages/csx-custom-elements/src/custom-element/custom-element.js b/packages/csx-custom-elements/src/custom-element/custom-element.js index a8b7a8f..0cc7951 100644 --- a/packages/csx-custom-elements/src/custom-element/custom-element.js +++ b/packages/csx-custom-elements/src/custom-element/custom-element.js @@ -3,22 +3,26 @@ import {render} from "../vdom"; /** * The decorators proposal has changed since @babel implemented it. This code will need to change at some point... + * THIS IS TOTALLY FIGGIN BROKEN!! valueMap used to be just value, but it turns out is not unique amongst decorated props. + * (it appears to be run once per class definition, and thus multiple instances would share the same value-reference) */ export function State() { return function decorator(target){ let key = target.key; let descriptor = target.descriptor; - let value = undefined; + let valueMap = new WeakMap(); let {get: oldGet, set: oldSet} = descriptor; // Add a getter/setter or replace if they're already there with something that intercepts it (this gets a whole lot easyer in the new proposal if i'm not mistaken) descriptor['get'] = oldGet || (function(){ - return value + return valueMap.get(this) }); descriptor['set'] = function(newValue){ - if(newValue!==descriptor.get.call(this)){ - value = newValue; + let oldValue = descriptor.get.call(this); + if(newValue!==oldValue){ + valueMap.set(this,newValue); this.markDirty && this.markDirty(); + } if(oldSet) return oldSet.call(this, newValue); }; @@ -29,7 +33,7 @@ export function State() { // CAUTION: this is again dangerous, the initialize function should be called right before the constructor, but after it was fully defined. if(target.initializer){ - value = target.initializer(target); + valueMap.set(target, target.initializer(target)); delete target.initializer; } @@ -59,7 +63,7 @@ export class CustomElement extends HTMLElement { let newVNode = this.render(); render(newVNode, { host: this, - oldVNode: this.#renderedVNode, + old: this.#renderedVNode, }); this.#renderedVNode = newVNode; } diff --git a/packages/csx-custom-elements/src/vdom/as-vnode.js b/packages/csx-custom-elements/src/vdom/as-vnode.js new file mode 100644 index 0000000..2fd69ea --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/as-vnode.js @@ -0,0 +1,17 @@ +import './types'; + +/** + * This exists as a very basic example/test for JSX-to-DOM. + * + * The custom babel-plugin-transform-csx-jsx removes the need for this, only use asVNode when using with the default + * transform-react plugin of babel + * + * @param {VNodeType} type + * @param {VNodeProps} props + * @param {VNodeChildren} children + * @return {VNode} + */ +export function asVNode(type, props, ...children) { + let vnode = {type, props, children}; + return vnode; +} \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/constants.js b/packages/csx-custom-elements/src/vdom/constants.js deleted file mode 100644 index 0ff4d60..0000000 --- a/packages/csx-custom-elements/src/vdom/constants.js +++ /dev/null @@ -1,21 +0,0 @@ - -export const VNODE_EXCLUDE = { - [null]: true, - [undefined]: true -}; - -// Keys of a Element to be set directly rather than using setAttribute -export const VNODEPROP_DIRECT = { - ['checked']: true -}; -export const VNODEPROP_EXCLUDE_DIRECT = { - ['style']: true, - ['class']: true, -}; - -export const VNODEPROP_IGNORE = { - ['key']: true, -}; - -export const Host = Symbol('host'); -export const ShadowDOM = Symbol('shadow-dom'); \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/index.js b/packages/csx-custom-elements/src/vdom/index.js index 22ef628..4038113 100644 --- a/packages/csx-custom-elements/src/vdom/index.js +++ b/packages/csx-custom-elements/src/vdom/index.js @@ -1,3 +1,4 @@ -export * from "./vnode"; -export * from "./render"; -export {Host, ShadowDOM} from "./constants"; \ No newline at end of file +export {asVNode} from "./as-vnode"; +export {render} from "./render"; +export {Host} from "./renderers/hostnode"; +export {ShadowDOM} from "./renderers/shadownode"; \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/render.js b/packages/csx-custom-elements/src/vdom/render.js index 47fbd09..952e4df 100644 --- a/packages/csx-custom-elements/src/vdom/render.js +++ b/packages/csx-custom-elements/src/vdom/render.js @@ -1,201 +1,238 @@ -import './vnode'; +import './types'; import { - Host, ShadowDOM, - VNODE_EXCLUDE, - VNODEPROP_DIRECT, VNODEPROP_EXCLUDE_DIRECT, - VNODEPROP_IGNORE, -} from "./constants"; + HostNodeRenderer, Host, + ShadowNodeRenderer, ShadowDOM, + PrimitiveRenderer, Primitive, + NodeTreeRenderer +} from "./renderers"; + +// TODO consider using existence of renderer.remove to identify whether a VNode is ChildNode or not +// TODO Rework all below so we can determine per type how to handle it (not all represent a childNode of the host etc...) +// TODO Element renderer (for things that are already DOM-elements) +export function getNodeMeta(vnode){ + if(vnode===undefined||vnode===null) return undefined; // Indicate it shouldn't render + 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: type}; +} + /** * @typedef {Object} RenderOptions * @category VDOM - * @property {Element} host - A host element to update to the specified VDOM - * TODO: Other options clearly... + * @property {Element} [host] - A host 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?) */ +/** + * Temporary data structure for listing an old VNode + * @typedef VOldQueueItem + * @interface + * @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 + **/ + + /** * This exists as a very basic example/test for JSX-to-DOM + * @category VDOM * @param {VNode} vnode * @param {RenderOptions} opts * @return {Element} */ export function render(vnode, opts = {}) { - // TODO figure out how to handle events (its not that easy to create (click)={this.onClick} or something, that is not supported by the @babel/parser and we'd have to fork it.. - // --> We've got a basic onClick (react-style) system set up now - // TODO innerHTML, innerText and other tags/props that are trickyer then just mapping value to attribute + // TODO innerHTML, innerText and other tags/props that are trickyer then just mapping value to attribute (see NodeTreeRenderer) // TODO ref-prop (should it only return once all child els are created and appended to the child?!) - // TODO Proper updating of a previous rendered vnode + // TODO Proper updating of a previous rendered vnode (we're working on it!) + + /** + * + * @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) + }] + }; - let queue = [{vnode, opts, parent: null}]; let newRoot = undefined; - while(queue.length>0){ - let {vnode, opts, parent} = queue.splice(0,1)[0]; - let { - /** - * @type {Element} - */ - host, - /** - * @type {VNode} - */ - oldVNode - } = opts; - - if(VNODE_EXCLUDE[vnode]) continue;// Skip - - if(!['object', 'function', 'symbol'].includes(typeof(vnode))) { - // Presumed primitive type -> TEXT - if(!host) { - host = document.createTextNode(vnode); - }else{ - host.data = vnode; - } - }else if(vnode?.type === ShadowDOM) { - // ShadowDOM-keyword -> SHADOW - if(!parent.node) { - throw new Error("Can't identify to what element ShadowDOM is to be attached"); - } - if(!host) { - host = parent.node.attachShadow({ mode: 'open' }); - }else{ - host = parent.node.shadowRoot; - } - }else if(vnode?.type === Host){ - // Host-keyword -> PARENT - let shadowParent = parent?.node || host; - if(!shadowParent) { - throw new Error("Can't identify to what element ShadowDOM is to be attached"); - } - host = shadowParent; - }else if(!host){ - if(typeof(vnode?.type) === 'string'){ - // String-type -> DOM - host = document.createElement(vnode.type); - }else if(vnode?.type?.tagName){ - // Object-type -> CUSTOM-ELEMENT - host = document.createElement(vnode.type.tagName); - }else{ - throw new Error("Unrecognized vnode type", vnode); - } + while(state.queue.length>0){ + let {item, meta, previous} = state.queue.splice(0,1)[0]; + let renderer = meta.renderer; + if(!renderer) throw new Error("No renderer for vnode", item.vnode); + + // Create the element if no matching existing element was set + let newlyCreated = false; + if (!item.host) { + item.host = renderer.create(item); + newlyCreated = true; } - - // Props - if (vnode?.props) { - let props = vnode.props; - let oldProps = oldVNode?.props || {}; - let propOps = [ - ...Object.entries(oldProps).filter(([key,val])=>!props.hasOwnProperty(key)).map(([key,value])=>({delete:1, key, value})), - ...Object.entries(props).map(([key,value])=>({delete:1, key, value, prevValue: oldProps[key]})) - ]; + + // Update the element + renderer.update(item); + + // Update children + if(item.vnode?.children || item.old?.children) { + let childTypes = new Set(); - for(let {deleted, key, value, prevValue} of propOps){ - if(VNODEPROP_IGNORE[key]){ - // NO-OP - }else if(VNODEPROP_DIRECT[key]){ - host[key] = value; - }else{ - if(value===prevValue) { - // NO-OP, hasn't changed - }else if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){ - host[key] = value; - }else if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){ - // Convert camelCase to dash-case - let eventName = key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}); - if(deleted) { - host.removeEventListener(eventName, value); - }else if(value instanceof Function){ - host.addEventListener(eventName, value); - }else{ - new Error("Unsupported event-handler"); - } - }else { - if(deleted){ - host.removeAttribute(key); - }else{ - if (value === false || value===null || value==='') { - host.removeAttribute(key); - } else if (value === true) { - host.setAttribute(key, ""); - } else { - host.setAttribute(key, value); - } - } - } - } - } - // if (props.style && typeof (props.style) === 'object') { - // for (let styleKey in props.style) { - // host.style[ styleKey ] = props.style[ styleKey ]; - // } - // } - } - - // Children - if (vnode?.children) { - // TODO: Properly iterate the children update VNode only where necessary (we'll probably just reach another point where we throw the whole thing upside down again...) - let childVQueue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children]; - let oldChildVQueue = oldVNode?.children instanceof Array? oldVNode.children.slice() - : oldVNode?.children!==null&&oldVNode?.children!==undefined? [oldVNode.children] : []; - let childQueue = []; - let oldChildQueue = []; - let hostIndex = 0; - while(childVQueue.length){ - let child = childVQueue.splice(0,1)[0]; - let oldChild = oldChildVQueue.splice(0,1)[0]; - if(child instanceof Array){ - childVQueue.splice(0,0,...child); - if(oldChild instanceof Array){ - // here we typically know we should be checking Refs - oldChildVQueue.splice(0,0,...oldChild); - } - }else{ - // Really dirty way here to pass in what was probably the old element for this, and it must absolutely go - if(oldChild === child){ - console.log("Not changed, skipping"); - hostIndex++; - }else{ - let oldChildEl = oldVNode?host.childNodes[hostIndex]:null; - - console.log("Updating:", child, oldChild); - - let updateNode = (oldChild?.type === child?.type); - if(updateNode){ - hostIndex++; - } - - childQueue.push({ - vnode: child, - opts: { - ...opts, - host: updateNode? oldChildEl : undefined, - oldVNode: updateNode? oldChild : undefined + // 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.> } + */ + 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);// Tract 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 }, - parent: { // TODO specify a previous child or index here if need be... - node:host, - - // Really dirty way here to pass in what was probably the old element for this, and it must absolutely go - oldChild: oldChildEl - } + meta: meta }); } } } - queue.splice(0,0,...childQueue); + + // Flatten and organize old-children + /** + * @type { Object.> } + */ + 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);// Tract that children of this type exist and should be iterated later + oldVChildren[childType] = oldVChildren[childType] || []; // Make sure the array exists + oldVChildren[childType].push({ + vnode: next, + element: childElement, + meta: meta + }); + } + } + } + + + + let sortedChildTypes = Array.from(childTypes).sort((a,b)=>a==='node'?1:-1); // Always do ChildNode-types last + let queuedItems = []; + let previous = null; + for(let childType of sortedChildTypes){ + let newChildren = vChildren[childType]; + let oldChildren = oldVChildren[childType]; + + while(newChildren && newChildren.length){ + let child = newChildren.splice(0,1)[0]; + let oldChild = oldChildren && oldChildren.splice(0,1)[0]; + + child.previous = previous; + if(oldChild && child.meta.normedType === oldChild.meta.normedType){ + // Update old-child + child.item.host = oldChild.element; + child.item.old = oldChild.vnode; + queuedItems.push(child); + }else{ + // New child + if(oldChild){ + if(oldChild.meta.renderer.remove) + oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element }); + else + item.host.removeChild(oldChild.element); + } + queuedItems.push(child);// TODO where should the new child be inserted... + } + previous = child.item; + } + while(oldChildren && oldChildren.length){ + let oldChild = oldChildren.splice(0,1)[0]; + if(oldChild.meta.renderer.remove) + oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element }); + else + item.host.removeChild(oldChild.element); + } + } + + state.queue.splice(0, 0, ...queuedItems); } - if(parent){ - if(parent.oldChild){ - if(parent.oldChild !== host) { - console.log(parent.node, host); - parent.node.replaceChild(host, parent.oldChild); + 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); + } + //item.parent.appendChild(item.host); } - }else{ - parent.node.appendChild(host); } - }else{ - newRoot = host; + if(!item.parent) newRoot = item.host; + } + + // //if(item.host instanceof Element || item.host instanceof Text){// Not good enough, both Element and Text inherit from Node, which would make more sense, but ShadowRoot also inherits from node, and unlike the others it shouldnt be appended as a child-element + // if(item.host instanceof ShadowRoot){ + // console.log("I expected a crash here...", item); + // } + // if(item.host instanceof Node){ + // let parent = item.parent; + // if(parent){ + // //parent.insertBefore(nextChildNode, childNodes[i])// When its not the last el... + // parent.appendChild(item.host); + // }else{ + // newRoot = item.host;// This could be implemented as refs... + // } + // } + // if(item.host instanceof ShadowRoot){ + // //console.log("Does this work then?..", item); + // // item.parent.removeChild(item.host);// No it does not! + // } } - return newRoot; -} \ No newline at end of file +} diff --git a/packages/csx-custom-elements/src/vdom/renderers/hostnode.js b/packages/csx-custom-elements/src/vdom/renderers/hostnode.js new file mode 100644 index 0000000..48401c5 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/renderers/hostnode.js @@ -0,0 +1,37 @@ +import '../types'; +import {NodeTreeRenderer} from "./nodetree"; + +export const Host = Symbol('Host'); + +/** + * Takes care of rendering a Host-node + * + * @class + * @implements {VNodeRenderer} + */ +export const HostNodeRenderer = { + /** + * @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; + }, + + /** + * @param {VRenderItem} item + */ + remove(item){ + // NO-OP + }, + + /** + * + * @param {VRenderItem} item + * @param {VRenderState} state + */ + update(item, state){ + item.host = item.host || item.parent; + NodeTreeRenderer.update(item,state); + }, +}; \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/renderers/index.js b/packages/csx-custom-elements/src/vdom/renderers/index.js new file mode 100644 index 0000000..cc23283 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/renderers/index.js @@ -0,0 +1,4 @@ +export * from "./hostnode"; +export * from "./nodetree"; +export * from "./nodeprimitive"; +export * from "./shadownode"; \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/renderers/nodeprimitive.js b/packages/csx-custom-elements/src/vdom/renderers/nodeprimitive.js new file mode 100644 index 0000000..49587f9 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/renderers/nodeprimitive.js @@ -0,0 +1,29 @@ +import '../types'; + +export const Primitive = Symbol("primitive"); + +/** + * Takes care of rendering a Primitive-type (text, boolean, number, ...) + * + * @class + * @implements {VNodeRenderer} + */ +export const PrimitiveRenderer = { + /** + * @param {VRenderItem} item + */ + create(item){ + return item.document.createTextNode(item.vnode); + }, + + /** + * @param {VRenderItem} item + */ + update(item){ + /** + * @type {Text} + */ + let host = item.host; + host.data = item.vnode; + } +}; diff --git a/packages/csx-custom-elements/src/vdom/renderers/nodetree.js b/packages/csx-custom-elements/src/vdom/renderers/nodetree.js new file mode 100644 index 0000000..7393db4 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/renderers/nodetree.js @@ -0,0 +1,106 @@ +import '../types'; + +// Keys of a Element to be set directly rather than using setAttribute +const VNODEPROP_DIRECT = { + //['checked']: true NOT NEEDED! +}; +const VNODEPROP_EXCLUDE_DIRECT = { + ['style']: true, + ['class']: true, +}; +const VNODEPROP_IGNORE = { + ['key']: true, +}; + +/** + * Takes care of rendering a typical VNode (like div, span or any custom-element) + * + * @class + * @implements {VNodeRenderer} + */ +export const NodeTreeRenderer = { + /** + * + * @param {VRenderItem} item + */ + create(item){ + let vnode = item.vnode; + if(typeof(vnode.type) === 'string'){ + // String-type -> DOM + return item.document.createElement(vnode.type); + }else if(vnode.type?.tagName){ + // Object-type -> CUSTOM-ELEMENT + return item.document.createElement(vnode.type.tagName); + }else{ + throw new Error("Unrecognized vnode type", vnode); + } + }, + + /** + * @param {VRenderItem} item + */ + update(item){ + let vnode = item.vnode; + /** + * @type {VNodeProps} + */ + let props = vnode?.props || {}; + /** + * @type {VNodeProps} + */ + let oldProps = item.old?.props || {}; + let host = item.host; + + // Diff the props + let propDiffs = []; + for(let key in oldProps){ + let oldVal = oldProps[key]; + if(!props.hasOwnProperty(key)){ + propDiffs.push([key, undefined, oldVal]); + }else{ + let newVal = props[key]; + if(oldVal!==newVal){ + propDiffs.push([key, newVal, oldVal]); + } + } + } + for(let key in props){ + let newVal = props[key]; + if(!oldProps.hasOwnProperty(key)){ + propDiffs.push([key,newVal, undefined]); + } + } + + // Now apply each + for(let [key, newVal, oldVal] of propDiffs){ + if(VNODEPROP_IGNORE[key]){ + // Prop to be ignored (like 'key') + }else if(VNODEPROP_DIRECT[key]){ + // Direct-value prop only (e.g. checked attribute of checkbox will reflect in attributes automatically, no need to set the attribute..) + host[key] = newVal; + }else if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){ + // Event-prop + // Convert event name from camelCase to dash-case (this means that this on syntax might not be able to cover all custom-events) + let eventName = key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}); + if(!newVal){ + host.removeEventListener(eventName, oldVal); + }else{ + host.addEventListener(eventName, newVal); + } + // TODO might want to support objects for defining events, so we can specifiy passive or not, and other event options + }else{ + if(!VNODEPROP_EXCLUDE_DIRECT[key]){ + // Unless otherwise excluded, set the prop directly on the Element as well (because this is what we'd typically want to do passing complex objects into custom-elements) + host[key] = newVal; + } + if(newVal===undefined || newVal === false || newVal===null || newVal===''){ + host.removeAttribute(key); + }else if(newVal === true){ + host.setAttribute(key, ""); + }else{ + host.setAttribute(key, newVal); + } + } + } + } +}; diff --git a/packages/csx-custom-elements/src/vdom/renderers/shadownode.js b/packages/csx-custom-elements/src/vdom/renderers/shadownode.js new file mode 100644 index 0000000..c9ff0d7 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/renderers/shadownode.js @@ -0,0 +1,33 @@ +import '../types'; + +export const ShadowDOM = Symbol('ShadowDOM'); + +/** + * Takes care of rendering a ShadowDOM-node + * + * @class + * @implements {VNodeRenderer} + */ +export const ShadowNodeRenderer = { + /** + * @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) + }, + + /** + * @param {VRenderItem} item + */ + remove(item){ + // TODO there is no detachShadow function provided by the DOM, how would one ever remove a shadowRoot?? + }, + + /** + * @param {VRenderItem} item + */ + update(item) { + item.host = item.host || item.parent.shadowRoot; + }, +}; diff --git a/packages/csx-custom-elements/src/vdom/types/index.js b/packages/csx-custom-elements/src/vdom/types/index.js new file mode 100644 index 0000000..3d32ca6 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/types/index.js @@ -0,0 +1,4 @@ +export * from "./vnode"; +export * from "./render-item"; +export * from "./render-state"; +export * from "./vnode-renderer"; diff --git a/packages/csx-custom-elements/src/vdom/types/render-item.js b/packages/csx-custom-elements/src/vdom/types/render-item.js new file mode 100644 index 0000000..7656d09 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/types/render-item.js @@ -0,0 +1,14 @@ +import './vnode'; + +/** + * Per node rendering-state when rendering a tree of VNodes + * @typedef VRenderItem + * @interface + * @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 + **/ diff --git a/packages/csx-custom-elements/src/vdom/types/render-state.js b/packages/csx-custom-elements/src/vdom/types/render-state.js new file mode 100644 index 0000000..0ca42db --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/types/render-state.js @@ -0,0 +1,36 @@ +import "./render-item"; +import "./vnode-renderer"; +import "./vnode"; + +// Note: This type is not meant to be public + +/** + * Per node rendering-state when rendering a tree of VNodes + * @typedef VRenderQueueItemMetadata + * @interface + * @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. + * // TODO positional info.. + **/ + +/** + * Per node rendering-state when rendering a tree of VNodes + * @typedef VRenderQueueItem + * @interface + * @category VDOM.renderer + * @property {VRenderItem} item - The item to queue 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 + **/ + + +/** + * Global rendering-state when rendering a tree of VNodes + * @typedef VRenderState + * @interface + * @category VDOM.renderer + * @property {Array.} queue - The queue of items to be rendered + * @property {Array.<[Function,Element]>} refs - Ref-callback functions be called when rendering is done + * @property {Map.} keyedElements - A map of keyed elements (TODO this needs refining) + **/ diff --git a/packages/csx-custom-elements/src/vdom/types/vnode-renderer.js b/packages/csx-custom-elements/src/vdom/types/vnode-renderer.js new file mode 100644 index 0000000..58f2cff --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/types/vnode-renderer.js @@ -0,0 +1,31 @@ +import "./render-item";// Info about what we're rendering and where to + +// 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#create + * @param {VRenderItem} item + * @returns {Element} + */ + +/** + * This method updates the element corresponding to a vnode + * @method + * @name VNodeRenderer#update + * @param {VRenderItem} item + */ + +/** + * This method removes the element corresponding to a vnode + * @method + * @name VNodeRenderer#remove + * @param {VRenderItem} item + */ \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/types/vnode.js b/packages/csx-custom-elements/src/vdom/types/vnode.js new file mode 100644 index 0000000..6c622e8 --- /dev/null +++ b/packages/csx-custom-elements/src/vdom/types/vnode.js @@ -0,0 +1,39 @@ +/** + * A basic virtual-node representing a primitive type (typically text-nodes) + * @typedef {(string|number)} VNodePrimitive + * @category VDOM + */ + +/** + * Type of a virtual node, this is usally the string-tag but may refer to the CustomElement class, or a function + * @typedef {string|null|Object|Function} VNodeType + * @category VDOM + **/ + +/** + * Properties of a virtual-node. + * @typedef {Object.} VNodeProps + * @category VDOM + **/ + +/** + * A tree of virtual-nodes (e.g, type,props,attr and nested children) + * @typedef VNodeTree + * @interface + * @category VDOM + * @property {VNodeType} type - TagName or CustomElement of the html-element + * @property {VNodeProps} props - Properties to set on the element + * @property {VNodeChildren} children - Children of the element + **/ + +/** + * Children a VNode tree (these may contain nested VNode-arrays) + * @typedef {Array.} VNodeChildren + * @category VDOM + **/ + +/** + * Any virtual-node that may be rendered to DOM + * @typedef {VNodeTree|VNodePrimitive|undefined|Element} VNode + * @category VDOM + **/ \ No newline at end of file diff --git a/packages/csx-custom-elements/src/vdom/vnode.js b/packages/csx-custom-elements/src/vdom/vnode.js deleted file mode 100644 index 30006c0..0000000 --- a/packages/csx-custom-elements/src/vdom/vnode.js +++ /dev/null @@ -1,44 +0,0 @@ - -/** - * Type of a node, this is usally the string-tag, but when a CustomElement was created using the @CustomElement annotation the class itself may be used - * @typedef {string|null|Component} VNodeType - * @category VDOM - **/ - -/** - * Properties of a node - * @typedef {Object.} VNodeProps - * @category VDOM - **/ - -/** - * Children of a node - * @typedef {VNode|Element|Array.} VNodeChildren - * @category VDOM - **/ - -/** - * @typedef VNode - * @interface - * @category VDOM - * @property {VNodeType} type - TagName or CustomElement of the html-element - * @property {VNodeProps} props - Properties to set on the element - * @property {VNodeChildren} children - Children of the element - **/ - -/** - * This exists as a very basic example/test for JSX-to-DOM. - * - * The custom babel-plugin-transform-csx-jsx removes the need for this, only use asVNode when using with the default - * transform-react plugin of babel - * - * @param {VNodeType} type - * @param {VNodeProps} props - * @param {VNodeChildren} children - * @return {VNode} - */ -export function asVNode(type, props, ...children) { - let vnode = {type, props, children}; - console.log(vnode); - return vnode; -} \ No newline at end of file diff --git a/test/todos-mvc/components/my-todo.jsx b/test/todos-mvc/components/my-todo.jsx index 4c43b94..951eed3 100644 --- a/test/todos-mvc/components/my-todo.jsx +++ b/test/todos-mvc/components/my-todo.jsx @@ -7,11 +7,20 @@ import {TodoItem} from './todo-item'; @defineElement('my-todo') export class MyTodo extends CustomElement{ uid = 1; - // @State Won't work; - @State() todos = [ - {id: this.uid++, text: "my initial todo", checked: false }, - {id: this.uid++, text: "Learn about Web Components", checked: false }, - ]; + @State() todos; + // = [ + // {id: this.uid++, text: "my initial todo", checked: false }, + // {id: this.uid++, text: "Learn about Web Components", checked: false }, + // ]; + + constructor(){ + super(); + this.uid = 1; + this.todos = [ + {id: this.uid++, text: "my initial todo", checked: false }, + {id: this.uid++, text: "Learn about Web Components", checked: false }, + ] + } render(){ return ( @@ -27,7 +36,8 @@ export class MyTodo extends CustomElement{ {this.todos.map(item => { item.text } @@ -44,12 +54,14 @@ export class MyTodo extends CustomElement{ this.todos = [...this.todos, { id: this.uid++, text, checked: false }]; } }; - handleCheck = ({detail: checked}, id) => { + handleCheck = ({detail: {checked,id}}) => { let indexOf = this.todos.findIndex(t=>t.id===id); - let updated = {...this.todos[indexOf], checked}; - this.todos = [...this.todos.slice(0,indexOf), updated, ...this.todos.slice(indexOf+1)]; + if(indexOf>=0) { + let updated = { ...this.todos[ indexOf ], checked }; + this.todos = [...this.todos.slice(0, indexOf), updated, ...this.todos.slice(indexOf + 1)]; + } }; - handleRemove = (e,id)=>{ + handleRemove = ({detail: {id}})=>{ let indexOf = this.todos.findIndex(t=>t.id===id); this.todos = [...this.todos.slice(0,indexOf), ...this.todos.slice(indexOf+1)]; } diff --git a/test/todos-mvc/components/todo-item.jsx b/test/todos-mvc/components/todo-item.jsx index 90d23e5..149f849 100644 --- a/test/todos-mvc/components/todo-item.jsx +++ b/test/todos-mvc/components/todo-item.jsx @@ -1,9 +1,11 @@ -import {defineElement, render, CustomElement, Host, ShadowDOM} from "../../../packages/csx-custom-elements"; +import {defineElement, render, CustomElement, Host, ShadowDOM, State} from "../../../packages/csx-custom-elements"; import style from './todo-item.scss'; @defineElement('todo-item') export class TodoItem extends CustomElement{ - checked = false;// TODO annotate as prop (attribute) + @State() checked = false;// TODO annotate as prop instead of state (attribute) + @State() model; // TODO annotate as prop instead of state + @State() text; render(){ return ( @@ -27,12 +29,13 @@ export class TodoItem extends CustomElement{ handleChange = ()=>{ this.dispatchEvent(new CustomEvent('check', { - detail: (this.checked=!this.checked), + detail: {checked: (this.checked=!this.checked), id: this.model}, bubbles: true })); }; handleClick = ()=>{ this.dispatchEvent(new CustomEvent('remove', { + detail: {id: this.model}, bubbles: true })); };