Fixed a bug relating to custom-elements's first vnode where render(){ return <div class="example" /> } would set the example-class on the custom-element itself.
Added support for className and style similar to react Cleaned up some comments Reworked how tests are built in order to add a new test "pdf" which was a small side-project where previous mentioned bug showed up, it's an example using HTML to create a PDF for printing
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
"rollup-plugin-commonjs": "latest",
|
||||
"rollup-plugin-terser": "latest",
|
||||
"rollup-plugin-json": "latest",
|
||||
"rollup-plugin-postcss": "latest",
|
||||
"npm-run-all": "latest"
|
||||
},
|
||||
"dependencies":{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {render} from "../vdom";
|
||||
import {render, Host} from "../vdom";
|
||||
|
||||
// TODO the custom-element class could be removed, and its functionality implemented through the @defineElement decorator
|
||||
// Currently there are issues: if a custom-element reimplements the connectedCallback, they need to magically know that
|
||||
@@ -23,6 +23,9 @@ export class CustomElement extends HTMLElement {
|
||||
update(){
|
||||
if (this.render) {
|
||||
let newVNode = this.render();
|
||||
if(newVNode.type !== Host){
|
||||
newVNode = {type: Host, children: newVNode instanceof Array? newVNode : [newVNode]};
|
||||
}
|
||||
render(newVNode, {
|
||||
host: this,
|
||||
old: this.#renderedVNode,
|
||||
|
||||
@@ -6,25 +6,23 @@ import {
|
||||
NodeTreeRenderer, NativeRenderer
|
||||
} from "./renderers";
|
||||
|
||||
// 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
|
||||
if(vnode instanceof Element) return {renderer: NativeRenderer, normedType: Element};
|
||||
export function getNodeMeta(vnode) {
|
||||
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
|
||||
if (vnode instanceof Element) return { renderer: NativeRenderer, normedType: Element };
|
||||
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};
|
||||
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] - A host element to update to the specified 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?)
|
||||
* @property {Element} [parent] - The parent element (TODO not sure what this will do when specified; Insert it as child element of the parent where?)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -35,9 +33,12 @@ export function getNodeMeta(vnode){
|
||||
* @return {Element}
|
||||
*/
|
||||
export function render(vnode, opts = {}) {
|
||||
// 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 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}
|
||||
@@ -48,7 +49,7 @@ export function render(vnode, opts = {}) {
|
||||
queue: [{
|
||||
// Start item
|
||||
item: {
|
||||
document: opts.document||document,
|
||||
document: opts.document || document,
|
||||
host: opts.host,
|
||||
parent: opts.parent,
|
||||
old: opts.old,
|
||||
@@ -59,23 +60,23 @@ export function render(vnode, opts = {}) {
|
||||
};
|
||||
|
||||
let newRoot = undefined;
|
||||
while(state.queue.length>0){
|
||||
let {item, meta, previous} = state.queue.splice(0,1)[0];
|
||||
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);
|
||||
|
||||
if (!renderer) throw new Error("No renderer for vnode", item.vnode);
|
||||
|
||||
// SVG handling..
|
||||
if(!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
|
||||
else if(item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
||||
if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
|
||||
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
||||
|
||||
// Create the element if no matching existing element was set
|
||||
let newlyCreated = false;
|
||||
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
|
||||
state.refs.push([item.vnode.props.ref,item.host]);
|
||||
|
||||
if (item.vnode?.props?.ref) {// If props specify a ref-function, queue it to be called at the end of the render
|
||||
state.refs.push([item.vnode.props.ref, item.host]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +84,7 @@ export function render(vnode, opts = {}) {
|
||||
renderer.update(item, meta);
|
||||
|
||||
// Update children
|
||||
if(meta.normedType!==Element && (item.vnode?.children || item.old?.children)) {
|
||||
if (meta.normedType !== Element && (item.vnode?.children || item.old?.children)) {
|
||||
let childTypes = new Set();
|
||||
|
||||
// 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)
|
||||
@@ -91,19 +92,19 @@ export function render(vnode, opts = {}) {
|
||||
* @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 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) {
|
||||
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)
|
||||
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({
|
||||
vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
|
||||
vChildren[ childType ].push({
|
||||
item: {
|
||||
...item,
|
||||
old: undefined,
|
||||
@@ -121,64 +122,64 @@ export function render(vnode, opts = {}) {
|
||||
/**
|
||||
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
|
||||
*/
|
||||
let oldVChildren = { };
|
||||
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{
|
||||
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) {
|
||||
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){
|
||||
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){
|
||||
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
|
||||
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.key,oldItem);
|
||||
oldVChildren[ childType ].push(oldItem);
|
||||
if (next.props?.key) {
|
||||
state.keyedElements.set(next.key, oldItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let sortedChildTypes = Array.from(childTypes).sort((a,b)=>a==='node'?1:-1); // Always do ChildNode-types last
|
||||
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];
|
||||
for (let childType of sortedChildTypes) {
|
||||
let newChildren = vChildren[ childType ];
|
||||
let oldChildren = oldVChildren[ childType ];
|
||||
|
||||
while(newChildren && newChildren.length){
|
||||
let child = newChildren.splice(0,1)[0];
|
||||
while (newChildren && newChildren.length) {
|
||||
let child = newChildren.splice(0, 1)[ 0 ];
|
||||
|
||||
// Key handling
|
||||
let childKey = child.item.vnode.props?.key;
|
||||
/**@type {VOldQueueItem}*/ let oldChild;
|
||||
if(childKey){
|
||||
if (childKey) {
|
||||
oldChild = state.keyedElements.get(childKey);
|
||||
if(oldChild) {
|
||||
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);
|
||||
} else {
|
||||
// Old keyed child not already in the right place
|
||||
let indexOfKeyed = oldChildren.indexOf(oldChild);
|
||||
if(indexOfKeyed) {
|
||||
if (indexOfKeyed) {
|
||||
oldChildren.splice(indexOfKeyed, 1);
|
||||
item.host.removeChild(oldChild.element);
|
||||
}
|
||||
@@ -190,32 +191,33 @@ export function render(vnode, opts = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!oldChild) oldChild = oldChildren && oldChildren.splice(0,1)[0];
|
||||
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){
|
||||
if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
|
||||
&& (child.meta.normedType !== Element || child.item.vnode === oldChild.vnode)) {
|
||||
// Update old-child
|
||||
child.item.host = oldChild.element;
|
||||
child.item.old = oldChild.vnode;
|
||||
queuedItems.push(child);
|
||||
}else{
|
||||
} else {
|
||||
// New child
|
||||
if(oldChild){
|
||||
if(oldChild.meta.renderer.remove)
|
||||
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);
|
||||
}
|
||||
if(!child.meta.renderer.remove){
|
||||
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.meta.renderer.remove)
|
||||
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);
|
||||
@@ -225,23 +227,23 @@ export function render(vnode, opts = {}) {
|
||||
state.queue.splice(0, 0, ...queuedItems);
|
||||
}
|
||||
|
||||
if(newlyCreated){
|
||||
if(!meta.renderer.remove){
|
||||
if(item.parent){
|
||||
if(!previous){
|
||||
if (newlyCreated) {
|
||||
if (!meta.renderer.remove) {
|
||||
if (item.parent) {
|
||||
if (!previous) {
|
||||
// First child
|
||||
item.parent.prepend(item.host);
|
||||
}else{
|
||||
} else {
|
||||
// Subsequent child
|
||||
previous.host.after(item.host);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!item.parent) newRoot = item.host;
|
||||
if (!item.parent) newRoot = item.host;
|
||||
}
|
||||
}
|
||||
|
||||
for(let [refCb, refItem] of state.refs){
|
||||
for (let [refCb, refItem] of state.refs) {
|
||||
refCb(refItem);
|
||||
}
|
||||
return newRoot;
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
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,
|
||||
['ref']: true
|
||||
};
|
||||
const VNODE_SPECIAL_PROPS = {
|
||||
['key']: false,
|
||||
['ref']: false,
|
||||
// TODO: className, (style), event/events, (see react-docs)
|
||||
}
|
||||
|
||||
let namespace = {
|
||||
svg: "http://www.w3.org/2000/svg"
|
||||
}
|
||||
// Plain HTML doens't need a namespace, if it did it would've shown up here..
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes care of rendering a typical VNode (like div, span or any custom-element)
|
||||
@@ -90,11 +73,14 @@ export const NodeTreeRenderer = {
|
||||
// Now apply each
|
||||
for(let [key, newVal, oldVal] of propDiffs){
|
||||
let special = VNODE_SPECIAL_PROPS[key];
|
||||
if(special === false){
|
||||
if(special === false) {
|
||||
// Ignore the prop
|
||||
}else if(vtype.props?.has && vtype.props.has(key)){
|
||||
// Registered prop of the Custom-Element
|
||||
host[key] = newVal;
|
||||
}else if(special instanceof Function){
|
||||
// Special prop
|
||||
special({host, newVal, oldVal, key});
|
||||
}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<EvenName> syntax might not be able to cover all custom-events)
|
||||
@@ -117,3 +103,58 @@ export const NodeTreeRenderer = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Keys of a Element that need special handling rather than simply using setAttribute
|
||||
const VNODE_SPECIAL_PROPS = {
|
||||
['key']: false,
|
||||
['ref']: false,
|
||||
['className']: ({host, newVal, oldVal, key})=>{
|
||||
let oldClasses = new Set();
|
||||
let newClasses = new Set();
|
||||
|
||||
if(typeof(oldVal) === 'string'){
|
||||
oldVal = oldVal.split(' ').map(x=>x.trim()).filter(x=>x);
|
||||
for(let className of oldVal){ oldClasses.add(className); }
|
||||
}else if(typeof(oldVal)==='object'){
|
||||
for(let key in oldVal){
|
||||
if(oldVal[key]) oldClasses.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof(newVal) === 'string'){
|
||||
newVal = newVal.split(' ').map(x=>x.trim()).filter(x=>x);
|
||||
for(let className of newVal){ newClasses.add(className); }
|
||||
}else if(typeof(newVal)==='object'){
|
||||
for(let key in newVal){
|
||||
if(newVal[key]) newClasses.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
host.classList.remove(...Array.from(oldClasses));
|
||||
host.classList.add(...Array.from(newClasses));
|
||||
},
|
||||
['style']: ({host, newVal, oldVal, key})=>{
|
||||
if(typeof(newVal) === 'string'){
|
||||
host.setAttribute('style', newVal);
|
||||
}else if(typeof(newVal) ==='object'){
|
||||
if(oldVal){
|
||||
for(let key in oldVal){
|
||||
if(!newVal[key]){
|
||||
host.style[key]=null;
|
||||
}
|
||||
}
|
||||
for(let key in newVal){
|
||||
host.style[key] = newVal[key];
|
||||
}
|
||||
}
|
||||
}else if(!newVal){
|
||||
host.removeAttribute('style');
|
||||
}
|
||||
},
|
||||
['events']: ({host, newVal, oldVal, key})=>{
|
||||
//TODO!!
|
||||
throw new Error("We're still planning to implement this but it hasn't been done yet!");
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user