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:
parent
5169c5018d
commit
863adb9449
@ -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" ],
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
@ -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))) {
|
||||
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;
|
||||
}
|
||||
@ -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" ],
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = ()=>{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user