Compare commits

...

4 Commits

21 changed files with 664 additions and 197 deletions

View File

@ -2,7 +2,7 @@
"presets": [ "presets": [
], ],
"plugins": [ "plugins": [
[ "@babel/plugin-proposal-decorators", { "legacy": true }], [ "@babel/plugin-proposal-decorators" , { "decoratorsBeforeExport": true }],
[ "@babel/plugin-proposal-class-properties", { "loose": true } ], [ "@babel/plugin-proposal-class-properties", { "loose": true } ],
[ "@babel/plugin-proposal-private-methods", {"loose": true } ], [ "@babel/plugin-proposal-private-methods", {"loose": true } ],
[ "@babel/plugin-proposal-optional-chaining" ], [ "@babel/plugin-proposal-optional-chaining" ],

View File

@ -1,5 +1,47 @@
import {render} from "../vdom"; 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 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 valueMap.get(this)
});
descriptor['set'] = function(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);
};
// CAUTION: this is dangerous. We need intend to conver regular fields to get/set methods here.
delete descriptor.writable;
target.kind = 'method'; // update to get and set if need be..
// CAUTION: this is again dangerous, the initialize function should be called right before the constructor, but after it was fully defined.
if(target.initializer){
valueMap.set(target, target.initializer(target));
delete target.initializer;
}
return target;
}
}
/** /**
* This CustomElement class is to avoid having to do an ugly workaround in every custom-element: * This CustomElement class is to avoid having to do an ugly workaround in every custom-element:
* Which would be replacing 'HTMLElement' with '(class extends HTMLElement{})' * Which would be replacing 'HTMLElement' with '(class extends HTMLElement{})'
@ -8,15 +50,29 @@ import {render} from "../vdom";
*/ */
export class CustomElement extends HTMLElement { export class CustomElement extends HTMLElement {
connectedCallback() { connectedCallback() {
if(this.render){ this.update();
let newVNode = this.render();
render(newVNode, {
host: this
});
}
} }
disconnectedCallback(){ disconnectedCallback(){
} }
#markedDirty;
#renderedVNode;
update(){
if (this.render) {
let newVNode = this.render();
render(newVNode, {
host: this,
old: this.#renderedVNode,
});
this.#renderedVNode = newVNode;
}
this.#markedDirty=false;
}
markDirty() {
if (!this.#markedDirty) {
this.#markedDirty = requestAnimationFrame(() => this.update());
}
}
} }

View File

@ -1,18 +1,22 @@
/** /**
* The decorators proposal has changed since @babel implemented it. This code will need to change at some point... * The decorators proposal has changed since @babel implemented it. This code will need to change at some point...
*/ */
export function defineElement(tagName, options) { export function defineElement(tagName, options) {
return function decorator(target){ return function decorator(target) {
// Register the tagName as a custom-element with the browser // Queue defining element in a finisher, because apparantly thats how the non-legacy decorator proposal works (again, new proposal will be different...)
window.customElements.define(tagName, target, options); target.finisher = (finishedTarget)=>{
// Register the tagName as a custom-element with the browser
// Define the chosen tagName on the class itself so our vdom.render-function knows what DOM-Element to create window.customElements.define(tagName, finishedTarget, options);
Object.defineProperty(target, 'tagName', {
value: tagName, // Define the chosen tagName on the class itself so our vdom.render-function knows what DOM-Element to create
writable: false, Object.defineProperty(finishedTarget, 'tagName', {
enumerable: false, value: tagName,
configurable: false writable: false,
}); enumerable: false,
} configurable: false
} });
return finishedTarget;
};
return target;
};
}

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from "./vnode"; export {asVNode} from "./as-vnode";
export * from "./render"; export {render} from "./render";
export {Host, ShadowDOM} from "./constants"; export {Host} from "./renderers/hostnode";
export {ShadowDOM} from "./renderers/shadownode";

View File

@ -1,119 +1,217 @@
import './vnode'; import './types';
import { import {
Host, ShadowDOM, HostNodeRenderer, Host,
VNODE_EXCLUDE, ShadowNodeRenderer, ShadowDOM,
VNODEPROP_DIRECT, VNODEPROP_EXCLUDE_DIRECT, PrimitiveRenderer, Primitive,
VNODEPROP_IGNORE, NodeTreeRenderer
} from "./constants"; } 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};
}
// This is copied from the blog sample right now. it should process jsx but it aint what it needs to be
/** /**
* @typedef {Object} RenderOptions * @typedef {Object} RenderOptions
* @category VDOM * @category VDOM
* @property {Element} host - A host element to update to the specified VDOM * @property {Element} [host] - A host element to update to the specified VDOM
* TODO: Other options clearly... * @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 * This exists as a very basic example/test for JSX-to-DOM
* @category VDOM
* @param {VNode} vnode * @param {VNode} vnode
* @param {RenderOptions} opts * @param {RenderOptions} opts
* @return {Element} * @return {Element}
*/ */
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 innerHTML, innerText and other tags/props that are trickyer then just mapping value to attribute (see NodeTreeRenderer)
// ---> 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: Replace how this works into a queue instead of a recursive call, also consider changing JSX to support (changed)="..." notation
// 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?!)
let { // TODO Proper updating of a previous rendered vnode (we're working on it!)
/**
* @type {Element}
*/
host
} = opts;
if(VNODE_EXCLUDE[vnode]) return undefined; /**
*
* @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)
}]
};
if(!host){ let newRoot = undefined;
if(!['object', 'function', 'symbol'].includes(typeof(vnode))){ while(state.queue.length>0){
host = document.createTextNode(vnode); let {item, meta, previous} = state.queue.splice(0,1)[0];
}else if(typeof(vnode?.type) === 'string'){ let renderer = meta.renderer;
host = document.createElement(vnode.type); if(!renderer) throw new Error("No renderer for vnode", item.vnode);
}else if(vnode?.type?.tagName){
host = document.createElement(vnode.type.tagName); // Create the element if no matching existing element was set
}else{ let newlyCreated = false;
throw new Error("Unrecognized vnode type", vnode); if (!item.host) {
item.host = renderer.create(item);
newlyCreated = true;
} }
}
// Update the element
// Props renderer.update(item);
if (vnode?.props) {
if (vnode.props.style && typeof (vnode.props.style) === 'object') { // Update children
for (let styleKey in vnode.props.style) { if(item.vnode?.children || item.old?.children) {
host.style[ styleKey ] = vnode.props.style[ styleKey ]; 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)
for (let key in vnode.props) { /**
let val = vnode.props[key]; * @type { Object.<VNodeType, Array.<VRenderQueueItem>> }
if(VNODEPROP_IGNORE[key]){ */
// NO-OP let vChildren = {};
}else if(VNODEPROP_DIRECT[key]){ let queue = (item.vnode?.children||[]).slice();
host[key] = val; while(queue.length>0){
}else{ let next = queue.splice(0,1)[0];
if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){ if(next instanceof Array) queue.splice(0,0,...next);
host[key] = val; else{
} let meta = getNodeMeta(next);
if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){ if(meta && meta.renderer) {
if(val instanceof Function){ // Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
host.addEventListener( let childType = meta.normedType;
// Convert camelCase to dash-case 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)
key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}), childTypes.add(childType);// Tract that children of this type exist and should be iterated later
val vChildren[childType] = vChildren[childType] || []; // Make sure the array exists
); vChildren[childType].push({
}else{ item: {
new Error("Unsupported event-handler"); ...item,
} old: undefined,
vnode: next,
}else { host: undefined,
if (val === false || val===null || val==='') { parent: item.host
host.removeAttribute(key); },
} else if (val === true) { meta: meta
host.setAttribute(key, ""); });
} else {
host.setAttribute(key, val);
} }
} }
} }
}
} // Flatten and organize old-children
/**
// Children * @type { Object.<VNodeType, Array.<VOldQueueItem>> }
if (vnode?.children) { */
let queue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children]; let oldVChildren = { };
while(queue.length){ let curElement = item.host.firstChild;
let child = queue.splice(0,1)[0]; queue = (item.old?.children || []).slice();
if(child instanceof Array){ while(queue.length>0){
queue.splice(0,0,...child); let next = queue.splice(0,1)[0];
}else{ if(next instanceof Array) queue.splice(0,0,...next);
if(child?.type === ShadowDOM){ else{
let shadow = host.attachShadow({mode: 'open'}); let meta = getNodeMeta(next);
render({children: child.children}, { if(meta && meta.renderer) {
...opts, // Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
host: shadow let childType = meta.normedType;
}); let childElement;
}else{ if(!meta.renderer.remove){
let el = child instanceof Element? child : render(child, { childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
...opts, if(curElement){
host: undefined childElement = curElement;
}); curElement = curElement.nextSibling;
if(el!==undefined) host.appendChild(el); }
}
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(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);
}
}
}
if(!item.parent) newRoot = item.host;
} }
} }
return newRoot;
return host; }
}

View File

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

View File

@ -0,0 +1,4 @@
export * from "./hostnode";
export * from "./nodetree";
export * from "./nodeprimitive";
export * from "./shadownode";

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export * from "./vnode";
export * from "./render-item";
export * from "./render-state";
export * from "./vnode-renderer";

View File

@ -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
**/

View File

@ -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.<VRenderQueueItem>} queue - The queue of items to be rendered
* @property {Array.<[Function,Element]>} refs - Ref-callback functions be called when rendering is done
* @property {Map.<string, VNode>} keyedElements - A map of keyed elements (TODO this needs refining)
**/

View File

@ -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
*/

View File

@ -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.<string, any>} 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.<VNode|VNodeChildren>} VNodeChildren
* @category VDOM
**/
/**
* Any virtual-node that may be rendered to DOM
* @typedef {VNodeTree|VNodePrimitive|undefined|Element} VNode
* @category VDOM
**/

View File

@ -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.<string, any>} VNodeProps
* @category VDOM
**/
/**
* Children of a node
* @typedef {VNode|Element|Array.<VNode|Element>} 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;
}

View File

@ -7,7 +7,7 @@
}] }]
], ],
"plugins": [ "plugins": [
[ "@babel/plugin-proposal-decorators", { "legacy": true }], [ "@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }],
[ "@babel/plugin-proposal-class-properties", { "loose": true } ], [ "@babel/plugin-proposal-class-properties", { "loose": true } ],
[ "@babel/plugin-proposal-private-methods", {"loose": true } ], [ "@babel/plugin-proposal-private-methods", {"loose": true } ],
[ "@babel/plugin-proposal-optional-chaining" ], [ "@babel/plugin-proposal-optional-chaining" ],

View File

@ -1,4 +1,4 @@
import {defineElement, render, CustomElement, Host} from "../../../packages/csx-custom-elements"; import {defineElement, render, CustomElement, Host, State} from "../../../packages/csx-custom-elements";
import style from './my-todo.scss'; import style from './my-todo.scss';
import {TodoInput} from './todo-input'; import {TodoInput} from './todo-input';
@ -7,10 +7,20 @@ import {TodoItem} from './todo-item';
@defineElement('my-todo') @defineElement('my-todo')
export class MyTodo extends CustomElement{ export class MyTodo extends CustomElement{
uid = 1; uid = 1;
todos = [ @State() todos;
{id: this.uid++, text: "my initial todo", checked: false }, // = [
{id: this.uid++, text: "Learn about Web Components", checked: false }, // {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(){ render(){
return ( return (
@ -26,8 +36,11 @@ export class MyTodo extends CustomElement{
{this.todos.map(item => {this.todos.map(item =>
<todo-item <todo-item
model={ item.id } model={ item.id }
checked={( item.checked )} text={item.text}
>{ item.text }</todo-item> checked={ item.checked }
>
{ item.text }
</todo-item>
)} )}
</ul> </ul>
</section> </section>
@ -36,14 +49,19 @@ export class MyTodo extends CustomElement{
} }
handleSubmit = ({ detail: text }) => { handleSubmit = ({ detail: text }) => {
this.todos = [...this.todos, { id: this.uid++, text, checked: false }]; if(text) {
console.log("Submit rcvd: " + text);
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 indexOf = this.todos.findIndex(t=>t.id===id);
let updated = {...this.todos[indexOf], checked}; if(indexOf>=0) {
this.todos = [...this.todos.slice(0,indexOf), updated, ...this.todos.slice(indexOf+1)]; 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); let indexOf = this.todos.findIndex(t=>t.id===id);
this.todos = [...this.todos.slice(0,indexOf), ...this.todos.slice(indexOf+1)]; this.todos = [...this.todos.slice(0,indexOf), ...this.todos.slice(indexOf+1)];
} }

View File

@ -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'; import style from './todo-item.scss';
@defineElement('todo-item') @defineElement('todo-item')
export class TodoItem extends CustomElement{ 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(){ render(){
return ( return (
@ -27,11 +29,14 @@ export class TodoItem extends CustomElement{
handleChange = ()=>{ handleChange = ()=>{
this.dispatchEvent(new CustomEvent('check', { this.dispatchEvent(new CustomEvent('check', {
detail: (this.checked=!this.checked) detail: {checked: (this.checked=!this.checked), id: this.model},
bubbles: true
})); }));
}; };
handleClick = ()=>{ handleClick = ()=>{
this.dispatchEvent(new CustomEvent('remove', { this.dispatchEvent(new CustomEvent('remove', {
detail: {id: this.model},
bubbles: true
})); }));
}; };
} }