Saving state before severe refactoring (to support a proper update-loop and future extensibility)

This commit is contained in:
Miel Truyen 2019-10-26 23:52:38 +02:00
parent 863adb9449
commit fc527cb156
4 changed files with 108 additions and 55 deletions

View File

@ -59,7 +59,7 @@ export class CustomElement extends HTMLElement {
let newVNode = this.render(); let newVNode = this.render();
render(newVNode, { render(newVNode, {
host: this, host: this,
oldVNode: this.#renderedVNode oldVNode: this.#renderedVNode,
}); });
this.#renderedVNode = newVNode; this.#renderedVNode = newVNode;
} }

View File

@ -21,7 +21,7 @@ import {
*/ */
export function render(vnode, opts = {}) { 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.. // 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 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 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
@ -43,22 +43,36 @@ export function render(vnode, opts = {}) {
if(VNODE_EXCLUDE[vnode]) continue;// Skip 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); host = document.createTextNode(vnode);
}else if(vnode?.type === ShadowDOM) { }else{
if(!parent.node) { host.data = vnode;
throw new Error("Can't identify to what element ShadowDOM is to be attached"); }
} }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' }); host = parent.node.attachShadow({ mode: 'open' });
}else if(vnode?.type === Host){ }else{
if(!parent.node) { host = parent.node.shadowRoot;
throw new Error("Can't identify to what element ShadowDOM is to be attached"); }
} }else if(vnode?.type === Host){
host = parent.node; // Host-keyword -> PARENT
}else if(typeof(vnode?.type) === 'string'){ 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); host = document.createElement(vnode.type);
}else if(vnode?.type?.tagName){ }else if(vnode?.type?.tagName){
// Object-type -> CUSTOM-ELEMENT
host = document.createElement(vnode.type.tagName); host = document.createElement(vnode.type.tagName);
}else{ }else{
throw new Error("Unrecognized vnode type", vnode); throw new Error("Unrecognized vnode type", vnode);
@ -68,70 +82,102 @@ export function render(vnode, opts = {}) {
// Props // Props
if (vnode?.props) { if (vnode?.props) {
let props = 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 {deleted, key, value, prevValue} of propOps){
for (let styleKey in props.style) {
host.style[ styleKey ] = props.style[ styleKey ];
}
}
for (let key in props) {
let val = props[key];
if(VNODEPROP_IGNORE[key]){ if(VNODEPROP_IGNORE[key]){
// NO-OP // NO-OP
}else if(VNODEPROP_DIRECT[key]){ }else if(VNODEPROP_DIRECT[key]){
host[key] = val; host[key] = value;
}else{ }else{
if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){ if(value===prevValue) {
host[key] = val; // NO-OP, hasn't changed
} }else if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){ host[key] = value;
if(val instanceof Function){ }else if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){
host.addEventListener( // Convert camelCase to dash-case
// Convert camelCase to dash-case let eventName = key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())});
key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}), if(deleted) {
val host.removeEventListener(eventName, value);
); }else if(value instanceof Function){
host.addEventListener(eventName, value);
}else{ }else{
new Error("Unsupported event-handler"); new Error("Unsupported event-handler");
} }
}else { }else {
if (val === false || val===null || val==='') { if(deleted){
host.removeAttribute(key); host.removeAttribute(key);
} else if (val === true) { }else{
host.setAttribute(key, ""); if (value === false || value===null || value==='') {
} else { host.removeAttribute(key);
host.setAttribute(key, val); } 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 // Children
if (vnode?.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 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 childQueue = [];
let oldChildQueue = [];
let hostIndex = 0;
while(childVQueue.length){ while(childVQueue.length){
let child = childVQueue.splice(0,1)[0]; let child = childVQueue.splice(0,1)[0];
// TODO support chidl instances of Element let oldChild = oldChildVQueue.splice(0,1)[0];
if(child instanceof Array){ if(child instanceof Array){
childVQueue.splice(0,0,...child); childVQueue.splice(0,0,...child);
if(oldChild instanceof Array){
// here we typically know we should be checking Refs
oldChildVQueue.splice(0,0,...oldChild);
}
}else{ }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;
childQueue.push({ console.log("Updating:", child, oldChild);
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 let updateNode = (oldChild?.type === child?.type);
oldChild: oldVNode?host.childNodes[childQueue.length]:null 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); queue.splice(0,0,...childQueue);
@ -139,7 +185,10 @@ export function render(vnode, opts = {}) {
if(parent){ if(parent){
if(parent.oldChild){ 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{ }else{
parent.node.appendChild(host); parent.node.appendChild(host);
} }

View File

@ -28,7 +28,9 @@ export class MyTodo extends CustomElement{
<todo-item <todo-item
model={ item.id } model={ item.id }
checked={( item.checked )} checked={( item.checked )}
>{ item.text }</todo-item> >
{ item.text }
</todo-item>
)} )}
</ul> </ul>
</section> </section>

View File

@ -28,10 +28,12 @@ export class TodoItem extends CustomElement{
handleChange = ()=>{ handleChange = ()=>{
this.dispatchEvent(new CustomEvent('check', { this.dispatchEvent(new CustomEvent('check', {
detail: (this.checked=!this.checked), detail: (this.checked=!this.checked),
bubbles: true
})); }));
}; };
handleClick = ()=>{ handleClick = ()=>{
this.dispatchEvent(new CustomEvent('remove', { this.dispatchEvent(new CustomEvent('remove', {
bubbles: true
})); }));
}; };
} }