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 e112c87..a8b7a8f 100644 --- a/packages/csx-custom-elements/src/custom-element/custom-element.js +++ b/packages/csx-custom-elements/src/custom-element/custom-element.js @@ -59,7 +59,7 @@ export class CustomElement extends HTMLElement { let newVNode = this.render(); render(newVNode, { host: this, - oldVNode: this.#renderedVNode + oldVNode: this.#renderedVNode, }); this.#renderedVNode = newVNode; } diff --git a/packages/csx-custom-elements/src/vdom/render.js b/packages/csx-custom-elements/src/vdom/render.js index f9903af..47fbd09 100644 --- a/packages/csx-custom-elements/src/vdom/render.js +++ b/packages/csx-custom-elements/src/vdom/render.js @@ -21,7 +21,7 @@ import { */ 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 + // --> 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 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 @@ -42,23 +42,37 @@ export function render(vnode, opts = {}) { } = opts; if(VNODE_EXCLUDE[vnode]) continue;// Skip - - if(!host){ - if(!['object', 'function', 'symbol'].includes(typeof(vnode))) { + + if(!['object', 'function', 'symbol'].includes(typeof(vnode))) { + // Presumed primitive type -> TEXT + if(!host) { host = document.createTextNode(vnode); - }else if(vnode?.type === ShadowDOM) { - if(!parent.node) { - throw new Error("Can't identify to what element ShadowDOM is to be attached"); - } + }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 if(vnode?.type === Host){ - if(!parent.node) { - throw new Error("Can't identify to what element ShadowDOM is to be attached"); - } - host = parent.node; - }else if(typeof(vnode?.type) === 'string'){ + }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); @@ -68,70 +82,102 @@ export function render(vnode, opts = {}) { // 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]})) + ]; - if (props.style && typeof (props.style) === 'object') { - for (let styleKey in props.style) { - host.style[ styleKey ] = props.style[ styleKey ]; - } - } - for (let key in props) { - let val = props[key]; + for(let {deleted, key, value, prevValue} of propOps){ if(VNODEPROP_IGNORE[key]){ // NO-OP }else if(VNODEPROP_DIRECT[key]){ - host[key] = val; + host[key] = value; }else{ - if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){ - host[key] = val; - } - if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){ - if(val instanceof Function){ - host.addEventListener( - // Convert camelCase to dash-case - key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}), - val - ); + 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 (val === false || val===null || val==='') { + if(deleted){ host.removeAttribute(key); - } else if (val === true) { - host.setAttribute(key, ""); - } else { - host.setAttribute(key, val); + }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]; - // TODO support chidl instances of Element + 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{ - - childQueue.push({ - vnode: child, - opts: { - ...opts, - host: undefined, - }, - 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: oldVNode?host.childNodes[childQueue.length]:null + // 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 + }, + 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 + } + }); + } } } queue.splice(0,0,...childQueue); @@ -139,7 +185,10 @@ export function render(vnode, opts = {}) { if(parent){ if(parent.oldChild){ - parent.node.replaceChild(host, parent.oldChild); + if(parent.oldChild !== host) { + console.log(parent.node, host); + parent.node.replaceChild(host, parent.oldChild); + } }else{ parent.node.appendChild(host); } diff --git a/test/todos-mvc/components/my-todo.jsx b/test/todos-mvc/components/my-todo.jsx index b793b7c..4c43b94 100644 --- a/test/todos-mvc/components/my-todo.jsx +++ b/test/todos-mvc/components/my-todo.jsx @@ -28,7 +28,9 @@ export class MyTodo extends CustomElement{ { item.text } + > + { item.text } + )} diff --git a/test/todos-mvc/components/todo-item.jsx b/test/todos-mvc/components/todo-item.jsx index e886968..90d23e5 100644 --- a/test/todos-mvc/components/todo-item.jsx +++ b/test/todos-mvc/components/todo-item.jsx @@ -28,10 +28,12 @@ export class TodoItem extends CustomElement{ handleChange = ()=>{ this.dispatchEvent(new CustomEvent('check', { detail: (this.checked=!this.checked), + bubbles: true })); }; handleClick = ()=>{ this.dispatchEvent(new CustomEvent('remove', { + bubbles: true })); }; } \ No newline at end of file