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();
render(newVNode, {
host: this,
oldVNode: this.#renderedVNode
oldVNode: this.#renderedVNode,
});
this.#renderedVNode = newVNode;
}

View File

@ -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
@ -43,22 +43,36 @@ export function render(vnode, 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{
// 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({
vnode: child,
opts: {
...opts,
host: undefined,
},
parent: { // TODO specify a previous child or index here if need be...
node:host,
console.log("Updating:", child, oldChild);
// 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
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);
}

View File

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

View File

@ -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
}));
};
}