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": [
|
"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" ],
|
||||||
|
|||||||
@ -1,5 +1,43 @@
|
|||||||
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...
|
||||||
|
*/
|
||||||
|
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:
|
* 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 +46,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,
|
||||||
|
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...
|
* 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) {
|
||||||
|
// 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
|
// 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
|
// 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,
|
value: tagName,
|
||||||
writable: false,
|
writable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
configurable: false
|
configurable: false
|
||||||
});
|
});
|
||||||
}
|
return finishedTarget;
|
||||||
|
};
|
||||||
|
return target;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@ -6,8 +6,6 @@ import {
|
|||||||
VNODEPROP_IGNORE,
|
VNODEPROP_IGNORE,
|
||||||
} from "./constants";
|
} 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
|
* @typedef {Object} RenderOptions
|
||||||
* @category VDOM
|
* @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..
|
// 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 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?!)
|
||||||
|
// 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 {
|
let {
|
||||||
/**
|
/**
|
||||||
* @type {Element}
|
* @type {Element}
|
||||||
*/
|
*/
|
||||||
host
|
host,
|
||||||
|
/**
|
||||||
|
* @type {VNode}
|
||||||
|
*/
|
||||||
|
oldVNode
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
if(VNODE_EXCLUDE[vnode]) return undefined;
|
if(VNODE_EXCLUDE[vnode]) continue;// Skip
|
||||||
|
|
||||||
if(!host){
|
if(!host){
|
||||||
if(!['object', 'function', 'symbol'].includes(typeof(vnode))) {
|
if(!['object', 'function', 'symbol'].includes(typeof(vnode))) {
|
||||||
host = document.createTextNode(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'){
|
}else if(typeof(vnode?.type) === 'string'){
|
||||||
host = document.createElement(vnode.type);
|
host = document.createElement(vnode.type);
|
||||||
}else if(vnode?.type?.tagName){
|
}else if(vnode?.type?.tagName){
|
||||||
@ -50,13 +67,15 @@ export function render(vnode, opts = {}) {
|
|||||||
|
|
||||||
// Props
|
// Props
|
||||||
if (vnode?.props) {
|
if (vnode?.props) {
|
||||||
if (vnode.props.style && typeof (vnode.props.style) === 'object') {
|
let props = vnode.props;
|
||||||
for (let styleKey in vnode.props.style) {
|
|
||||||
host.style[ styleKey ] = vnode.props.style[ styleKey ];
|
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) {
|
for (let key in props) {
|
||||||
let val = vnode.props[key];
|
let val = props[key];
|
||||||
if(VNODEPROP_IGNORE[key]){
|
if(VNODEPROP_IGNORE[key]){
|
||||||
// NO-OP
|
// NO-OP
|
||||||
}else if(VNODEPROP_DIRECT[key]){
|
}else if(VNODEPROP_DIRECT[key]){
|
||||||
@ -91,29 +110,43 @@ export function render(vnode, opts = {}) {
|
|||||||
|
|
||||||
// Children
|
// Children
|
||||||
if (vnode?.children) {
|
if (vnode?.children) {
|
||||||
let queue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
|
let childVQueue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
|
||||||
while(queue.length){
|
let childQueue = [];
|
||||||
let child = queue.splice(0,1)[0];
|
while(childVQueue.length){
|
||||||
|
let child = childVQueue.splice(0,1)[0];
|
||||||
|
// TODO support chidl instances of Element
|
||||||
if(child instanceof Array){
|
if(child instanceof Array){
|
||||||
queue.splice(0,0,...child);
|
childVQueue.splice(0,0,...child);
|
||||||
}else{
|
}else{
|
||||||
if(child?.type === ShadowDOM){
|
|
||||||
let shadow = host.attachShadow({mode: 'open'});
|
childQueue.push({
|
||||||
render({children: child.children}, {
|
vnode: child,
|
||||||
|
opts: {
|
||||||
...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,
|
queue.splice(0,0,...childQueue);
|
||||||
host: undefined
|
|
||||||
});
|
|
||||||
if(el!==undefined) host.appendChild(el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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": [
|
"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" ],
|
||||||
|
|||||||
@ -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,7 +7,8 @@ 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 Won't work;
|
||||||
|
@State() todos = [
|
||||||
{id: this.uid++, text: "my initial todo", checked: false },
|
{id: this.uid++, text: "my initial todo", checked: false },
|
||||||
{id: this.uid++, text: "Learn about Web Components", checked: false },
|
{id: this.uid++, text: "Learn about Web Components", checked: false },
|
||||||
];
|
];
|
||||||
@ -36,7 +37,10 @@ export class MyTodo extends CustomElement{
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = ({ detail: text }) => {
|
handleSubmit = ({ detail: text }) => {
|
||||||
|
if(text) {
|
||||||
|
console.log("Submit rcvd: " + text);
|
||||||
this.todos = [...this.todos, { id: this.uid++, text, checked: false }];
|
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);
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class TodoItem extends CustomElement{
|
|||||||
|
|
||||||
handleChange = ()=>{
|
handleChange = ()=>{
|
||||||
this.dispatchEvent(new CustomEvent('check', {
|
this.dispatchEvent(new CustomEvent('check', {
|
||||||
detail: (this.checked=!this.checked)
|
detail: (this.checked=!this.checked),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
handleClick = ()=>{
|
handleClick = ()=>{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user