A working version but using non-legacy decorators should be reconsidered, and probably quite a few bugs left in the render loop
This commit is contained in:
parent
fc527cb156
commit
3a135a30d1
@ -3,22 +3,26 @@ import {render} from "../vdom";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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...
|
||||||
|
* 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() {
|
export function State() {
|
||||||
return function decorator(target){
|
return function decorator(target){
|
||||||
let key = target.key;
|
let key = target.key;
|
||||||
let descriptor = target.descriptor;
|
let descriptor = target.descriptor;
|
||||||
let value = undefined;
|
let valueMap = new WeakMap();
|
||||||
let {get: oldGet, set: oldSet} = descriptor;
|
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)
|
// 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(){
|
descriptor['get'] = oldGet || (function(){
|
||||||
return value
|
return valueMap.get(this)
|
||||||
});
|
});
|
||||||
descriptor['set'] = function(newValue){
|
descriptor['set'] = function(newValue){
|
||||||
if(newValue!==descriptor.get.call(this)){
|
let oldValue = descriptor.get.call(this);
|
||||||
value = newValue;
|
if(newValue!==oldValue){
|
||||||
|
valueMap.set(this,newValue);
|
||||||
this.markDirty && this.markDirty();
|
this.markDirty && this.markDirty();
|
||||||
|
|
||||||
}
|
}
|
||||||
if(oldSet) return oldSet.call(this, newValue);
|
if(oldSet) return oldSet.call(this, newValue);
|
||||||
};
|
};
|
||||||
@ -29,7 +33,7 @@ export function State() {
|
|||||||
|
|
||||||
// CAUTION: this is again dangerous, the initialize function should be called right before the constructor, but after it was fully defined.
|
// CAUTION: this is again dangerous, the initialize function should be called right before the constructor, but after it was fully defined.
|
||||||
if(target.initializer){
|
if(target.initializer){
|
||||||
value = target.initializer(target);
|
valueMap.set(target, target.initializer(target));
|
||||||
delete target.initializer;
|
delete target.initializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +63,7 @@ export class CustomElement extends HTMLElement {
|
|||||||
let newVNode = this.render();
|
let newVNode = this.render();
|
||||||
render(newVNode, {
|
render(newVNode, {
|
||||||
host: this,
|
host: this,
|
||||||
oldVNode: this.#renderedVNode,
|
old: this.#renderedVNode,
|
||||||
});
|
});
|
||||||
this.#renderedVNode = newVNode;
|
this.#renderedVNode = newVNode;
|
||||||
}
|
}
|
||||||
|
|||||||
17
packages/csx-custom-elements/src/vdom/as-vnode.js
Normal file
17
packages/csx-custom-elements/src/vdom/as-vnode.js
Normal 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;
|
||||||
|
}
|
||||||
@ -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');
|
|
||||||
@ -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";
|
||||||
@ -1,201 +1,238 @@
|
|||||||
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};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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 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
|
// TODO Proper updating of a previous rendered vnode (we're working on it!)
|
||||||
|
|
||||||
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}
|
*
|
||||||
|
* @type {VRenderState}
|
||||||
*/
|
*/
|
||||||
host,
|
let state = {
|
||||||
/**
|
keyedElements: new Map(),
|
||||||
* @type {VNode}
|
refs: [],
|
||||||
*/
|
queue: [{
|
||||||
oldVNode
|
// Start item
|
||||||
} = opts;
|
item: {
|
||||||
|
document: opts.document||document,
|
||||||
if(VNODE_EXCLUDE[vnode]) continue;// Skip
|
host: opts.host,
|
||||||
|
parent: opts.parent,
|
||||||
if(!['object', 'function', 'symbol'].includes(typeof(vnode))) {
|
old: opts.old,
|
||||||
// Presumed primitive type -> TEXT
|
vnode: vnode
|
||||||
if(!host) {
|
|
||||||
host = document.createTextNode(vnode);
|
|
||||||
}else{
|
|
||||||
host.data = vnode;
|
|
||||||
}
|
|
||||||
}else if(vnode?.type === ShadowDOM) {
|
|
||||||
// ShadowDOM-keyword -> SHADOW
|
|
||||||
if(!parent.node) {
|
|
||||||
throw new Error("Can't identify to what element ShadowDOM is to be attached");
|
|
||||||
}
|
|
||||||
if(!host) {
|
|
||||||
host = parent.node.attachShadow({ mode: 'open' });
|
|
||||||
}else{
|
|
||||||
host = parent.node.shadowRoot;
|
|
||||||
}
|
|
||||||
}else if(vnode?.type === Host){
|
|
||||||
// Host-keyword -> PARENT
|
|
||||||
let shadowParent = parent?.node || host;
|
|
||||||
if(!shadowParent) {
|
|
||||||
throw new Error("Can't identify to what element ShadowDOM is to be attached");
|
|
||||||
}
|
|
||||||
host = shadowParent;
|
|
||||||
}else if(!host){
|
|
||||||
if(typeof(vnode?.type) === 'string'){
|
|
||||||
// String-type -> DOM
|
|
||||||
host = document.createElement(vnode.type);
|
|
||||||
}else if(vnode?.type?.tagName){
|
|
||||||
// Object-type -> CUSTOM-ELEMENT
|
|
||||||
host = document.createElement(vnode.type.tagName);
|
|
||||||
}else{
|
|
||||||
throw new Error("Unrecognized vnode type", vnode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props
|
|
||||||
if (vnode?.props) {
|
|
||||||
let props = vnode.props;
|
|
||||||
let oldProps = oldVNode?.props || {};
|
|
||||||
let propOps = [
|
|
||||||
...Object.entries(oldProps).filter(([key,val])=>!props.hasOwnProperty(key)).map(([key,value])=>({delete:1, key, value})),
|
|
||||||
...Object.entries(props).map(([key,value])=>({delete:1, key, value, prevValue: oldProps[key]}))
|
|
||||||
];
|
|
||||||
|
|
||||||
for(let {deleted, key, value, prevValue} of propOps){
|
|
||||||
if(VNODEPROP_IGNORE[key]){
|
|
||||||
// NO-OP
|
|
||||||
}else if(VNODEPROP_DIRECT[key]){
|
|
||||||
host[key] = value;
|
|
||||||
}else{
|
|
||||||
if(value===prevValue) {
|
|
||||||
// NO-OP, hasn't changed
|
|
||||||
}else if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
|
|
||||||
host[key] = value;
|
|
||||||
}else if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){
|
|
||||||
// Convert camelCase to dash-case
|
|
||||||
let eventName = key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())});
|
|
||||||
if(deleted) {
|
|
||||||
host.removeEventListener(eventName, value);
|
|
||||||
}else if(value instanceof Function){
|
|
||||||
host.addEventListener(eventName, value);
|
|
||||||
}else{
|
|
||||||
new Error("Unsupported event-handler");
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
if(deleted){
|
|
||||||
host.removeAttribute(key);
|
|
||||||
}else{
|
|
||||||
if (value === false || value===null || value==='') {
|
|
||||||
host.removeAttribute(key);
|
|
||||||
} else if (value === true) {
|
|
||||||
host.setAttribute(key, "");
|
|
||||||
} else {
|
|
||||||
host.setAttribute(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (props.style && typeof (props.style) === 'object') {
|
|
||||||
// for (let styleKey in props.style) {
|
|
||||||
// host.style[ styleKey ] = props.style[ styleKey ];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Children
|
|
||||||
if (vnode?.children) {
|
|
||||||
// TODO: Properly iterate the children update VNode only where necessary (we'll probably just reach another point where we throw the whole thing upside down again...)
|
|
||||||
let childVQueue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
|
|
||||||
let oldChildVQueue = oldVNode?.children instanceof Array? oldVNode.children.slice()
|
|
||||||
: oldVNode?.children!==null&&oldVNode?.children!==undefined? [oldVNode.children] : [];
|
|
||||||
let childQueue = [];
|
|
||||||
let oldChildQueue = [];
|
|
||||||
let hostIndex = 0;
|
|
||||||
while(childVQueue.length){
|
|
||||||
let child = childVQueue.splice(0,1)[0];
|
|
||||||
let oldChild = oldChildVQueue.splice(0,1)[0];
|
|
||||||
if(child instanceof Array){
|
|
||||||
childVQueue.splice(0,0,...child);
|
|
||||||
if(oldChild instanceof Array){
|
|
||||||
// here we typically know we should be checking Refs
|
|
||||||
oldChildVQueue.splice(0,0,...oldChild);
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
// Really dirty way here to pass in what was probably the old element for this, and it must absolutely go
|
|
||||||
if(oldChild === child){
|
|
||||||
console.log("Not changed, skipping");
|
|
||||||
hostIndex++;
|
|
||||||
}else{
|
|
||||||
let oldChildEl = oldVNode?host.childNodes[hostIndex]:null;
|
|
||||||
|
|
||||||
console.log("Updating:", child, oldChild);
|
|
||||||
|
|
||||||
let updateNode = (oldChild?.type === child?.type);
|
|
||||||
if(updateNode){
|
|
||||||
hostIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
childQueue.push({
|
|
||||||
vnode: child,
|
|
||||||
opts: {
|
|
||||||
...opts,
|
|
||||||
host: updateNode? oldChildEl : undefined,
|
|
||||||
oldVNode: updateNode? oldChild : undefined
|
|
||||||
},
|
},
|
||||||
parent: { // TODO specify a previous child or index here if need be...
|
meta: getNodeMeta(vnode)
|
||||||
node:host,
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
// Really dirty way here to pass in what was probably the old element for this, and it must absolutely go
|
let newRoot = undefined;
|
||||||
oldChild: oldChildEl
|
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);
|
||||||
|
|
||||||
|
// Create the element if no matching existing element was set
|
||||||
|
let newlyCreated = false;
|
||||||
|
if (!item.host) {
|
||||||
|
item.host = renderer.create(item);
|
||||||
|
newlyCreated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the element
|
||||||
|
renderer.update(item);
|
||||||
|
|
||||||
|
// Update children
|
||||||
|
if(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)
|
||||||
|
/**
|
||||||
|
* @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 meta = getNodeMeta(next);
|
||||||
|
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)
|
||||||
|
childTypes.add(childType);// Tract that children of this type exist and should be iterated later
|
||||||
|
vChildren[childType] = vChildren[childType] || []; // Make sure the array exists
|
||||||
|
vChildren[childType].push({
|
||||||
|
item: {
|
||||||
|
...item,
|
||||||
|
old: undefined,
|
||||||
|
vnode: next,
|
||||||
|
host: undefined,
|
||||||
|
parent: item.host
|
||||||
|
},
|
||||||
|
meta: meta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
queue.splice(0,0,...childQueue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(parent){
|
// Flatten and organize old-children
|
||||||
if(parent.oldChild){
|
/**
|
||||||
if(parent.oldChild !== host) {
|
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
|
||||||
console.log(parent.node, host);
|
*/
|
||||||
parent.node.replaceChild(host, parent.oldChild);
|
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{
|
||||||
|
let meta = getNodeMeta(next);
|
||||||
|
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){
|
||||||
|
childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
|
||||||
|
if(curElement){
|
||||||
|
childElement = curElement;
|
||||||
|
curElement = curElement.nextSibling;
|
||||||
}
|
}
|
||||||
}else{
|
|
||||||
parent.node.appendChild(host);
|
|
||||||
}
|
}
|
||||||
}else{
|
childTypes.add(childType);// Tract that children of this type exist and should be iterated later
|
||||||
newRoot = host;
|
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);
|
||||||
|
}
|
||||||
|
//item.parent.appendChild(item.host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!item.parent) newRoot = item.host;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// //if(item.host instanceof Element || item.host instanceof Text){// Not good enough, both Element and Text inherit from Node, which would make more sense, but ShadowRoot also inherits from node, and unlike the others it shouldnt be appended as a child-element
|
||||||
|
// if(item.host instanceof ShadowRoot){
|
||||||
|
// console.log("I expected a crash here...", item);
|
||||||
|
// }
|
||||||
|
// if(item.host instanceof Node){
|
||||||
|
// let parent = item.parent;
|
||||||
|
// if(parent){
|
||||||
|
// //parent.insertBefore(nextChildNode, childNodes[i])// When its not the last el...
|
||||||
|
// parent.appendChild(item.host);
|
||||||
|
// }else{
|
||||||
|
// newRoot = item.host;// This could be implemented as refs...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if(item.host instanceof ShadowRoot){
|
||||||
|
// //console.log("Does this work then?..", item);
|
||||||
|
// // item.parent.removeChild(item.host);// No it does not!
|
||||||
|
// }
|
||||||
|
}
|
||||||
return newRoot;
|
return newRoot;
|
||||||
}
|
}
|
||||||
37
packages/csx-custom-elements/src/vdom/renderers/hostnode.js
Normal file
37
packages/csx-custom-elements/src/vdom/renderers/hostnode.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
4
packages/csx-custom-elements/src/vdom/renderers/index.js
Normal file
4
packages/csx-custom-elements/src/vdom/renderers/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./hostnode";
|
||||||
|
export * from "./nodetree";
|
||||||
|
export * from "./nodeprimitive";
|
||||||
|
export * from "./shadownode";
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
106
packages/csx-custom-elements/src/vdom/renderers/nodetree.js
Normal file
106
packages/csx-custom-elements/src/vdom/renderers/nodetree.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
4
packages/csx-custom-elements/src/vdom/types/index.js
Normal file
4
packages/csx-custom-elements/src/vdom/types/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./vnode";
|
||||||
|
export * from "./render-item";
|
||||||
|
export * from "./render-state";
|
||||||
|
export * from "./vnode-renderer";
|
||||||
14
packages/csx-custom-elements/src/vdom/types/render-item.js
Normal file
14
packages/csx-custom-elements/src/vdom/types/render-item.js
Normal 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
|
||||||
|
**/
|
||||||
36
packages/csx-custom-elements/src/vdom/types/render-state.js
Normal file
36
packages/csx-custom-elements/src/vdom/types/render-state.js
Normal 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)
|
||||||
|
**/
|
||||||
@ -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
|
||||||
|
*/
|
||||||
39
packages/csx-custom-elements/src/vdom/types/vnode.js
Normal file
39
packages/csx-custom-elements/src/vdom/types/vnode.js
Normal 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
|
||||||
|
**/
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -7,11 +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;
|
||||||
// @State Won't work;
|
@State() todos;
|
||||||
@State() todos = [
|
// = [
|
||||||
|
// {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: "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 },
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
return (
|
return (
|
||||||
@ -27,7 +36,8 @@ 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}
|
||||||
|
checked={ item.checked }
|
||||||
>
|
>
|
||||||
{ item.text }
|
{ item.text }
|
||||||
</todo-item>
|
</todo-item>
|
||||||
@ -44,12 +54,14 @@ export class MyTodo extends CustomElement{
|
|||||||
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);
|
||||||
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)];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,12 +29,13 @@ 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
|
bubbles: true
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
handleClick = ()=>{
|
handleClick = ()=>{
|
||||||
this.dispatchEvent(new CustomEvent('remove', {
|
this.dispatchEvent(new CustomEvent('remove', {
|
||||||
|
detail: {id: this.model},
|
||||||
bubbles: true
|
bubbles: true
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user