Reworked how decorators are used to get to our initial steps of actually updating the DOM on a state change. *phew*

This commit is contained in:
Miel Truyen 2019-10-23 23:34:45 +02:00
parent 5169c5018d
commit 863adb9449
7 changed files with 200 additions and 107 deletions

View File

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

View File

@ -1,5 +1,43 @@
import {render} from "../vdom";
/**
* The decorators proposal has changed since @babel implemented it. This code will need to change at some point...
*/
export function State() {
return function decorator(target){
let key = target.key;
let descriptor = target.descriptor;
let value = undefined;
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 value
});
descriptor['set'] = function(newValue){
if(newValue!==descriptor.get.call(this)){
value = 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){
value = target.initializer(target);
delete target.initializer;
}
return target;
}
}
/**
* 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{})'
@ -8,15 +46,29 @@ import {render} from "../vdom";
*/
export class CustomElement extends HTMLElement {
connectedCallback() {
if(this.render){
let newVNode = this.render();
render(newVNode, {
host: this
});
}
this.update();
}
disconnectedCallback(){
}
#markedDirty;
#renderedVNode;
update(){
if (this.render) {
let newVNode = this.render();
render(newVNode, {
host: this,
oldVNode: 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...
*/
export function defineElement(tagName, options) {
return function decorator(target){
return function decorator(target) {
// Queue defining element in a finisher, because apparantly thats how the non-legacy decorator proposal works (again, new proposal will be different...)
target.finisher = (finishedTarget)=>{
// Register the tagName as a custom-element with the browser
window.customElements.define(tagName, target, options);
window.customElements.define(tagName, finishedTarget, options);
// Define the chosen tagName on the class itself so our vdom.render-function knows what DOM-Element to create
Object.defineProperty(target, 'tagName', {
Object.defineProperty(finishedTarget, 'tagName', {
value: tagName,
writable: false,
enumerable: false,
configurable: false
});
}
return finishedTarget;
};
return target;
};
}

View File

@ -6,8 +6,6 @@ import {
VNODEPROP_IGNORE,
} from "./constants";
// This is copied from the blog sample right now. it should process jsx but it aint what it needs to be
/**
* @typedef {Object} RenderOptions
* @category VDOM
@ -25,20 +23,39 @@ 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
// 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 Proper updating of a previous rendered vnode
let queue = [{vnode, opts, parent: null}];
let newRoot = undefined;
while(queue.length>0){
let {vnode, opts, parent} = queue.splice(0,1)[0];
let {
/**
* @type {Element}
*/
host
host,
/**
* @type {VNode}
*/
oldVNode
} = opts;
if(VNODE_EXCLUDE[vnode]) return undefined;
if(VNODE_EXCLUDE[vnode]) continue;// Skip
if(!host){
if(!['object', 'function', 'symbol'].includes(typeof(vnode))){
if(!['object', 'function', 'symbol'].includes(typeof(vnode))) {
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");
}
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'){
host = document.createElement(vnode.type);
}else if(vnode?.type?.tagName){
@ -50,13 +67,15 @@ export function render(vnode, opts = {}) {
// Props
if (vnode?.props) {
if (vnode.props.style && typeof (vnode.props.style) === 'object') {
for (let styleKey in vnode.props.style) {
host.style[ styleKey ] = vnode.props.style[ styleKey ];
let props = vnode.props;
if (props.style && typeof (props.style) === 'object') {
for (let styleKey in props.style) {
host.style[ styleKey ] = props.style[ styleKey ];
}
}
for (let key in vnode.props) {
let val = vnode.props[key];
for (let key in props) {
let val = props[key];
if(VNODEPROP_IGNORE[key]){
// NO-OP
}else if(VNODEPROP_DIRECT[key]){
@ -91,29 +110,43 @@ export function render(vnode, opts = {}) {
// Children
if (vnode?.children) {
let queue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
while(queue.length){
let child = queue.splice(0,1)[0];
let childVQueue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
let childQueue = [];
while(childVQueue.length){
let child = childVQueue.splice(0,1)[0];
// TODO support chidl instances of Element
if(child instanceof Array){
queue.splice(0,0,...child);
childVQueue.splice(0,0,...child);
}else{
if(child?.type === ShadowDOM){
let shadow = host.attachShadow({mode: 'open'});
render({children: child.children}, {
childQueue.push({
vnode: child,
opts: {
...opts,
host: shadow
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
}
});
}else{
let el = child instanceof Element? child : render(child, {
...opts,
host: undefined
});
if(el!==undefined) host.appendChild(el);
}
}
queue.splice(0,0,...childQueue);
}
if(parent){
if(parent.oldChild){
parent.node.replaceChild(host, parent.oldChild);
}else{
parent.node.appendChild(host);
}
}else{
newRoot = host;
}
}
return host;
return newRoot;
}

View File

@ -7,7 +7,7 @@
}]
],
"plugins": [
[ "@babel/plugin-proposal-decorators", { "legacy": true }],
[ "@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }],
[ "@babel/plugin-proposal-class-properties", { "loose": true } ],
[ "@babel/plugin-proposal-private-methods", {"loose": true } ],
[ "@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 {TodoInput} from './todo-input';
@ -7,7 +7,8 @@ import {TodoItem} from './todo-item';
@defineElement('my-todo')
export class MyTodo extends CustomElement{
uid = 1;
todos = [
// @State Won't work;
@State() todos = [
{id: this.uid++, text: "my initial todo", checked: false },
{id: this.uid++, text: "Learn about Web Components", checked: false },
];
@ -36,7 +37,10 @@ export class MyTodo extends CustomElement{
}
handleSubmit = ({ detail: text }) => {
if(text) {
console.log("Submit rcvd: " + text);
this.todos = [...this.todos, { id: this.uid++, text, checked: false }];
}
};
handleCheck = ({detail: checked}, id) => {
let indexOf = this.todos.findIndex(t=>t.id===id);

View File

@ -27,7 +27,7 @@ export class TodoItem extends CustomElement{
handleChange = ()=>{
this.dispatchEvent(new CustomEvent('check', {
detail: (this.checked=!this.checked)
detail: (this.checked=!this.checked),
}));
};
handleClick = ()=>{